flow.record 3.11.dev4__tar.gz → 3.12.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {flow.record-3.11.dev4/flow.record.egg-info → flow.record-3.12.dev1}/PKG-INFO +1 -1
  2. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/elastic.py +1 -1
  3. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/base.py +3 -2
  4. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/fieldtypes/__init__.py +62 -32
  5. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/jsonpacker.py +1 -1
  6. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/packer.py +9 -6
  7. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/stream.py +2 -2
  8. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/tools/rdump.py +5 -4
  9. flow.record-3.12.dev1/flow/record/version.py +4 -0
  10. {flow.record-3.11.dev4 → flow.record-3.12.dev1/flow.record.egg-info}/PKG-INFO +1 -1
  11. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow.record.egg-info/requires.txt +6 -0
  12. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/pyproject.toml +2 -0
  13. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/_utils.py +2 -2
  14. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_fieldtypes.py +78 -24
  15. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_json_packer.py +2 -2
  16. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_multi_timestamp.py +14 -12
  17. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_packer.py +5 -3
  18. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_rdump.py +132 -0
  19. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_record_adapter.py +1 -1
  20. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_regression.py +2 -2
  21. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_selector.py +2 -2
  22. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tox.ini +1 -1
  23. flow.record-3.11.dev4/flow/record/version.py +0 -4
  24. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/COPYRIGHT +0 -0
  25. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/LICENSE +0 -0
  26. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/MANIFEST.in +0 -0
  27. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/README.md +0 -0
  28. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/examples/filesystem.py +0 -0
  29. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/examples/passivedns.py +0 -0
  30. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/examples/records.json +0 -0
  31. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/examples/tcpconn.py +0 -0
  32. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/__init__.py +0 -0
  33. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/__init__.py +0 -0
  34. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/archive.py +0 -0
  35. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/avro.py +0 -0
  36. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/broker.py +0 -0
  37. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/csvfile.py +0 -0
  38. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/jsonfile.py +0 -0
  39. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/line.py +0 -0
  40. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/mongo.py +0 -0
  41. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/split.py +0 -0
  42. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/splunk.py +0 -0
  43. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/stream.py +0 -0
  44. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/text.py +0 -0
  45. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/adapter/xlsx.py +0 -0
  46. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/exceptions.py +0 -0
  47. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/fieldtypes/credential.py +0 -0
  48. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/fieldtypes/net/__init__.py +0 -0
  49. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/fieldtypes/net/ip.py +0 -0
  50. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/fieldtypes/net/ipv4.py +0 -0
  51. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/fieldtypes/net/tcp.py +0 -0
  52. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/fieldtypes/net/udp.py +0 -0
  53. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/selector.py +0 -0
  54. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/tools/__init__.py +0 -0
  55. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/tools/geoip.py +0 -0
  56. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/utils.py +0 -0
  57. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow/record/whitelist.py +0 -0
  58. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow.record.egg-info/SOURCES.txt +0 -0
  59. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow.record.egg-info/dependency_links.txt +0 -0
  60. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow.record.egg-info/entry_points.txt +0 -0
  61. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/flow.record.egg-info/top_level.txt +0 -0
  62. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/setup.cfg +0 -0
  63. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/__init__.py +0 -0
  64. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/docs/Makefile +0 -0
  65. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/docs/conf.py +0 -0
  66. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/docs/index.rst +0 -0
  67. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/selector_explain_example.py +0 -0
  68. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/standalone_test.py +0 -0
  69. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_avro_adapter.py +0 -0
  70. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_compiled_selector.py +0 -0
  71. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_deprecations.py +0 -0
  72. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_fieldtype_ip.py +0 -0
  73. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_json_record_adapter.py +0 -0
  74. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_record.py +0 -0
  75. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_record_descriptor.py +0 -0
  76. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/test_splunk_adapter.py +0 -0
  77. {flow.record-3.11.dev4 → flow.record-3.12.dev1}/tests/utils_inspect.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flow.record
3
- Version: 3.11.dev4
3
+ Version: 3.12.dev1
4
4
  Summary: A library for defining and creating structured data (called records) that can be streamed to disk or piped to other tools that use flow.record
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -99,7 +99,7 @@ class ElasticReader(AbstractReader):
99
99
  index: str = "records",
100
100
  http_compress: Union[str, bool] = True,
101
101
  selector: Union[None, Selector, CompiledSelector] = None,
102
- **kwargs
102
+ **kwargs,
103
103
  ) -> None:
104
104
  self.index = index
105
105
  self.uri = uri
@@ -12,7 +12,7 @@ import os
12
12
  import re
13
13
  import sys
14
14
  import warnings
15
- from datetime import datetime
15
+ from datetime import datetime, timezone
16
16
  from itertools import zip_longest
17
17
  from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Tuple
18
18
  from urllib.parse import parse_qsl, urlparse
@@ -44,6 +44,7 @@ from .utils import to_native_str, to_str
44
44
  from .whitelist import WHITELIST, WHITELIST_TREE
45
45
 
46
46
  log = logging.getLogger(__package__)
47
+ _utcnow = functools.partial(datetime.now, timezone.utc)
47
48
 
48
49
  RECORD_VERSION = 1
