|
1 | 1 | import asyncio
|
| 2 | +import dateutil.parser |
2 | 3 | import logging
|
| 4 | +import math |
3 | 5 | import re
|
| 6 | +import time |
4 | 7 | import unittest
|
5 | 8 | import os
|
6 | 9 | from datetime import datetime, timezone
|
7 | 10 | from io import StringIO
|
8 | 11 |
|
| 12 | +import pandas |
9 | 13 | import pytest
|
10 | 14 | import warnings
|
11 | 15 | from aioresponses import aioresponses
|
@@ -199,30 +203,151 @@ async def test_write_empty_data(self):
|
199 | 203 |
|
200 | 204 | self.assertEqual(True, response)
|
201 | 205 |
|
| 206 | + def gen_fractional_utc(self, nano, precision) -> str: |
| 207 | + raw_sec = nano / 1_000_000_000 |
| 208 | + if precision == WritePrecision.NS: |
| 209 | + rem = f"{nano % 1_000_000_000}".rjust(9,"0").rstrip("0") |
| 210 | + return (datetime.fromtimestamp(math.floor(raw_sec), tz=timezone.utc) |
| 211 | + .isoformat() |
| 212 | + .replace("+00:00", "") + f".{rem}Z") |
| 213 | + #f".{rem}Z")) |
| 214 | + elif precision == WritePrecision.US: |
| 215 | + # rem = f"{round(nano / 1_000) % 1_000_000}"#.ljust(6,"0") |
| 216 | + return (datetime.fromtimestamp(round(raw_sec,6), tz=timezone.utc) |
| 217 | + .isoformat() |
| 218 | + .replace("+00:00","") |
| 219 | + .strip("0") + "Z" |
| 220 | + ) |
| 221 | + elif precision == WritePrecision.MS: |
| 222 | + #rem = f"{round(nano / 1_000_000) % 1_000}".rjust(3, "0") |
| 223 | + return (datetime.fromtimestamp(round(raw_sec,3), tz=timezone.utc) |
| 224 | + .isoformat() |
| 225 | + .replace("+00:00","") |
| 226 | + .strip("0") + "Z" |
| 227 | + ) |
| 228 | + elif precision == WritePrecision.S: |
| 229 | + return (datetime.fromtimestamp(round(raw_sec), tz=timezone.utc) |
| 230 | + .isoformat() |
| 231 | + .replace("+00:00","Z")) |
| 232 | + else: |
| 233 | + raise ValueError(f"Unknown precision: {precision}") |
| 234 | + |
| 235 | + |
202 | 236 | @async_test
|
203 | 237 | async def test_write_points_different_precision(self):
|
| 238 | + now_ns = time.time_ns() |
| 239 | + now_us = now_ns / 1_000 |
| 240 | + now_ms = now_us / 1_000 |
| 241 | + now_s = now_ms / 1_000 |
| 242 | + |
| 243 | + now_date_s = self.gen_fractional_utc(now_ns, WritePrecision.S) |
| 244 | + now_date_ms = self.gen_fractional_utc(now_ns, WritePrecision.MS) |
| 245 | + now_date_us = self.gen_fractional_utc(now_ns, WritePrecision.US) |
| 246 | + now_date_ns = self.gen_fractional_utc(now_ns, WritePrecision.NS) |
| 247 | + |
| 248 | + points = { |
| 249 | + WritePrecision.S: [], |
| 250 | + WritePrecision.MS: [], |
| 251 | + WritePrecision.US: [], |
| 252 | + WritePrecision.NS: [] |
| 253 | + } |
| 254 | + |
| 255 | + expected = {} |
| 256 | + |
204 | 257 | measurement = generate_name("measurement")
|
205 |
| - _point1 = Point(measurement).tag("location", "Prague").field("temperature", 25.3) \ |
206 |
| - .time(datetime.fromtimestamp(0, tz=timezone.utc), write_precision=WritePrecision.S) |
207 |
| - _point2 = Point(measurement).tag("location", "New York").field("temperature", 24.3) \ |
208 |
| - .time(datetime.fromtimestamp(1, tz=timezone.utc), write_precision=WritePrecision.MS) |
209 |
| - _point3 = Point(measurement).tag("location", "Berlin").field("temperature", 24.3) \ |
210 |
| - .time(datetime.fromtimestamp(2, tz=timezone.utc), write_precision=WritePrecision.NS) |
211 |
| - await self.client.write_api().write(bucket="my-bucket", record=[_point1, _point2, _point3], |
| 258 | + # basic date-time value |
| 259 | + points[WritePrecision.S].append(Point(measurement).tag("method", "SecDateTime").field("temperature", 25.3) \ |
| 260 | + .time(datetime.fromtimestamp(round(now_s), tz=timezone.utc), write_precision=WritePrecision.S)) |
| 261 | + expected['SecDateTime'] = now_date_s |
| 262 | + points[WritePrecision.MS].append(Point(measurement).tag("method", "MilDateTime").field("temperature", 24.3) \ |
| 263 | + .time(datetime.fromtimestamp(round(now_s,3), tz=timezone.utc), write_precision=WritePrecision.MS)) |
| 264 | + expected['MilDateTime'] = now_date_ms |
| 265 | + points[WritePrecision.US].append(Point(measurement).tag("method", "MicDateTime").field("temperature", 24.3) \ |
| 266 | + .time(datetime.fromtimestamp(round(now_s,6), tz=timezone.utc), write_precision=WritePrecision.US)) |
| 267 | + expected['MicDateTime'] = now_date_us |
| 268 | + # N.B. datetime does not handle nanoseconds |
| 269 | +# points[WritePrecision.NS].append(Point(measurement).tag("method", "NanDateTime").field("temperature", 24.3) \ |
| 270 | +# .time(datetime.fromtimestamp(now_s, tz=timezone.utc), write_precision=WritePrecision.NS)) |
| 271 | + |
| 272 | + # long timestamps based on POSIX time |
| 273 | + points[WritePrecision.S].append(Point(measurement).tag("method", "SecPosix").field("temperature", 24.3) \ |
| 274 | + .time(round(now_s), write_precision=WritePrecision.S)) |
| 275 | + expected['SecPosix'] = now_date_s |
| 276 | + points[WritePrecision.MS].append(Point(measurement).tag("method", "MilPosix").field("temperature", 24.3) \ |
| 277 | + .time(round(now_ms), write_precision=WritePrecision.MS)) |
| 278 | + expected['MilPosix'] = now_date_ms |
| 279 | + points[WritePrecision.US].append(Point(measurement).tag("method", "MicPosix").field("temperature", 24.3) \ |
| 280 | + .time(round(now_us), write_precision=WritePrecision.US)) |
| 281 | + expected['MicPosix'] = now_date_us |
| 282 | + points[WritePrecision.NS].append(Point(measurement).tag("method", "NanPosix").field("temperature", 24.3) \ |
| 283 | + .time(now_ns, write_precision=WritePrecision.NS)) |
| 284 | + expected['NanPosix'] = now_date_ns |
| 285 | + |
| 286 | + # ISO Zulu datetime with ms, us and ns e.g. "2024-09-27T13:17:16.412399728Z" |
| 287 | + points[WritePrecision.S].append(Point(measurement).tag("method", "SecDTZulu").field("temperature", 24.3) \ |
| 288 | + .time(now_date_s, write_precision=WritePrecision.S)) |
| 289 | + expected['SecDTZulu'] = now_date_s |
| 290 | + points[WritePrecision.MS].append(Point(measurement).tag("method", "MilDTZulu").field("temperature", 24.3) \ |
| 291 | + .time(now_date_ms, write_precision=WritePrecision.MS)) |
| 292 | + expected['MilDTZulu'] = now_date_ms |
| 293 | + points[WritePrecision.US].append(Point(measurement).tag("method", "MicDTZulu").field("temperature", 24.3) \ |
| 294 | + .time(now_date_us, write_precision=WritePrecision.US)) |
| 295 | + expected['MicDTZulu'] = now_date_us |
| 296 | + # This keeps resulting in micro second resolution in response |
| 297 | +# points[WritePrecision.NS].append(Point(measurement).tag("method", "NanDTZulu").field("temperature", 24.3) \ |
| 298 | +# .time(now_date_ns, write_precision=WritePrecision.NS)) |
| 299 | + |
| 300 | + recs = [x for x in [v for v in points.values()]] |
| 301 | + |
| 302 | + await self.client.write_api().write(bucket="my-bucket", record=recs, |
212 | 303 | write_precision=WritePrecision.NS)
|
213 | 304 | query = f'''
|
214 | 305 | from(bucket:"my-bucket")
|
215 | 306 | |> range(start: 0)
|
216 | 307 | |> filter(fn: (r) => r["_measurement"] == "{measurement}")
|
217 |
| - |> keep(columns: ["_time"]) |
| 308 | + |> keep(columns: ["method","_time"]) |
218 | 309 | '''
|
219 | 310 | query_api = self.client.query_api()
|
220 | 311 |
|
| 312 | + # ensure calls fully processed on server |
| 313 | + await asyncio.sleep(1) |
| 314 | + |
221 | 315 | raw = await query_api.query_raw(query)
|
222 |
| - self.assertEqual(8, len(raw.splitlines())) |
223 |
| - self.assertEqual(',,0,1970-01-01T00:00:02Z', raw.splitlines()[4]) |
224 |
| - self.assertEqual(',,0,1970-01-01T00:00:01Z', raw.splitlines()[5]) |
225 |
| - self.assertEqual(',,0,1970-01-01T00:00:00Z', raw.splitlines()[6]) |
| 316 | + linesRaw = raw.splitlines()[4:] |
| 317 | + |
| 318 | + lines = [] |
| 319 | + for lnr in linesRaw: |
| 320 | + lines.append(lnr[2:].split(",")) |
| 321 | + |
| 322 | + def get_time_for_method(lines, method): |
| 323 | + for l in lines: |
| 324 | + if l[2] == method: |
| 325 | + return l[1] |
| 326 | + return "" |
| 327 | + |
| 328 | + self.assertEqual(15, len(raw.splitlines())) |
| 329 | + |
| 330 | + for key in expected: |
| 331 | + t = get_time_for_method(lines,key) |
| 332 | + comp_time = dateutil.parser.isoparse(get_time_for_method(lines,key)) |
| 333 | + target_time = dateutil.parser.isoparse(expected[key]) |
| 334 | + self.assertEqual(target_time.date(), comp_time.date()) |
| 335 | + self.assertEqual(target_time.hour, comp_time.hour) |
| 336 | + self.assertEqual(target_time.second,comp_time.second) |
| 337 | + dif = abs(target_time.microsecond - comp_time.microsecond) |
| 338 | + if key[:3] == "Sec": |
| 339 | + # Already tested |
| 340 | + pass |
| 341 | + elif key[:3] == "Mil": |
| 342 | + # may be slight rounding differences |
| 343 | + self.assertLess(dif, 1500, f"failed to match timestamp for {key} {target_time} != {comp_time}") |
| 344 | + elif key[:3] == "Mic": |
| 345 | + # may be slight rounding differences |
| 346 | + self.assertLess(dif, 150, f"failed to match timestamp for {key} {target_time} != {comp_time}") |
| 347 | + elif key[:3] == "Nan": |
| 348 | + self.assertEqual(expected[key], get_time_for_method(lines, key)) |
| 349 | + else: |
| 350 | + raise Exception(f"Unhandled key {key}") |
226 | 351 |
|
227 | 352 | @async_test
|
228 | 353 | async def test_delete_api(self):
|
|
0 commit comments