flow.record 3.22.dev8__tar.gz → 3.22.dev10__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.
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/PKG-INFO +1 -1
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/sqlite.py +3 -3
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/base.py +17 -17
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/fieldtypes/__init__.py +1 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/packer.py +1 -1
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/selector.py +1 -1
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/version.py +3 -3
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow.record.egg-info/PKG-INFO +1 -1
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_sqlite_duckdb.py +37 -1
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/fieldtypes/test_fieldtypes.py +81 -1
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/record/test_record.py +10 -10
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/.git-blame-ignore-revs +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/.gitattributes +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/COPYRIGHT +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/LICENSE +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/MANIFEST.in +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/README.md +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/examples/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/examples/filesystem.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/examples/passivedns.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/examples/records.json +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/examples/selectors.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/examples/tcpconn.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/archive.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/avro.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/broker.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/csvfile.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/duckdb.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/elastic.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/jsonfile.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/line.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/mongo.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/split.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/splunk.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/stream.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/text.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/adapter/xlsx.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/context.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/exceptions.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/fieldtypes/credential.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/fieldtypes/net/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/fieldtypes/net/ip.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/fieldtypes/net/ipv4.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/fieldtypes/net/tcp.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/fieldtypes/net/udp.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/jsonpacker.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/stream.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/tools/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/tools/geoip.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/tools/rdump.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/utils.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow/record/whitelist.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow.record.egg-info/SOURCES.txt +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow.record.egg-info/dependency_links.txt +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow.record.egg-info/entry_points.txt +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow.record.egg-info/requires.txt +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/flow.record.egg-info/top_level.txt +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/pyproject.toml +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/setup.cfg +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/_data/.gitkeep +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/_docs/Makefile +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/_docs/conf.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/_docs/index.rst +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/_utils.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_avro.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_csv.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_elastic.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_json.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_line.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_splunk.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_text.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/adapter/test_xlsx.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/conftest.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/fieldtypes/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/fieldtypes/test_boolean.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/fieldtypes/test_ip.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/packer/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/packer/test_json_packer.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/packer/test_packer.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/record/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/record/test_adapter.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/record/test_context.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/record/test_descriptor.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/record/test_multi_timestamp.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/selector/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/selector/test_compiled.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/selector/test_selectors.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/test_deprecations.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/test_regressions.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/test_utils.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/tools/__init__.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tests/tools/test_rdump.py +0 -0
- {flow_record-3.22.dev8 → flow_record-3.22.dev10}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flow.record
|
|
3
|
-
Version: 3.22.
|
|
3
|
+
Version: 3.22.dev10
|
|
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-Expression: AGPL-3.0-or-later
|
|
@@ -109,10 +109,10 @@ def prepare_insert_sql(table_name: str, field_names: tuple[str]) -> str:
|
|
|
109
109
|
|
|
110
110
|
def db_insert_record(con: sqlite3.Connection, record: Record) -> None:
|
|
111
111
|
"""Insert a record into the database."""
|
|
112
|
-
|
|
112
|
+
descriptor = record._desc
|
|
113
|
+
table_name = descriptor.name
|
|
113
114
|
rdict = record._asdict()
|
|
114
|
-
|
|
115
|
-
sql = prepare_insert_sql(table_name, record.__slots__)
|
|
115
|
+
sql = prepare_insert_sql(table_name, tuple(rdict.keys()))
|
|
116
116
|
|
|
117
117
|
# Convert values to str() for types we don't support
|
|
118
118
|
values = []
|
|
@@ -247,22 +247,22 @@ class GroupedRecord(Record):
|
|
|
247
247
|
|
|
248
248
|
def __init__(self, name: str, records: list[Record | GroupedRecord]):
|
|
249
249
|
super().__init__()
|
|
250
|
-
self.
|
|
251
|
-
self.
|
|
252
|
-
self.
|
|
253
|
-
self.
|
|
250
|
+
self.__name__ = to_str(name)
|
|
251
|
+
self.__records__ = []
|
|
252
|
+
self.__descriptors__ = []
|
|
253
|
+
self.__flat_fields__ = []
|
|
254
254
|
|
|
255
255
|
# to avoid recursion in __setattr__ and __getattr__
|
|
256
256
|
self.__dict__["fieldname_to_record"] = OrderedDict()
|
|
257
257
|
|
|
258
258
|
for rec in records:
|
|
259
259
|
if isinstance(rec, GroupedRecord):
|
|
260
|
-
for r in rec.
|
|
261
|
-
self.
|
|
262
|
-
self.
|
|
260
|
+
for r in rec.__records__:
|
|
261
|
+
self.__records__.append(r)
|
|
262
|
+
self.__descriptors__.append(r._desc)
|
|
263
263
|
else:
|
|
264
|
-
self.
|
|
265
|
-
self.
|
|
264
|
+
self.__records__.append(rec)
|
|
265
|
+
self.__descriptors__.append(rec._desc)
|
|
266
266
|
|
|
267
267
|
all_fields = rec._desc.get_all_fields()
|
|
268
268
|
required_fields = rec._desc.get_required_fields()
|
|
@@ -272,10 +272,10 @@ class GroupedRecord(Record):
|
|
|
272
272
|
continue
|
|
273
273
|
self.fieldname_to_record[fname] = rec
|
|
274
274
|
if fname not in required_fields:
|
|
275
|
-
self.
|
|
275
|
+
self.__flat_fields__.append(field)
|
|
276
276
|
# Flat descriptor to maintain compatibility with Record
|
|
277
277
|
|
|
278
|
-
self._desc = RecordDescriptor(self.
|
|
278
|
+
self._desc = RecordDescriptor(self.__name__, [(f.typename, f.name) for f in self.__flat_fields__])
|
|
279
279
|
|
|
280
280
|
# _field_types to maintain compatibility with RecordDescriptor
|
|
281
281
|
self._field_types = self._desc.recordType._field_types
|
|
@@ -291,7 +291,7 @@ class GroupedRecord(Record):
|
|
|
291
291
|
None or the record
|
|
292
292
|
|
|
293
293
|
"""
|
|
294
|
-
for record in self.
|
|
294
|
+
for record in self.__records__:
|
|
295
295
|
if record._desc.name == type_name:
|
|
296
296
|
return record
|
|
297
297
|
return None
|
|
@@ -304,7 +304,7 @@ class GroupedRecord(Record):
|
|
|
304
304
|
return OrderedDict((k, getattr(self, k)) for k in keys if k not in exclude)
|
|
305
305
|
|
|
306
306
|
def __repr__(self) -> str:
|
|
307
|
-
return f"<{self.
|
|
307
|
+
return f"<{self.__name__} {self.__records__}>"
|
|
308
308
|
|
|
309
309
|
def __setattr__(self, attr: str, val: Any) -> None:
|
|
310
310
|
if attr in getattr(self, "fieldname_to_record", {}):
|
|
@@ -320,18 +320,18 @@ class GroupedRecord(Record):
|
|
|
320
320
|
|
|
321
321
|
def _pack(self) -> tuple[str, tuple]:
|
|
322
322
|
return (
|
|
323
|
-
self.
|
|
324
|
-
tuple(record._pack() for record in self.
|
|
323
|
+
self.__name__,
|
|
324
|
+
tuple(record._pack() for record in self.__records__),
|
|
325
325
|
)
|
|
326
326
|
|
|
327
327
|
def _replace(self, **kwds) -> GroupedRecord:
|
|
328
328
|
new_records = [
|
|
329
329
|
record.__class__(*map(kwds.pop, record.__slots__, (getattr(self, k) for k in record.__slots__)))
|
|
330
|
-
for record in self.
|
|
330
|
+
for record in self.__records__
|
|
331
331
|
]
|
|
332
332
|
if kwds:
|
|
333
333
|
raise ValueError(f"Got unexpected field names: {list(kwds)!r}")
|
|
334
|
-
return GroupedRecord(self.
|
|
334
|
+
return GroupedRecord(self.__name__, new_records)
|
|
335
335
|
|
|
336
336
|
|
|
337
337
|
def is_valid_field_name(name: str, check_reserved: bool = True) -> bool:
|
|
@@ -78,7 +78,7 @@ class RecordPacker:
|
|
|
78
78
|
packed = RECORD_PACK_TYPE_VARINT, (neg, v.to_bytes((v.bit_length() + 7) // 8, "big"))
|
|
79
79
|
|
|
80
80
|
elif isinstance(obj, GroupedRecord):
|
|
81
|
-
for desc in obj.
|
|
81
|
+
for desc in obj.__descriptors__:
|
|
82
82
|
if desc.identifier not in self.descriptors:
|
|
83
83
|
self.register(desc, True)
|
|
84
84
|
|
|
@@ -115,7 +115,7 @@ def upper(s: str | Any) -> str | Any:
|
|
|
115
115
|
def names(r: Record | WrappedRecord | GroupedRecord) -> set[str]:
|
|
116
116
|
"""Return the available names as a set in the Record otherwise ['UnknownRecord']."""
|
|
117
117
|
if isinstance(r, GroupedRecord):
|
|
118
|
-
return {sub_record._desc.name for sub_record in r.
|
|
118
|
+
return {sub_record._desc.name for sub_record in r.__records__}
|
|
119
119
|
if isinstance(r, (Record, WrappedRecord)):
|
|
120
120
|
return {r._desc.name}
|
|
121
121
|
return ["UnknownRecord"]
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '3.22.
|
|
32
|
-
__version_tuple__ = version_tuple = (3, 22, '
|
|
31
|
+
__version__ = version = '3.22.dev10'
|
|
32
|
+
__version_tuple__ = version_tuple = (3, 22, 'dev10')
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'ga594c193b'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flow.record
|
|
3
|
-
Version: 3.22.
|
|
3
|
+
Version: 3.22.dev10
|
|
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-Expression: AGPL-3.0-or-later
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import sqlite3
|
|
4
5
|
from contextlib import closing
|
|
5
6
|
from datetime import datetime, timezone
|
|
@@ -12,7 +13,7 @@ except ModuleNotFoundError:
|
|
|
12
13
|
|
|
13
14
|
import pytest
|
|
14
15
|
|
|
15
|
-
from flow.record import Record, RecordDescriptor, RecordReader, RecordWriter
|
|
16
|
+
from flow.record import GroupedRecord, Record, RecordDescriptor, RecordReader, RecordWriter
|
|
16
17
|
from flow.record.adapter.sqlite import prepare_insert_sql
|
|
17
18
|
from flow.record.base import normalize_fieldname
|
|
18
19
|
from flow.record.exceptions import RecordDescriptorError
|
|
@@ -400,3 +401,38 @@ def test_selector(tmp_path: Path, db: Database) -> None:
|
|
|
400
401
|
with RecordReader(f"{db.scheme}://{db_path}", selector="r.name == 'record12345'") as reader:
|
|
401
402
|
records = list(reader)
|
|
402
403
|
assert len(records) == 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@sqlite_duckdb_parametrize
|
|
407
|
+
def test_grouped_record(tmp_path: Path, db: Database) -> None:
|
|
408
|
+
"""Test adapter with grouped records."""
|
|
409
|
+
db_path = tmp_path / "records.db"
|
|
410
|
+
|
|
411
|
+
DigestRecord = RecordDescriptor(
|
|
412
|
+
"meta/record",
|
|
413
|
+
[
|
|
414
|
+
("digest", "digest"),
|
|
415
|
+
],
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
with RecordWriter(f"{db.scheme}://{db_path}") as writer:
|
|
419
|
+
for record in generate_records(10):
|
|
420
|
+
digest_record = DigestRecord(
|
|
421
|
+
digest=(
|
|
422
|
+
hashlib.md5(record.name.encode()).hexdigest(),
|
|
423
|
+
hashlib.sha1(record.name.encode()).hexdigest(),
|
|
424
|
+
hashlib.sha256(record.name.encode()).hexdigest(),
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
grouped = GroupedRecord("grouped/record", [digest_record, record])
|
|
428
|
+
writer.write(grouped)
|
|
429
|
+
|
|
430
|
+
with RecordReader(f"{db.scheme}://{db_path}", selector="r.name == 'record5'") as reader:
|
|
431
|
+
records = list(reader)
|
|
432
|
+
assert len(records) == 1
|
|
433
|
+
assert records[0].name == "record5"
|
|
434
|
+
assert records[0].digest == (
|
|
435
|
+
f"(md5={hashlib.md5(b'record5').hexdigest()}, "
|
|
436
|
+
f"sha1={hashlib.sha1(b'record5').hexdigest()}, "
|
|
437
|
+
f"sha256={hashlib.sha256(b'record5').hexdigest()})"
|
|
438
|
+
)
|
|
@@ -13,6 +13,7 @@ import pytest
|
|
|
13
13
|
import flow.record.fieldtypes
|
|
14
14
|
from flow.record import RecordDescriptor, RecordReader, RecordWriter, fieldtypes
|
|
15
15
|
from flow.record.fieldtypes import (
|
|
16
|
+
HAS_ZONE_INFO,
|
|
16
17
|
PY_312_OR_HIGHER,
|
|
17
18
|
PY_313_OR_HIGHER,
|
|
18
19
|
TYPE_POSIX,
|
|
@@ -28,6 +29,9 @@ from flow.record.fieldtypes import (
|
|
|
28
29
|
)
|
|
29
30
|
from flow.record.fieldtypes import datetime as dt
|
|
30
31
|
|
|
32
|
+
if HAS_ZONE_INFO:
|
|
33
|
+
from flow.record.fieldtypes import ZoneInfo
|
|
34
|
+
|
|
31
35
|
if TYPE_CHECKING:
|
|
32
36
|
from collections.abc import Callable
|
|
33
37
|
|
|
@@ -427,7 +431,7 @@ def test_datetime() -> None:
|
|
|
427
431
|
("2006-11-10T14:29:55.585192699999999-07:00", datetime(2006, 11, 10, 21, 29, 55, 585192, tzinfo=UTC)),
|
|
428
432
|
],
|
|
429
433
|
)
|
|
430
|
-
def test_datetime_formats(tmp_path: pathlib.Path, value: str, expected_dt: datetime) -> None:
|
|
434
|
+
def test_datetime_formats(tmp_path: pathlib.Path, value: str | datetime | float, expected_dt: datetime) -> None:
|
|
431
435
|
TestRecord = RecordDescriptor(
|
|
432
436
|
"test/datetime",
|
|
433
437
|
[
|
|
@@ -448,6 +452,82 @@ def test_datetime_formats(tmp_path: pathlib.Path, value: str, expected_dt: datet
|
|
|
448
452
|
assert record.dt == expected_dt
|
|
449
453
|
|
|
450
454
|
|
|
455
|
+
DATETIME_FOLD_PARAMS = [
|
|
456
|
+
(datetime(2023, 1, 1, tzinfo=UTC, fold=1), datetime(2023, 1, 1, tzinfo=UTC)),
|
|
457
|
+
]
|
|
458
|
+
if HAS_ZONE_INFO:
|
|
459
|
+
DATETIME_FOLD_PARAMS.append(
|
|
460
|
+
(
|
|
461
|
+
datetime(2025, 10, 26, 2, 0, 3, tzinfo=ZoneInfo("Europe/Amsterdam"), fold=1),
|
|
462
|
+
datetime(2025, 10, 26, 1, 0, 3, tzinfo=UTC),
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@pytest.mark.skipif(not HAS_ZONE_INFO, reason="ZoneInfo is required for testing datetime fold parameter")
|
|
468
|
+
@pytest.mark.parametrize(("value", "expected_dt"), DATETIME_FOLD_PARAMS)
|
|
469
|
+
def test_datetime_formats_fold(tmp_path: pathlib.Path, value: datetime, expected_dt: datetime) -> None:
|
|
470
|
+
"""test whether datetime accepts fold parameters and converts it correctly"""
|
|
471
|
+
TestRecord = RecordDescriptor(
|
|
472
|
+
"test/datetime",
|
|
473
|
+
[
|
|
474
|
+
("datetime", "dt"),
|
|
475
|
+
],
|
|
476
|
+
)
|
|
477
|
+
record = TestRecord(dt=value)
|
|
478
|
+
assert record.dt.fold == 1
|
|
479
|
+
assert record.dt.astimezone(UTC) == expected_dt
|
|
480
|
+
|
|
481
|
+
# test packing / serialization of datetime fields
|
|
482
|
+
path = tmp_path / "datetime.records"
|
|
483
|
+
with RecordWriter(path) as writer:
|
|
484
|
+
writer.write(record)
|
|
485
|
+
|
|
486
|
+
# test unpacking / deserialization of datetime fields
|
|
487
|
+
with RecordReader(path) as reader:
|
|
488
|
+
record = next(iter(reader))
|
|
489
|
+
# Need to convert it to UTC specifically as the timezones do not match
|
|
490
|
+
assert record.dt.astimezone(UTC) == expected_dt
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@pytest.mark.skipif(not HAS_ZONE_INFO, reason="ZoneInfo is required for testing datetime fold parameter")
|
|
494
|
+
def test_datetime_fold_example() -> None:
|
|
495
|
+
"""
|
|
496
|
+
Test datetime fold parameter during daylight saving time changes in the Netherlands, which has a
|
|
497
|
+
timezone offset of +01:00 during wintertime and +02:00 during summertime.
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
TestRecord = RecordDescriptor(
|
|
501
|
+
"test/datetime",
|
|
502
|
+
[
|
|
503
|
+
("datetime", "dt"),
|
|
504
|
+
],
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# 2025-10-26 is the date of the end of daylight saving time in the Netherlands.
|
|
508
|
+
# At 3:00 AM the clock goes back to 2:00 AM, so 2:00 AM occurs twice. The first occurrence has fold=0
|
|
509
|
+
record = TestRecord("2025-10-26T00:00:00+00:00")
|
|
510
|
+
nl_dt = record.dt.astimezone(ZoneInfo("Europe/Amsterdam"))
|
|
511
|
+
assert nl_dt.isoformat() == "2025-10-26T02:00:00+02:00"
|
|
512
|
+
assert nl_dt.hour == 2
|
|
513
|
+
assert nl_dt.fold == 0
|
|
514
|
+
|
|
515
|
+
# test that both datetimes are considered equal when converted to UTC
|
|
516
|
+
record2 = TestRecord(nl_dt)
|
|
517
|
+
assert record.dt.astimezone(UTC) == record2.dt.astimezone(UTC)
|
|
518
|
+
|
|
519
|
+
# wintertime, the clock goes back from 3:00 AM to 2:00 AM, so 2:00 AM occurs twice. The second occurrence has fold=1
|
|
520
|
+
record = TestRecord("2025-10-26T01:00:00+00:00")
|
|
521
|
+
nl_dt = record.dt.astimezone(ZoneInfo("Europe/Amsterdam"))
|
|
522
|
+
assert nl_dt.isoformat() == "2025-10-26T02:00:00+01:00"
|
|
523
|
+
assert nl_dt.hour == 2
|
|
524
|
+
assert nl_dt.fold == 1
|
|
525
|
+
|
|
526
|
+
# test that both datetimes are considered equal when converted to UTC
|
|
527
|
+
record2 = TestRecord(nl_dt)
|
|
528
|
+
assert record.dt.astimezone(UTC) == record2.dt.astimezone(UTC)
|
|
529
|
+
|
|
530
|
+
|
|
451
531
|
def test_digest() -> None:
|
|
452
532
|
TestRecord = RecordDescriptor(
|
|
453
533
|
"test/digest",
|
|
@@ -178,15 +178,15 @@ def test_grouped_record() -> None:
|
|
|
178
178
|
grouped.hello = "new value"
|
|
179
179
|
assert grouped.hello == "new value"
|
|
180
180
|
assert grouped.profile == "omg"
|
|
181
|
-
assert grouped.
|
|
182
|
-
assert grouped.
|
|
181
|
+
assert grouped.__records__[0].hello == "new value"
|
|
182
|
+
assert grouped.__records__[1].hello == "other hello"
|
|
183
183
|
|
|
184
|
-
grouped.
|
|
184
|
+
grouped.__records__[1].hello = "testing"
|
|
185
185
|
assert grouped.hello != "testing"
|
|
186
186
|
assert grouped.hello == "new value"
|
|
187
|
-
assert grouped.
|
|
187
|
+
assert grouped.__records__[1].hello == "testing"
|
|
188
188
|
|
|
189
|
-
assert len(grouped.
|
|
189
|
+
assert len(grouped.__records__) == 2
|
|
190
190
|
|
|
191
191
|
# Test grouped._asdict
|
|
192
192
|
rdict = grouped._asdict()
|
|
@@ -250,7 +250,7 @@ def test_grouped_records_packing(tmp_path: Path) -> None:
|
|
|
250
250
|
assert isinstance(record, Record)
|
|
251
251
|
assert isinstance(record, GroupedRecord)
|
|
252
252
|
assert record.common == "world" # first 'key' has precendence
|
|
253
|
-
assert record.
|
|
253
|
+
assert record.__name__ == "grouped/ab"
|
|
254
254
|
assert record.a_string == "hello"
|
|
255
255
|
assert record.a_count == 12345
|
|
256
256
|
assert record.b_count == 54321
|
|
@@ -259,12 +259,12 @@ def test_grouped_records_packing(tmp_path: Path) -> None:
|
|
|
259
259
|
assert record._classification == "CLASSIFIED"
|
|
260
260
|
|
|
261
261
|
# access 'common' on second record directly
|
|
262
|
-
assert record.
|
|
262
|
+
assert record.__records__[1].common == "bye"
|
|
263
263
|
|
|
264
264
|
# access raw records directly
|
|
265
|
-
assert len(record.
|
|
266
|
-
assert record.
|
|
267
|
-
assert record.
|
|
265
|
+
assert len(record.__records__) == 2
|
|
266
|
+
assert record.__records__[0]._desc.name == "test/a"
|
|
267
|
+
assert record.__records__[1]._desc.name == "test/b"
|
|
268
268
|
|
|
269
269
|
# test using selectors
|
|
270
270
|
reader = RecordReader(path, selector="r.a_count == 12345")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|