49
50
  RESERVED_FIELDS = OrderedDict(
@@ -422,7 +423,7 @@ def _generate_record_class(name: str, fields: Tuple[Tuple[str, str]]) -> type:
422
423
  _globals = {
423
424
  "Record": Record,
424
425
  "RECORD_VERSION": RECORD_VERSION,
425
- "_utcnow": datetime.utcnow,
426
+ "_utcnow": _utcnow,
426
427
  "_zip_longest": zip_longest,
427
428
  }
428
429
  for field in all_fields.values():
@@ -1,20 +1,19 @@
1
+ from __future__ import annotations
2
+
1
3
  import binascii
2
4
  import math
3
5
  import os
4
6
  import pathlib
5
7
  import re
8
+ import sys
9
+ import warnings
6
10
  from binascii import a2b_hex, b2a_hex
7
11
  from datetime import datetime as _dt
8
12
  from datetime import timezone
9
13
  from posixpath import basename, dirname
10
- from typing import Any, Tuple
11
-
12
- try:
13
- import urlparse
14
- except ImportError:
15
- import urllib.parse as urlparse
16
-
17
- import warnings
14
+ from typing import Any, Optional, Tuple
15
+ from urllib.parse import urlparse
16
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
18
17
 
19
18
  from flow.record.base import FieldType
20
19
 
@@ -22,6 +21,12 @@ RE_NORMALIZE_PATH = re.compile(r"[\\/]+")
22
21
  RE_STRIP_NANOSECS = re.compile(r"(\.\d{6})\d+")
23
22
  NATIVE_UNICODE = isinstance("", str)
24
23
 
24
+ UTC = timezone.utc
25
+ ISO_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
26
+ ISO_FORMAT_WITH_MS = "%Y-%m-%dT%H:%M:%S.%f%z"
27
+
28
+ PY_311 = sys.version_info >= (3, 11, 0)
29
+
25
30
  PATH_POSIX = 0
26
31
  PATH_WINDOWS = 1
27
32
 
@@ -32,6 +37,31 @@ float_type = float
32
37
  path_type = pathlib.PurePath
33
38
 
34
39
 
40
+ def flow_record_tz(*, default_tz: str = "UTC") -> Optional[ZoneInfo | UTC]:
41
+ """Return a ``ZoneInfo`` object based on the ``FLOW_RECORD_TZ`` environment variable.
42
+
43
+ Args:
44
+ default_tz: Default timezone if ``FLOW_RECORD_TZ`` is not set (default: UTC).
45
+
46
+ Returns:
47
+ None if ``FLOW_RECORD_TZ=NONE`` otherwise ``ZoneInfo(FLOW_RECORD_TZ)`` or ``UTC`` if ZoneInfo is not found.
48
+ """
49
+ tz = os.environ.get("FLOW_RECORD_TZ", default_tz)
50
+ if tz.upper() == "NONE":
51
+ return None
52
+ try:
53
+ return ZoneInfo(tz)
54
+ except ZoneInfoNotFoundError as exc:
55
+ warnings.warn(f"{exc!r}, falling back to timezone.utc")
56
+ return UTC
57
+
58
+
59
+ # The environment variable ``FLOW_RECORD_TZ`` affects the display of datetime fields.
60
+ #
61
+ # The timezone to use when displaying datetime fields. By default this is UTC.
62
+ DISPLAY_TZINFO = flow_record_tz(default_tz="UTC")
63
+
64
+
35
65
  def defang(value: str) -> str:
36
66
  """Defangs the value to make URLs or ip addresses unclickable"""
37
67
  value = re.sub("^http://", "hxxp://", value, flags=re.IGNORECASE)
@@ -238,24 +268,24 @@ class datetime(_dt, FieldType):
238
268
  # String constructor is used for example in JsonRecordAdapter
239
269
  # Note: ISO 8601 is fully implemented in fromisoformat() from Python 3.11 and onwards.
240
270
  # Until then, we need to manually detect timezone info and handle it.
241
- if any(z in arg[19:] for z in ["Z", "+", "-"]):
242
- if "." in arg[19:]:
243
- try:
244
- return cls.strptime(arg, "%Y-%m-%dT%H:%M:%S.%f%z")
245
- except ValueError:
246
- # Sometimes nanoseconds need to be stripped
247
- return cls.strptime(re.sub(RE_STRIP_NANOSECS, "\\1", arg), "%Y-%m-%dT%H:%M:%S.%f%z")
248
- return cls.strptime(arg, "%Y-%m-%dT%H:%M:%S%z")
271
+ if not PY_311 and any(z in arg[19:] for z in ["Z", "+", "-"]):
272
+ spec = ISO_FORMAT_WITH_MS if "." in arg[19:] else ISO_FORMAT
273
+ try:
274
+ obj = cls.strptime(arg, spec)
275
+ except ValueError:
276
+ # Sometimes nanoseconds need to be stripped
277
+ obj = cls.strptime(re.sub(RE_STRIP_NANOSECS, "\\1", arg), spec)
249
278
  else:
250
279
  try:
251
- return cls.fromisoformat(arg)
280
+ obj = cls.fromisoformat(arg)
252
281
  except ValueError:
253
282
  # Sometimes nanoseconds need to be stripped
254
- return cls.fromisoformat(re.sub(RE_STRIP_NANOSECS, "\\1", arg))
283
+ obj = cls.fromisoformat(re.sub(RE_STRIP_NANOSECS, "\\1", arg))
255
284
  elif isinstance(arg, (int, float_type)):
256
- return cls.utcfromtimestamp(arg)
285
+ obj = cls.fromtimestamp(arg, UTC)
257
286
  elif isinstance(arg, (_dt,)):
258
- return _dt.__new__(
287
+ tzinfo = arg.tzinfo or UTC
288
+ obj = _dt.__new__(
259
289
  cls,
260
290
  arg.year,
261
291
  arg.month,
@@ -264,24 +294,24 @@ class datetime(_dt, FieldType):
264
294
  arg.minute,
265
295
  arg.second,
266
296
  arg.microsecond,
267
- arg.tzinfo,
297
+ tzinfo,
268
298
  )
299
+ else:
300
+ obj = _dt.__new__(cls, *args, **kwargs)
269
301
 
270
- return _dt.__new__(cls, *args, **kwargs)
271
-
272
- def __eq__(self, other):
273
- # Avoid TypeError: can't compare offset-naive and offset-aware datetimes
274
- # naive datetimes are treated as UTC in flow.record instead of local time
275
- ts1 = self.timestamp() if self.tzinfo else self.replace(tzinfo=timezone.utc).timestamp()
276
- ts2 = other.timestamp() if other.tzinfo else other.replace(tzinfo=timezone.utc).timestamp()
277
- return ts1 == ts2
302
+ # Ensure we always return a timezone aware datetime. Treat naive datetimes as UTC
303
+ if obj.tzinfo is None:
304
+ obj = obj.replace(tzinfo=UTC)
305
+ return obj
278
306
 
279
307
  def _pack(self):
280
308
  return self
281
309
 
310
+ def __str__(self):
311
+ return self.astimezone(DISPLAY_TZINFO).isoformat(" ") if DISPLAY_TZINFO else self.isoformat(" ")
312
+
282
313
  def __repr__(self):
283
- result = str(self)
284
- return result
314
+ return str(self)
285
315
 
286
316
  def __hash__(self):
287
317
  return _dt.__hash__(self)
@@ -462,7 +492,7 @@ class digest(FieldType):
462
492
 
463
493
  class uri(string, FieldType):
464
494
  def __init__(self, value):
465
- self._parsed = urlparse.urlparse(value)
495
+ self._parsed = urlparse(value)
466
496
 
467
497
  @staticmethod
468
498
  def normalize(path):
@@ -58,7 +58,7 @@ class JsonRecordPacker:
58
58
  }
59
59
  return serial
60
60
  if isinstance(obj, datetime):
61
- serial = obj.strftime("%Y-%m-%dT%H:%M:%S.%f")
61
+ serial = obj.isoformat()
62
62
  return serial
63
63
  if isinstance(obj, fieldtypes.digest):
64
64
  return {
@@ -1,6 +1,6 @@
1
- import datetime
2
1
  import functools
3
2
  import warnings
3
+ from datetime import datetime, timezone
4
4
 
5
5
  import msgpack
6
6
 
@@ -29,6 +29,8 @@ RECORD_PACK_TYPE_DATETIME = 0x10
29
29
  RECORD_PACK_TYPE_VARINT = 0x11
30
30
  RECORD_PACK_TYPE_GROUPEDRECORD = 0x12
31
31
 
32
+ UTC = timezone.utc
33
+
32
34
 
33
35
  def identifier_to_str(identifier):
34
36
  if isinstance(identifier, tuple) and len(identifier) == 2:
@@ -61,9 +63,11 @@ class RecordPacker:
61
63
  def pack_obj(self, obj, unversioned=False):
62
64
  packed = None
63
65
 
64
- if isinstance(obj, datetime.datetime):
65
- t = obj.utctimetuple()[:6] + (obj.microsecond,)
66
- packed = (RECORD_PACK_TYPE_DATETIME, t)
66
+ if isinstance(obj, datetime):
67
+ if obj.tzinfo is None or obj.tzinfo == UTC:
68
+ packed = (RECORD_PACK_TYPE_DATETIME, (*obj.timetuple()[:6], obj.microsecond))
69
+ else:
70
+ packed = (RECORD_PACK_TYPE_DATETIME, (obj.isoformat(),))
67
71
 
68
72
  elif isinstance(obj, int):
69
73
  neg = obj < 0
@@ -102,8 +106,7 @@ class RecordPacker:
102
106
  subtype, value = self.unpack(data)
103
107
 
104
108
  if subtype == RECORD_PACK_TYPE_DATETIME:
105
- dt = fieldtypes.datetime(*value)
106
- return dt
109
+ return fieldtypes.datetime(*value)
107
110
 
108
111
  if subtype == RECORD_PACK_TYPE_VARINT:
109
112
  neg, h = value
@@ -191,7 +191,7 @@ class PathTemplateWriter:
191
191
 
192
192
  def rotate_existing_file(self, path):
193
193
  if os.path.exists(path):
194
- now = datetime.datetime.utcnow()
194
+ now = datetime.datetime.now(datetime.timezone.utc)
195
195
  src = os.path.realpath(path)
196
196
 
197
197
  src_dir = os.path.dirname(src)
@@ -226,7 +226,7 @@ class PathTemplateWriter:
226
226
  return self.writer
227
227
 
228
228
  def write(self, record):
229
- ts = record._generated or datetime.datetime.utcnow()
229
+ ts = record._generated or datetime.datetime.now(datetime.timezone.utc)
230
230
  path = self.path_template.format(name=self.name, record=record, ts=ts)
231
231
  rs = self.record_stream_for_path(path)
232
232
  rs.write(record)
@@ -4,6 +4,7 @@ from __future__ import print_function
4
4
  import logging
5
5
  import sys
6
6
  from importlib import import_module
7
+ from itertools import islice
7
8
  from pathlib import Path
8
9
  from textwrap import indent
9
10
  from urllib.parse import parse_qsl, urlencode, urlparse
@@ -95,6 +96,7 @@ def main(argv=None):
95
96
  output = parser.add_argument_group("output control")
96
97
  output.add_argument("-f", "--format", metavar="FORMAT", help="Format string")
97
98
  output.add_argument("-c", "--count", type=int, help="Exit after COUNT records")
99
+ output.add_argument("--skip", metavar="COUNT", type=int, default=0, help="Skip the first COUNT records")
98
100
  output.add_argument("-w", "--writer", metavar="OUTPUT", default=None, help="Write records to output")
99
101
  output.add_argument("-m", "--mode", default=None, choices=("csv", "json", "jsonlines", "line"), help="Output mode")
100
102
  output.add_argument(
@@ -201,9 +203,11 @@ def main(argv=None):
201
203
 
202
204
  selector = make_selector(args.selector, not args.no_compile)
203
205
  seen_desc = set()
206
+ islice_stop = (args.count + args.skip) if args.count else None
207
+ record_iterator = islice(record_stream(args.src, selector), args.skip, islice_stop)
204
208
  count = 0
205
209
  with RecordWriter(uri) as record_writer:
206
- for count, rec in enumerate(record_stream(args.src, selector), start=1):
210
+ for count, rec in enumerate(record_iterator, start=1):
207
211
  if args.record_source is not None:
208
212
  rec._source = args.record_source
209
213
  if args.record_classification is not None:
@@ -227,9 +231,6 @@ def main(argv=None):
227
231
  else:
228
232
  record_writer.write(rec)
229
233
 
230
- if args.count and count >= args.count:
231
- break
232
-
233
234
  if args.list:
234
235
  print("Processed {} records".format(count))
235
236
 
@@ -0,0 +1,4 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ __version__ = version = '3.12.dev1'
4
+ __version_tuple__ = version_tuple = (3, 12, 'dev1')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flow.record
3
- Version: 3.11.dev4
3
+ Version: 3.12.dev1
4
4
  Summary: A library for defining and creating structured data (called records) that can be streamed to disk or piped to other tools that use flow.record
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -1,5 +1,11 @@
1
1
  msgpack>=0.5.2
2
2
 
3
+ [:platform_system == "Windows"]
4
+ tzdata
5
+
6
+ [:python_version < "3.9"]
7
+ backports.zoneinfo[tzdata]
8
+
3
9
  [avro]
4
10
  fastavro[snappy]
5
11
 
@@ -24,6 +24,8 @@ classifiers = [
24
24
  ]
25
25
  dependencies = [
26
26
  "msgpack>=0.5.2",
27
+ "backports.zoneinfo[tzdata]; python_version<'3.9'",
28
+ "tzdata; platform_system=='Windows'",
27
29
  ]
28
30
  dynamic = ["version"]
29
31
 
@@ -19,7 +19,7 @@ def generate_records(count=100):
19
19
  )
20
20
 
21
21
  for i in range(count):
22
- embedded = TestRecordEmbedded(datetime.datetime.utcnow())
22
+ embedded = TestRecordEmbedded(datetime.datetime.now(datetime.timezone.utc))
23
23
  yield TestRecord(number=i, record=embedded)
24
24
 
25
25
 
@@ -33,4 +33,4 @@ def generate_plain_records(count=100):
33
33
  )
34
34
 
35
35
  for i in range(count):
36
- yield TestRecord(number=i, dt=datetime.datetime.utcnow())
36
+ yield TestRecord(number=i, dt=datetime.datetime.now(datetime.timezone.utc))
@@ -1,9 +1,9 @@
1
1
  # coding: utf-8
2
2
 
3
- import datetime
4
3
  import hashlib
5
4
  import os
6
5
  import pathlib
6
+ from datetime import datetime, timedelta, timezone
7
7
 
8
8
  import pytest
9
9
 
@@ -18,6 +18,8 @@ from flow.record.fieldtypes import (
18
18
  from flow.record.fieldtypes import datetime as dt
19
19
  from flow.record.fieldtypes import fieldtype_for_value, net, uri
20
20
 
21
+ UTC = timezone.utc
22
+
21
23
  INT64_MAX = (1 << 63) - 1
22
24
  INT32_MAX = (1 << 31) - 1
23
25
  INT16_MAX = (1 << 15) - 1
@@ -398,29 +400,29 @@ def test_datetime():
398
400
  ],
399
401
  )
400
402
 
401
- now = datetime.datetime.utcnow()
403
+ now = datetime.now(UTC)
402
404
  r = TestRecord(now)
403
405
  assert r.ts == now
404
406
 
405
407
  r = TestRecord("2018-03-22T15:15:23")
406
- assert r.ts == datetime.datetime(2018, 3, 22, 15, 15, 23)
408
+ assert r.ts == datetime(2018, 3, 22, 15, 15, 23, tzinfo=UTC)
407
409
 
408
410
  r = TestRecord("2018-03-22T15:15:23.000000")
409
- assert r.ts == datetime.datetime(2018, 3, 22, 15, 15, 23)
411
+ assert r.ts == datetime(2018, 3, 22, 15, 15, 23, tzinfo=UTC)
410
412
 
411
413
  r = TestRecord("2018-03-22T15:15:23.123456")
412
- assert r.ts == datetime.datetime(2018, 3, 22, 15, 15, 23, 123456)
414
+ assert r.ts == datetime(2018, 3, 22, 15, 15, 23, 123456, tzinfo=UTC)
413
415
 
414
- dt = datetime.datetime(2018, 3, 22, 15, 15, 23, 123456)
416
+ dt = datetime(2018, 3, 22, 15, 15, 23, 123456, tzinfo=UTC)
415
417
  dt_str = dt.isoformat()
416
418
  r = TestRecord(dt_str)
417
419
  assert r.ts == dt
418
420
 
419
421
  r = TestRecord(1521731723)
420
- assert r.ts == datetime.datetime(2018, 3, 22, 15, 15, 23)
422
+ assert r.ts == datetime(2018, 3, 22, 15, 15, 23, tzinfo=UTC)
421
423
 
422
424
  r = TestRecord(1521731723.123456)
423
- assert r.ts == datetime.datetime(2018, 3, 22, 15, 15, 23, 123456)
425
+ assert r.ts == datetime(2018, 3, 22, 15, 15, 23, 123456, tzinfo=UTC)
424
426
 
425
427
  r = TestRecord("2018-03-22T15:15:23.123456")
426
428
  test = {r.ts: "Success"}
@@ -430,18 +432,18 @@ def test_datetime():
430
432
  @pytest.mark.parametrize(
431
433
  "value,expected_dt",
432
434
  [
433
- ("2023-12-31T13:37:01.123456Z", datetime.datetime(2023, 12, 31, 13, 37, 1, 123456)),
434
- ("2023-01-10T16:12:01+00:00", datetime.datetime(2023, 1, 10, 16, 12, 1)),
435
- ("2023-01-10T16:12:01", datetime.datetime(2023, 1, 10, 16, 12, 1)),
436
- ("2023-01-10T16:12:01Z", datetime.datetime(2023, 1, 10, 16, 12, 1)),
437
- ("2022-12-01T13:00:23.499460Z", datetime.datetime(2022, 12, 1, 13, 0, 23, 499460)),
438
- ("2019-09-26T07:58:30.996+0200", datetime.datetime(2019, 9, 26, 5, 58, 30, 996000)),
439
- ("2011-11-04T00:05:23+04:00", datetime.datetime(2011, 11, 3, 20, 5, 23)),
440
- ("2023-01-01T12:00:00+01:00", datetime.datetime(2023, 1, 1, 11, 0, 0, tzinfo=datetime.timezone.utc)),
441
- ("2006-11-10T14:29:55.5851926", datetime.datetime(2006, 11, 10, 14, 29, 55, 585192)),
442
- ("2006-11-10T14:29:55.585192699999999", datetime.datetime(2006, 11, 10, 14, 29, 55, 585192)),
443
- (datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc), datetime.datetime(2023, 1, 1)),
444
- (0, datetime.datetime(1970, 1, 1, 0, 0)),
435
+ ("2023-12-31T13:37:01.123456Z", datetime(2023, 12, 31, 13, 37, 1, 123456, tzinfo=UTC)),
436
+ ("2023-01-10T16:12:01+00:00", datetime(2023, 1, 10, 16, 12, 1, tzinfo=UTC)),
437
+ ("2023-01-10T16:12:01", datetime(2023, 1, 10, 16, 12, 1, tzinfo=UTC)),
438
+ ("2023-01-10T16:12:01Z", datetime(2023, 1, 10, 16, 12, 1, tzinfo=UTC)),
439
+ ("2022-12-01T13:00:23.499460Z", datetime(2022, 12, 1, 13, 0, 23, 499460, tzinfo=UTC)),
440
+ ("2019-09-26T07:58:30.996+0200", datetime(2019, 9, 26, 5, 58, 30, 996000, tzinfo=UTC)),
441
+ ("2011-11-04T00:05:23+04:00", datetime(2011, 11, 3, 20, 5, 23, tzinfo=UTC)),
442
+ ("2023-01-01T12:00:00+01:00", datetime(2023, 1, 1, 11, 0, 0, tzinfo=UTC)),
443
+ ("2006-11-10T14:29:55.5851926", datetime(2006, 11, 10, 14, 29, 55, 585192, tzinfo=UTC)),
444
+ ("2006-11-10T14:29:55.585192699999999", datetime(2006, 11, 10, 14, 29, 55, 585192, tzinfo=UTC)),
445
+ (datetime(2023, 1, 1, tzinfo=UTC), datetime(2023, 1, 1, tzinfo=UTC)),
446
+ (0, datetime(1970, 1, 1, 0, 0, tzinfo=UTC)),
445
447
  ],
446
448
  )
447
449
  def test_datetime_formats(tmp_path, value, expected_dt):
@@ -740,7 +742,7 @@ def test_fieldtype_for_value():
740
742
  assert fieldtype_for_value(1.337) == "float"
741
743
  assert fieldtype_for_value(b"\r\n") == "bytes"
742
744
  assert fieldtype_for_value("hello world") == "string"
743
- assert fieldtype_for_value(datetime.datetime.now()) == "datetime"
745
+ assert fieldtype_for_value(datetime.now()) == "datetime"
744
746
  assert fieldtype_for_value([1, 2, 3, 4, 5]) == "string"
745
747
  assert fieldtype_for_value([1, 2, 3, 4, 5], None) is None
746
748
  assert fieldtype_for_value(object(), None) is None
@@ -775,7 +777,7 @@ def test_dynamic():
775
777
  assert r.value == [1, 2, 3]
776
778
  assert isinstance(r.value, flow.record.fieldtypes.stringlist)
777
779
 
778
- now = datetime.datetime.utcnow()
780
+ now = datetime.now(UTC)
779
781
  r = TestRecord(now)
780
782
  assert r.value == now
781
783
  assert isinstance(r.value, flow.record.fieldtypes.datetime)
@@ -899,11 +901,63 @@ def test_datetime_handle_nanoseconds_without_timezone():
899
901
  d2 = dt("2006-11-10T14:29:55")
900
902
  assert isinstance(d1, dt)
901
903
  assert isinstance(d2, dt)
902
- assert d1 == datetime.datetime(2006, 11, 10, 14, 29, 55, 585192)
904
+ assert d1 == datetime(2006, 11, 10, 14, 29, 55, 585192, tzinfo=UTC)
903
905
  assert d1.microsecond == 585192
904
- assert d2 == datetime.datetime(2006, 11, 10, 14, 29, 55)
906
+ assert d2 == datetime(2006, 11, 10, 14, 29, 55, tzinfo=UTC)
905
907
  assert d2.microsecond == 0
906
908
 
907
909
 
910
+ @pytest.mark.parametrize(
911
+ "record_filename",
912
+ [
913
+ "out.records.gz",
914
+ "out.records",
915
+ "out.json",
916
+ "out.jsonl",
917
+ ],
918
+ )
919
+ def test_datetime_timezone_aware(tmp_path, record_filename):
920
+ TestRecord = RecordDescriptor(
921
+ "test/tz",
922
+ [
923
+ ("datetime", "ts"),
924
+ ],
925
+ )
926
+ tz = timezone(timedelta(hours=1))
927
+ stamp = datetime.now(tz)
928
+
929
+ with RecordWriter(tmp_path / record_filename) as writer:
930
+ record = TestRecord(stamp)
931
+ writer.write(record)
932
+ assert record.ts == stamp
933
+ assert record.ts.utcoffset() == timedelta(hours=1)
934
+ assert record._generated.tzinfo == UTC
935
+
936
+ with RecordReader(tmp_path / record_filename) as reader:
937
+ for record in reader:
938
+ assert record.ts == stamp
939
+ assert record.ts.utcoffset() == timedelta(hours=1)
940
+ assert record._generated.tzinfo == UTC
941
+
942
+
943
+ def test_datetime_comparisions():
944
+ with pytest.raises(TypeError, match=".* compare .*naive"):
945
+ assert dt("2023-01-01") > datetime(2022, 1, 1)
946
+
947
+ with pytest.raises(TypeError, match=".* compare .*naive"):
948
+ assert datetime(2022, 1, 1) < dt("2023-01-01")
949
+
950
+ assert dt("2023-01-01") > datetime(2022, 1, 1, tzinfo=UTC)
951
+ assert dt("2023-01-01") == datetime(2023, 1, 1, tzinfo=UTC)
952
+ assert dt("2023-01-01") == datetime(2023, 1, 1, tzinfo=UTC)
953
+ assert dt("2023-01-01T13:36") <= datetime(2023, 1, 1, 13, 37, tzinfo=UTC)
954
+ assert dt("2023-01-01T13:37") <= datetime(2023, 1, 1, 13, 37, tzinfo=UTC)
955
+ assert dt("2023-01-01T13:37") >= datetime(2023, 1, 1, 13, 36, tzinfo=UTC)
956
+ assert dt("2023-01-01T13:37") >= datetime(2023, 1, 1, 13, 37, tzinfo=UTC)
957
+ assert dt("2023-01-01T13:36") < datetime(2023, 1, 1, 13, 37, tzinfo=UTC)
958
+ assert dt("2023-01-01T13:37") > datetime(2023, 1, 1, 13, 36, tzinfo=UTC)
959
+ assert dt("2023-01-02") != datetime(2023, 3, 4, tzinfo=UTC)
960
+
961
+
908
962
  if __name__ == "__main__":
909
963
  __import__("standalone_test").main(globals())
@@ -1,5 +1,5 @@
1
1
  import json
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
3
 
4
4
  import pytest
5
5
 
@@ -9,7 +9,7 @@ from flow.record.exceptions import RecordDescriptorNotFound
9
9
 
10
10
  def test_record_in_record():
11
11
  packer = JsonRecordPacker()
12
- dt = datetime.utcnow()
12
+ dt = datetime.now(timezone.utc)
13
13
 
14
14
  RecordA = RecordDescriptor(
15
15
  "test/record_a",
@@ -1,8 +1,10 @@
1
- import datetime
1
+ from datetime import datetime, timedelta, timezone
2
2
 
3
3
  from flow.record import RecordDescriptor, iter_timestamped_records
4
4
  from flow.record.base import merge_record_descriptors
5
5
 
6
+ UTC = timezone.utc
7
+
6
8
 
7
9
  def test_multi_timestamp():
8
10
  TestRecord = RecordDescriptor(
@@ -15,22 +17,22 @@ def test_multi_timestamp():
15
17
  )
16
18
 
17
19
  test_record = TestRecord(
18
- ctime=datetime.datetime(2020, 1, 1, 1, 1, 1),
19
- atime=datetime.datetime(2022, 11, 22, 13, 37, 37),
20
+ ctime=datetime(2020, 1, 1, 1, 1, 1),
21
+ atime=datetime(2022, 11, 22, 13, 37, 37),
20
22
  data="test",
21
23
  )
22
24
 
23
25
  ts_records = list(iter_timestamped_records(test_record))
24
26
 
25
27
  for rec in ts_records:
26
- assert rec.ctime == datetime.datetime(2020, 1, 1, 1, 1, 1)
27
- assert rec.atime == datetime.datetime(2022, 11, 22, 13, 37, 37)
28
+ assert rec.ctime == datetime(2020, 1, 1, 1, 1, 1, tzinfo=UTC)
29
+ assert rec.atime == datetime(2022, 11, 22, 13, 37, 37, tzinfo=UTC)
28
30
  assert rec.data == "test"
29
31
 
30
- assert ts_records[0].ts == datetime.datetime(2020, 1, 1, 1, 1, 1)
32
+ assert ts_records[0].ts == datetime(2020, 1, 1, 1, 1, 1, tzinfo=UTC)
31
33
  assert ts_records[0].ts_description == "ctime"
32
34
 
33
- assert ts_records[1].ts == datetime.datetime(2022, 11, 22, 13, 37, 37)
35
+ assert ts_records[1].ts == datetime(2022, 11, 22, 13, 37, 37, tzinfo=UTC)
34
36
  assert ts_records[1].ts_description == "atime"
35
37
 
36
38
 
@@ -58,7 +60,7 @@ def test_multi_timestamp_single_datetime():
58
60
  )
59
61
 
60
62
  test_record = TestRecord(
61
- ctime=datetime.datetime(2020, 1, 1, 1, 1, 1),
63
+ ctime=datetime(2020, 1, 1, 1, 1, 1),
62
64
  data="test",
63
65
  )
64
66
  ts_records = list(iter_timestamped_records(test_record))
@@ -77,7 +79,7 @@ def test_multi_timestamp_ts_fieldname():
77
79
  )
78
80
 
79
81
  test_record = TestRecord(
80
- ts=datetime.datetime(2020, 1, 1, 1, 1, 1),
82
+ ts=datetime(2020, 1, 1, 1, 1, 1),
81
83
  data="test",
82
84
  )
83
85
  ts_records = list(iter_timestamped_records(test_record))
@@ -95,7 +97,7 @@ def test_multi_timestamp_timezone():
95
97
  ],
96
98
  )
97
99
 
98
- correct_ts = datetime.datetime(2023, 12, 31, 13, 37, 1, 123456, tzinfo=datetime.timezone.utc)
100
+ correct_ts = datetime(2023, 12, 31, 13, 37, 1, 123456, tzinfo=UTC)
99
101
 
100
102
  ts_notations = [
101
103
  correct_ts,
@@ -127,8 +129,8 @@ def test_multi_timestamp_descriptor_cache():
127
129
  merge_record_descriptors.cache_clear()
128
130
  for i in range(10):
129
131
  test_record = TestRecord(
130
- ctime=datetime.datetime.utcnow() + datetime.timedelta(hours=69),
131
- atime=datetime.datetime.utcnow() + datetime.timedelta(hours=420),
132
+ ctime=datetime.now(UTC) + timedelta(hours=69),
133
+ atime=datetime.now(UTC) + timedelta(hours=420),
132
134
  count=i,
133
135
  data=f"test {i}",
134
136
  )
@@ -1,4 +1,4 @@
1
- import datetime
1
+ from datetime import datetime, timezone
2
2
 
3
3
  import pytest
4
4
 
@@ -7,6 +7,8 @@ from flow.record.exceptions import RecordDescriptorNotFound
7
7
  from flow.record.fieldtypes import uri
8
8
  from flow.record.packer import RECORD_PACK_EXT_TYPE
9
9
 
10
+ UTC = timezone.utc
11
+
10
12
 
11
13
  def test_uri_packing():
12
14
  packer = RecordPacker()
@@ -151,7 +153,7 @@ def test_dynamic_packer():
151
153
  assert r.value == [1, True, b"b", "u"]
152
154
  assert isinstance(r.value, fieldtypes.stringlist)
153
155
 
154
- now = datetime.datetime.utcnow()
156
+ now = datetime.now(UTC)
155
157
  t = TestRecord(now)
156
158
  data = packer.pack(t)
157
159
  r = packer.unpack(data)
@@ -195,7 +197,7 @@ def test_pack_digest():
195
197
 
196
198
  def test_record_in_record():
197
199
  packer = RecordPacker()
198
- dt = datetime.datetime.utcnow()
200
+ dt = datetime.now(UTC)
199
201
 
200
202
  RecordA = RecordDescriptor(
201
203
  "test/record_a",
@@ -4,10 +4,14 @@ import json
4
4
  import os
5
5
  import platform
6
6
  import subprocess
7
+ from datetime import timezone
8
+ from unittest import mock
7
9
 
8
10
  import pytest
9
11
 
12
+ import flow.record.fieldtypes
10
13
  from flow.record import RecordDescriptor, RecordReader, RecordWriter
14
+ from flow.record.fieldtypes import flow_record_tz
11
15
  from flow.record.tools import rdump
12
16
 
13
17
 
@@ -455,3 +459,131 @@ def test_rdump_headerless_csv(tmp_path, capsysbinary):
455
459
  b"<csv/reader count='2' text='world'>",
456
460
  b"<csv/reader count='3' text='bar'>",
457
461
  ]
462
+
463
+
464
+ @pytest.mark.parametrize(
465
+ ("total_records", "count", "skip", "expected_numbers"),
466
+ [
467
+ (10, None, 2, [2, 3, 4, 5, 6, 7, 8, 9]),
468
+ (10, 3, None, [0, 1, 2]),
469
+ (10, 2, 3, [3, 4]),
470
+ (10, None, 9, [9]),
471
+ (10, None, 10, []),
472
+ ],
473
+ )
474
+ def test_rdump_count_and_skip(tmp_path, capsysbinary, total_records, count, skip, expected_numbers):
475
+ TestRecord = RecordDescriptor(
476
+ "test/record",
477
+ [
478
+ ("varint", "number"),
479
+ ("string", "foo"),
480
+ ],
481
+ )
482
+
483
+ # Write test records to a file
484
+ full_set_path = tmp_path / "test_full_set.records"
485
+ with RecordWriter(full_set_path) as writer:
486
+ for i in range(total_records):
487
+ record = TestRecord(number=i, foo="bar" + "baz" * i)
488
+ writer.write(record)
489
+
490
+ rdump_parameters = []
491
+ if count is not None:
492
+ rdump_parameters.append(f"--count={count}")
493
+ if skip is not None:
494
+ rdump_parameters.append(f"--skip={skip}")
495
+
496
+ rdump.main([str(full_set_path), "--csv", "-F", "number"] + rdump_parameters)
497
+ captured = capsysbinary.readouterr()
498
+ assert captured.err == b""
499
+
500
+ # Skip csv header
501
+ record_lines = captured.out.splitlines()[1:]
502
+
503
+ # Convert numbers to integers and validate
504
+ numbers = list(map(int, record_lines))
505
+ assert numbers == expected_numbers
506
+
507
+ # Write records using --skip and --count to a new file
508
+ subset_path = tmp_path / "test_subset.records"
509
+ rdump.main([str(full_set_path), "-w", str(subset_path)] + rdump_parameters)
510
+
511
+ # Read records from new file and validate
512
+ numbers = None
513
+ with RecordReader(subset_path) as reader:
514
+ numbers = [rec.number for rec in reader]
515
+ assert numbers == expected_numbers
516
+
517
+
518
+ @pytest.mark.parametrize(
519
+ "date_str,tz,expected_date_str",
520
+ [
521
+ ("2023-08-02T22:28:06.12345+01:00", None, "2023-08-02 21:28:06.123450+00:00"),
522
+ ("2023-08-02T22:28:06.12345+01:00", "NONE", "2023-08-02 22:28:06.123450+01:00"),
523
+ ("2023-08-02T22:28:06.12345-08:00", "NONE", "2023-08-02 22:28:06.123450-08:00"),
524
+ ("2023-08-02T20:51:32.123456+00:00", "Europe/Amsterdam", "2023-08-02 22:51:32.123456+02:00"),
525
+ ("2023-08-02T20:51:32.123456+00:00", "America/New_York", "2023-08-02 16:51:32.123456-04:00"),
526
+ ],
527
+ )
528
+ @pytest.mark.parametrize(
529
+ "rdump_params",
530
+ [
531
+ [],
532
+ ["--mode=csv"],
533
+ ["--mode=line"],
534
+ ],
535
+ )
536
+ def test_flow_record_tz_output(tmp_path, capsys, date_str, tz, expected_date_str, rdump_params):
537
+ TestRecord = RecordDescriptor(
538
+ "test/flow_record_tz",
539
+ [
540
+ ("datetime", "stamp"),
541
+ ],
542
+ )
543
+ with RecordWriter(tmp_path / "test.records") as writer:
544
+ writer.write(TestRecord(stamp=date_str))
545
+
546
+ env_dict = {}
547
+ if tz is not None:
548
+ env_dict["FLOW_RECORD_TZ"] = tz
549
+
550
+ with mock.patch.dict(os.environ, env_dict, clear=True):
551
+ # Reconfigure DISPLAY_TZINFO
552
+ flow.record.fieldtypes.DISPLAY_TZINFO = flow_record_tz(default_tz="UTC")
553
+
554
+ rdump.main([str(tmp_path / "test.records")] + rdump_params)
555
+ captured = capsys.readouterr()
556
+ assert captured.err == ""
557
+ assert expected_date_str in captured.out
558
+
559
+ # restore DISPLAY_TZINFO just in case
560
+ flow.record.fieldtypes.DISPLAY_TZINFO = flow_record_tz(default_tz="UTC")
561
+
562
+
563
+ def test_flow_record_invalid_tz(tmp_path, capsys):
564
+ TestRecord = RecordDescriptor(
565
+ "test/flow_record_tz",
566
+ [
567
+ ("datetime", "stamp"),
568
+ ],
569
+ )
570
+ with RecordWriter(tmp_path / "test.records") as writer:
571
+ writer.write(TestRecord(stamp="2023-08-16T17:46:55.390691+02:00"))
572
+
573
+ env_dict = {
574
+ "FLOW_RECORD_TZ": "invalid",
575
+ }
576
+
577
+ with mock.patch.dict(os.environ, env_dict, clear=True):
578
+ # Reconfigure DISPLAY_TZINFO
579
+ with pytest.warns(UserWarning, match=".* falling back to timezone.utc"):
580
+ flow.record.fieldtypes.DISPLAY_TZINFO = flow_record_tz()
581
+
582
+ rdump.main([str(tmp_path / "test.records")])
583
+ captured = capsys.readouterr()
584
+ assert captured.err == ""
585
+ assert "2023-08-16 15:46:55.390691+00:00" in captured.out
586
+ assert flow.record.fieldtypes.DISPLAY_TZINFO == timezone.utc
587
+
588
+ # restore DISPLAY_TZINFO just in case
589
+ flow.record.fieldtypes.DISPLAY_TZINFO = flow_record_tz(default_tz="UTC")
@@ -203,7 +203,7 @@ def test_record_writer_stdout():
203
203
  def test_record_adapter_archive(tmpdir):
204
204
  # archive some records, using "testing" as name
205
205
  writer = RecordWriter("archive://{}?name=testing".format(tmpdir))
206
- dt = datetime.datetime.utcnow()
206
+ dt = datetime.datetime.now(datetime.timezone.utc)
207
207
  count = 0
208
208
  for rec in generate_records():
209
209
  writer.write(rec)
@@ -1,10 +1,10 @@
1
1
  import codecs
2
- import datetime
3
2
  import json
4
3
  import os
5
4
  import pathlib
6
5
  import subprocess
7
6
  import sys
7
+ from datetime import datetime, timezone
8
8
  from unittest.mock import mock_open, patch
9
9
 
10
10
  import msgpack
@@ -32,7 +32,7 @@ from flow.record.utils import is_stdout
32
32
  def test_datetime_serialization():
33
33
  packer = RecordPacker()
34
34
 
35
- now = datetime.datetime.utcnow()
35
+ now = datetime.now(timezone.utc)
36
36
 
37
37
  for tz in ["UTC", "Europe/Amsterdam"]:
38
38
  os.environ["TZ"] = tz
@@ -1,4 +1,4 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
 
3
3
  import pytest
4
4
 
@@ -449,7 +449,7 @@ def test_record_in_records():
449
449
  )
450
450
 
451
451
  test_str = "this is a test"
452
- dt = datetime.utcnow()
452
+ dt = datetime.now(timezone.utc)
453
453
  record_a = RecordA(some_dt=dt, field=test_str)
454
454
  record_b = RecordB(record=record_a, some_dt=dt)
455
455
 
@@ -49,7 +49,7 @@ deps =
49
49
  vermin
50
50
  commands =
51
51
  flake8 flow tests
52
- vermin -t=3.7- --no-tips --lint flow tests
52
+ vermin -t=3.7- --no-tips --lint --exclude zoneinfo flow tests
53
53
 
54
54
  [flake8]
55
55
  max-line-length = 120
@@ -1,4 +0,0 @@
1
- # file generated by setuptools_scm
2
- # don't change, don't track in version control
3
- __version__ = version = '3.11.dev4'
4
- __version_tuple__ = version_tuple = (3, 11, 'dev4')
File without changes