vortexpy 4.2.1__tar.gz → 4.3.0__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.
- {vortexpy-4.2.1 → vortexpy-4.3.0}/PKG-INFO +1 -1
- {vortexpy-4.2.1 → vortexpy-4.3.0}/pyproject.toml +1 -1
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/Tuple.py +32 -0
- vortexpy-4.3.0/vortex/TupleBson.py +182 -0
- vortexpy-4.3.0/vortex/TupleBson_test.py +353 -0
- vortexpy-4.2.1/vortex/TupleRestful.py → vortexpy-4.3.0/vortex/TupleDocumentConverter.py +50 -89
- vortexpy-4.3.0/vortex/TupleRestful.py +134 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexUtil.py +17 -2
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/__init__.py +1 -1
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics.py +156 -1
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics_test.py +69 -1
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/SOURCES.txt +3 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/README.md +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/setup.cfg +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/DeferUtil.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/Jsonable.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/Payload.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadEndpoint.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadEndpointTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadEnvelope.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadIO.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadResponse.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadResponse_test.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/SerialiseUtil.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/SerialiseUtil_test.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TestTuple.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleAction.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleRestful_test.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexAccessControlContextABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientTcp.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientWebsocketFactory.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexFactory.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexPayloadProtocol.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexPermission.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexPermissionError.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServer.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexSession.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexWritePushProducer.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoader.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBus.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessor.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorProxy.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableCache.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableHandler.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObserverClient.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataProviderABC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/mypy_plugin.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/py.typed +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClientTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulResourceTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/RPC.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/RPC_test.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/storage/TupleStorageSqlite.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortexpy"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3.0"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
authors = [{name = "Flying Ion Pty Ltd", email = "contact@flyingion.com"}]
|
|
10
10
|
description = "Flying Ion's observable, routable, data serialisation and transport code."
|
|
@@ -383,6 +383,13 @@ class _TupleToPlainJsonMixin:
|
|
|
383
383
|
self, includeNones, includeFalse
|
|
384
384
|
)
|
|
385
385
|
|
|
386
|
+
def tupleToBsonJsonDict(self, includeNones=True, includeFalse=True):
|
|
387
|
+
from vortex.TupleBson import TupleBsonConverter
|
|
388
|
+
|
|
389
|
+
return TupleBsonConverter.tupleToBsonJsonDict(
|
|
390
|
+
self, includeNones, includeFalse
|
|
391
|
+
)
|
|
392
|
+
|
|
386
393
|
def __tupleToJsonDict(
|
|
387
394
|
self, includeNones=True, includeFalse=True, useShortNames=True
|
|
388
395
|
):
|
|
@@ -506,6 +513,31 @@ class _TupleToPlainJsonMixin:
|
|
|
506
513
|
jsonDict, TupleClass
|
|
507
514
|
)
|
|
508
515
|
|
|
516
|
+
@staticmethod
|
|
517
|
+
def bsonJsonDictToTupleWithValidation(
|
|
518
|
+
jsonDict: dict, TupleClass: Type["Tuple"]
|
|
519
|
+
) -> "Tuple":
|
|
520
|
+
"""BSON document dict to Tuple with validation.
|
|
521
|
+
|
|
522
|
+
This method uses the typings on the fields in a tuple to validate the
|
|
523
|
+
types of data.
|
|
524
|
+
"""
|
|
525
|
+
from vortex.TupleBson import TupleBsonConverter
|
|
526
|
+
|
|
527
|
+
return TupleBsonConverter.bsonJsonDictToTupleWithValidation(
|
|
528
|
+
jsonDict, TupleClass
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def bsonJsonDictToTupleWithValidationResult(
|
|
533
|
+
jsonDict: dict, TupleClass: Type["Tuple"]
|
|
534
|
+
) -> "TupleValidationResult":
|
|
535
|
+
from vortex.TupleBson import TupleBsonConverter
|
|
536
|
+
|
|
537
|
+
return TupleBsonConverter.bsonJsonDictToTupleWithValidationResult(
|
|
538
|
+
jsonDict, TupleClass
|
|
539
|
+
)
|
|
540
|
+
|
|
509
541
|
|
|
510
542
|
class Tuple(Jsonable, _TupleToPlainJsonMixin, _TupleToSqlaJsonMixin):
|
|
511
543
|
"""Tuple
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
BSON document serialisation / deserialisation for vortex.Tuple.
|
|
11
|
+
|
|
12
|
+
Datetime fields use UTC epoch milliseconds on write and read, matching MongoDB
|
|
13
|
+
BSON UTC datetime. Native ``datetime`` values are also accepted on read.
|
|
14
|
+
Naive datetimes are treated as UTC at this boundary.
|
|
15
|
+
|
|
16
|
+
Implementation module; public interface remains on vortex.Tuple.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Type, get_args
|
|
23
|
+
|
|
24
|
+
from .SerialiseUtil import WKBElement
|
|
25
|
+
from .SerialiseUtil import convertFromWkbElement
|
|
26
|
+
from .TupleDocumentConverter import TupleDocumentConverterBase
|
|
27
|
+
from .TupleDocumentConverter import TupleValidationError
|
|
28
|
+
from .TupleDocumentConverter import TupleValidationResult
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from .Tuple import Tuple
|
|
32
|
+
|
|
33
|
+
_MIN_EPOCH_MILLISECONDS = 1_000_000_000_000
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TupleBsonConverter(TupleDocumentConverterBase):
|
|
37
|
+
@classmethod
|
|
38
|
+
def _ensureAwareDatetime(cls, value: datetime) -> datetime:
|
|
39
|
+
"""
|
|
40
|
+
Return a timezone-aware datetime for BSON serialisation.
|
|
41
|
+
|
|
42
|
+
Naive datetimes are treated as UTC.
|
|
43
|
+
"""
|
|
44
|
+
if value.tzinfo is None:
|
|
45
|
+
return value.replace(tzinfo=UTC)
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _datetimeToBson(cls, value: datetime) -> int:
|
|
50
|
+
"""Serialise a datetime to UTC epoch milliseconds."""
|
|
51
|
+
aware = cls._ensureAwareDatetime(value)
|
|
52
|
+
return int(aware.timestamp() * 1000)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def _datetimeFromBson(cls, value: object) -> datetime:
|
|
56
|
+
"""
|
|
57
|
+
Deserialise a BSON datetime field to UTC.
|
|
58
|
+
|
|
59
|
+
Accepts a ``datetime`` or epoch-millisecond ``int`` (not ``bool``).
|
|
60
|
+
Epoch-second integers are rejected.
|
|
61
|
+
"""
|
|
62
|
+
if isinstance(value, datetime):
|
|
63
|
+
return cls._ensureAwareDatetime(value)
|
|
64
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
65
|
+
if value < _MIN_EPOCH_MILLISECONDS:
|
|
66
|
+
raise TypeError(
|
|
67
|
+
"Expected epoch milliseconds since Unix epoch in UTC,"
|
|
68
|
+
f" got {value}"
|
|
69
|
+
)
|
|
70
|
+
return datetime.fromtimestamp(value / 1000, tz=UTC)
|
|
71
|
+
raise TypeError(
|
|
72
|
+
"Expected BSON datetime or epoch milliseconds,"
|
|
73
|
+
f" got {type(value).__name__}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def _utilCoercePlainDictItem(cls, item: Any) -> Any:
|
|
78
|
+
if isinstance(item, int) and not isinstance(item, bool):
|
|
79
|
+
try:
|
|
80
|
+
return cls._datetimeFromBson(item)
|
|
81
|
+
except TypeError:
|
|
82
|
+
return item
|
|
83
|
+
return item
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def _utilCoerceFieldDatetimeValue(
|
|
87
|
+
cls,
|
|
88
|
+
field_path: str,
|
|
89
|
+
value: Any,
|
|
90
|
+
typeHint: Any,
|
|
91
|
+
) -> tuple[Any, TupleValidationError | None]:
|
|
92
|
+
if typeHint is datetime or datetime in get_args(typeHint):
|
|
93
|
+
if isinstance(value, (datetime, int)) and not isinstance(value, bool):
|
|
94
|
+
try:
|
|
95
|
+
return cls._datetimeFromBson(value), None
|
|
96
|
+
except TypeError as exc:
|
|
97
|
+
return value, cls._makeError(field_path, str(exc), TypeError)
|
|
98
|
+
return value, None
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def _jsonDictToTupleWithValidationResult(
|
|
102
|
+
cls, jsonDict: dict[str, Any], TupleClass: Type[Tuple]
|
|
103
|
+
) -> TupleValidationResult:
|
|
104
|
+
return cls.bsonJsonDictToTupleWithValidationResult(jsonDict, TupleClass)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def tupleToBsonJsonDict(
|
|
108
|
+
cls,
|
|
109
|
+
tuple_: Tuple,
|
|
110
|
+
includeNones: bool = True,
|
|
111
|
+
includeFalse: bool = True,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
"""
|
|
114
|
+
Serialise a tuple to a BSON-shaped document dict.
|
|
115
|
+
|
|
116
|
+
Datetime fields become UTC epoch-millisecond integers.
|
|
117
|
+
"""
|
|
118
|
+
if not tuple_.__shortFieldNamesMap__:
|
|
119
|
+
raise Exception(
|
|
120
|
+
"Tuple %s has no shortFieldNames defined" % tuple_.tupleType()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
json: dict[str, Any] = {}
|
|
124
|
+
|
|
125
|
+
def convert(value: Any) -> Any:
|
|
126
|
+
if isinstance(value, list):
|
|
127
|
+
return [convert(item) for item in value]
|
|
128
|
+
|
|
129
|
+
if isinstance(value, dict):
|
|
130
|
+
return {
|
|
131
|
+
convert(key): convert(item) for key, item in value.items()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
from .Tuple import Tuple
|
|
135
|
+
|
|
136
|
+
if isinstance(value, Tuple):
|
|
137
|
+
return cls.tupleToBsonJsonDict(
|
|
138
|
+
value, includeNones, includeFalse
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if isinstance(value, WKBElement):
|
|
142
|
+
return convertFromWkbElement(value)
|
|
143
|
+
|
|
144
|
+
from .Tuple import TupleField
|
|
145
|
+
|
|
146
|
+
if isinstance(value, TupleField):
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
if isinstance(value, datetime):
|
|
150
|
+
return cls._datetimeToBson(value)
|
|
151
|
+
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
for shortName, normalName in tuple_.__shortFieldNamesMap__.items():
|
|
155
|
+
value = convert(getattr(tuple_, normalName))
|
|
156
|
+
if value is None and not includeNones:
|
|
157
|
+
continue
|
|
158
|
+
if value is False and not includeFalse:
|
|
159
|
+
continue
|
|
160
|
+
json[normalName] = value
|
|
161
|
+
|
|
162
|
+
return json
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def bsonJsonDictToTupleWithValidationResult(
|
|
166
|
+
cls, jsonDict: dict[str, Any], TupleClass: Type[Tuple]
|
|
167
|
+
) -> TupleValidationResult:
|
|
168
|
+
"""
|
|
169
|
+
Deserialise a BSON-shaped document dict into a tuple with validation.
|
|
170
|
+
"""
|
|
171
|
+
return cls.documentJsonDictToTupleWithValidationResult(
|
|
172
|
+
jsonDict, TupleClass
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def bsonJsonDictToTupleWithValidation(
|
|
177
|
+
cls, jsonDict: dict[str, Any], TupleClass: Type[Tuple]
|
|
178
|
+
) -> Tuple:
|
|
179
|
+
"""
|
|
180
|
+
Deserialise a BSON-shaped document dict, raising on the first error.
|
|
181
|
+
"""
|
|
182
|
+
return cls.documentJsonDictToTupleWithValidation(jsonDict, TupleClass)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from enum import IntEnum, StrEnum
|
|
11
|
+
from typing import Dict, Annotated
|
|
12
|
+
from typing import List
|
|
13
|
+
from typing import Union
|
|
14
|
+
|
|
15
|
+
from ddt import data, ddt
|
|
16
|
+
from twisted.trial import unittest
|
|
17
|
+
|
|
18
|
+
from vortex.Tuple import Tuple
|
|
19
|
+
from vortex.Tuple import TupleField
|
|
20
|
+
from vortex.Tuple import TupleHash, IntTupleFieldValidator
|
|
21
|
+
from vortex.Tuple import addTupleType
|
|
22
|
+
from vortex.TupleBson import TupleBsonConverter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@addTupleType
|
|
26
|
+
class BsonTestTuple(Tuple):
|
|
27
|
+
__tupleType__ = "flyingion.vortex.BsonTestTuple"
|
|
28
|
+
aInt: int = TupleField()
|
|
29
|
+
aIntWithValidator: Annotated[int, IntTupleFieldValidator(1, 100)] = (
|
|
30
|
+
TupleField()
|
|
31
|
+
)
|
|
32
|
+
aFloat: float = TupleField()
|
|
33
|
+
aString: str = TupleField()
|
|
34
|
+
aBoolTrue: bool = TupleField()
|
|
35
|
+
aBoolFalse: bool = TupleField()
|
|
36
|
+
aList: List[Union[str, int, bool, float]] = TupleField()
|
|
37
|
+
aDict: Dict[str, Union[str, float]] = TupleField()
|
|
38
|
+
aSubTuple: "BsonSubTestTuple" = TupleField()
|
|
39
|
+
aListOfSubTuples: List["BsonSubTestTuple"] = TupleField()
|
|
40
|
+
aDictOfSubTuples: Dict[str, "BsonSubTestTuple"] = TupleField()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BsonObjectTypeE(StrEnum):
|
|
44
|
+
EVENT = "event"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BsonPriorityE(IntEnum):
|
|
48
|
+
HIGH = 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@addTupleType
|
|
52
|
+
class BsonEnumTestTuple(Tuple):
|
|
53
|
+
__tupleType__ = "flyingion.vortex.BsonEnumTestTuple"
|
|
54
|
+
objectType: BsonObjectTypeE = TupleField()
|
|
55
|
+
optionalObjectType: BsonObjectTypeE | None = TupleField()
|
|
56
|
+
priority: BsonPriorityE = TupleField()
|
|
57
|
+
priorities: List[BsonPriorityE] = TupleField()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@addTupleType
|
|
61
|
+
class BsonOptionalDatetimeTestTuple(Tuple):
|
|
62
|
+
__tupleType__ = "flyingion.vortex.BsonOptionalDatetimeTestTuple"
|
|
63
|
+
requiredDate: datetime = TupleField()
|
|
64
|
+
optionalDate: datetime | None = TupleField()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@addTupleType
|
|
68
|
+
class BsonSubTestTuple(Tuple):
|
|
69
|
+
__tupleType__ = "flyingion.vortex.BsonSubTestTuple"
|
|
70
|
+
subInt: int = TupleField()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _makeBsonTestTuple():
|
|
74
|
+
tuple_ = BsonTestTuple()
|
|
75
|
+
tuple_.aBoolTrue = True
|
|
76
|
+
tuple_.aBoolFalse = False
|
|
77
|
+
tuple_.aFloat = 2.56
|
|
78
|
+
tuple_.aInt = 1231231
|
|
79
|
+
tuple_.aIntWithValidator = 50
|
|
80
|
+
tuple_.aString = "test string from 345345345@$#%#$%#$%#$%#"
|
|
81
|
+
tuple_.aList = ["y", 3, True, 3.3]
|
|
82
|
+
tuple_.aDict = {
|
|
83
|
+
"a": "test str",
|
|
84
|
+
"1": 1.234,
|
|
85
|
+
}
|
|
86
|
+
tuple_.aDictOfSubTuples = {}
|
|
87
|
+
|
|
88
|
+
subTuple = BsonSubTestTuple()
|
|
89
|
+
subTuple.subInt = 2048
|
|
90
|
+
tuple_.aSubTuple = subTuple
|
|
91
|
+
|
|
92
|
+
tuple_.aListOfSubTuples = []
|
|
93
|
+
for x in range(2):
|
|
94
|
+
subTuple = BsonSubTestTuple()
|
|
95
|
+
subTuple.subInt = x
|
|
96
|
+
tuple_.aListOfSubTuples.append(subTuple)
|
|
97
|
+
|
|
98
|
+
subTuple = BsonSubTestTuple()
|
|
99
|
+
subTuple.subInt = x + 10
|
|
100
|
+
tuple_.aDictOfSubTuples[str(x)] = subTuple
|
|
101
|
+
|
|
102
|
+
return tuple_
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _validBsonDict():
|
|
106
|
+
return _makeBsonTestTuple().tupleToBsonJsonDict()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@ddt
|
|
110
|
+
class TupleBsonTest(unittest.TestCase):
|
|
111
|
+
def _testSerialisation(self, origTuple):
|
|
112
|
+
bsonDict = origTuple.tupleToBsonJsonDict()
|
|
113
|
+
deserialisedTuple = origTuple.bsonJsonDictToTupleWithValidation(
|
|
114
|
+
bsonDict, type(origTuple)
|
|
115
|
+
)
|
|
116
|
+
return deserialisedTuple
|
|
117
|
+
|
|
118
|
+
@data(-1, 110)
|
|
119
|
+
def testIntValidatorInvalid(self, int_):
|
|
120
|
+
origTuple = _makeBsonTestTuple()
|
|
121
|
+
origTuple.aIntWithValidator = int_
|
|
122
|
+
self.assertRaises(ValueError, self._testSerialisation, origTuple)
|
|
123
|
+
|
|
124
|
+
@data(1, 99)
|
|
125
|
+
def testIntValidatorValid(self, int_):
|
|
126
|
+
origTuple = _makeBsonTestTuple()
|
|
127
|
+
origTuple.aIntWithValidator = int_
|
|
128
|
+
self.assertEqual(
|
|
129
|
+
self._testSerialisation(origTuple).aIntWithValidator, int_
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def testValidatorPass(self):
|
|
133
|
+
origTuple = _makeBsonTestTuple()
|
|
134
|
+
deserialisedTuple = self._testSerialisation(origTuple)
|
|
135
|
+
|
|
136
|
+
self.assertEqual(TupleHash(origTuple), TupleHash(origTuple))
|
|
137
|
+
self.assertEqual(
|
|
138
|
+
TupleHash(deserialisedTuple), TupleHash(deserialisedTuple)
|
|
139
|
+
)
|
|
140
|
+
self.assertEqual(
|
|
141
|
+
TupleHash(origTuple),
|
|
142
|
+
TupleHash(deserialisedTuple),
|
|
143
|
+
"Tuple serialise -> deserialise error",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def testDatetimeRoundTrip(self):
|
|
147
|
+
origTuple = BsonOptionalDatetimeTestTuple()
|
|
148
|
+
origTuple.requiredDate = datetime(
|
|
149
|
+
2020, 1, 2, 3, 4, 5, 678000, tzinfo=UTC
|
|
150
|
+
)
|
|
151
|
+
origTuple.optionalDate = datetime(
|
|
152
|
+
2021, 2, 3, 4, 5, 6, 789000, tzinfo=UTC
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
bsonDict = origTuple.tupleToBsonJsonDict()
|
|
156
|
+
self.assertIsInstance(bsonDict["requiredDate"], int)
|
|
157
|
+
self.assertIsInstance(bsonDict["optionalDate"], int)
|
|
158
|
+
|
|
159
|
+
deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
|
|
160
|
+
bsonDict, BsonOptionalDatetimeTestTuple
|
|
161
|
+
)
|
|
162
|
+
self.assertEqual(
|
|
163
|
+
deserialisedTuple.requiredDate, origTuple.requiredDate
|
|
164
|
+
)
|
|
165
|
+
self.assertEqual(
|
|
166
|
+
deserialisedTuple.optionalDate, origTuple.optionalDate
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def testDatetimeFromEpochMilliseconds(self):
|
|
170
|
+
epochMs = 1577934245678
|
|
171
|
+
deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
|
|
172
|
+
dict(requiredDate=epochMs, optionalDate=None),
|
|
173
|
+
BsonOptionalDatetimeTestTuple,
|
|
174
|
+
)
|
|
175
|
+
self.assertEqual(
|
|
176
|
+
deserialisedTuple.requiredDate,
|
|
177
|
+
datetime(2020, 1, 2, 3, 4, 5, 678000, tzinfo=UTC),
|
|
178
|
+
)
|
|
179
|
+
self.assertIsNone(deserialisedTuple.optionalDate)
|
|
180
|
+
|
|
181
|
+
def testDatetimeFromEpochSecondsRejected(self):
|
|
182
|
+
epochSeconds = 1577934245
|
|
183
|
+
with self.assertRaises(TypeError) as ctx:
|
|
184
|
+
Tuple.bsonJsonDictToTupleWithValidation(
|
|
185
|
+
dict(requiredDate=epochSeconds, optionalDate=None),
|
|
186
|
+
BsonOptionalDatetimeTestTuple,
|
|
187
|
+
)
|
|
188
|
+
self.assertIn("epoch milliseconds", str(ctx.exception))
|
|
189
|
+
|
|
190
|
+
def testDatetimeFromBoolRejected(self):
|
|
191
|
+
result = Tuple.bsonJsonDictToTupleWithValidationResult(
|
|
192
|
+
dict(requiredDate=True, optionalDate=None),
|
|
193
|
+
BsonOptionalDatetimeTestTuple,
|
|
194
|
+
)
|
|
195
|
+
self.assertEqual(len(result.errors), 1)
|
|
196
|
+
self.assertEqual(result.errors[0].field_path, "requiredDate")
|
|
197
|
+
self.assertEqual(result.errors[0].exc_type, TypeError)
|
|
198
|
+
|
|
199
|
+
def testOptionalDatetimeRoundTrip(self):
|
|
200
|
+
origTuple = BsonOptionalDatetimeTestTuple()
|
|
201
|
+
origTuple.requiredDate = datetime(
|
|
202
|
+
2024, 6, 15, 10, 30, 45, 123000, tzinfo=UTC
|
|
203
|
+
)
|
|
204
|
+
origTuple.optionalDate = datetime(
|
|
205
|
+
2024, 6, 16, 11, 31, 46, 654000, tzinfo=UTC
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
bsonDict = origTuple.tupleToBsonJsonDict()
|
|
209
|
+
deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
|
|
210
|
+
bsonDict, BsonOptionalDatetimeTestTuple
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self.assertEqual(deserialisedTuple.requiredDate, origTuple.requiredDate)
|
|
214
|
+
self.assertEqual(deserialisedTuple.optionalDate, origTuple.optionalDate)
|
|
215
|
+
|
|
216
|
+
def testStrEnumAndIntEnumRoundTrip(self):
|
|
217
|
+
origTuple = BsonEnumTestTuple()
|
|
218
|
+
origTuple.objectType = BsonObjectTypeE.EVENT
|
|
219
|
+
origTuple.optionalObjectType = BsonObjectTypeE.EVENT
|
|
220
|
+
origTuple.priority = BsonPriorityE.HIGH
|
|
221
|
+
origTuple.priorities = [BsonPriorityE.HIGH]
|
|
222
|
+
|
|
223
|
+
bsonDict = origTuple.tupleToBsonJsonDict()
|
|
224
|
+
deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
|
|
225
|
+
bsonDict, BsonEnumTestTuple
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self.assertEqual(deserialisedTuple.objectType, BsonObjectTypeE.EVENT)
|
|
229
|
+
self.assertEqual(
|
|
230
|
+
deserialisedTuple.optionalObjectType, BsonObjectTypeE.EVENT
|
|
231
|
+
)
|
|
232
|
+
self.assertEqual(deserialisedTuple.priority, BsonPriorityE.HIGH)
|
|
233
|
+
self.assertEqual(deserialisedTuple.priorities, [BsonPriorityE.HIGH])
|
|
234
|
+
|
|
235
|
+
def testOptionalDatetimeNoneRoundTrip(self):
|
|
236
|
+
origTuple = BsonOptionalDatetimeTestTuple()
|
|
237
|
+
origTuple.requiredDate = datetime(
|
|
238
|
+
2024, 6, 15, 10, 30, 45, 123000, tzinfo=UTC
|
|
239
|
+
)
|
|
240
|
+
origTuple.optionalDate = None
|
|
241
|
+
|
|
242
|
+
bsonDict = origTuple.tupleToBsonJsonDict(includeNones=False)
|
|
243
|
+
deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
|
|
244
|
+
bsonDict, BsonOptionalDatetimeTestTuple
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self.assertEqual(deserialisedTuple.requiredDate, origTuple.requiredDate)
|
|
248
|
+
self.assertIsNone(deserialisedTuple.optionalDate)
|
|
249
|
+
|
|
250
|
+
def testDatetimeFromBsonDirect(self):
|
|
251
|
+
aware = datetime(2020, 1, 2, 3, 4, 5, 678000, tzinfo=UTC)
|
|
252
|
+
self.assertEqual(TupleBsonConverter._datetimeFromBson(1577934245678), aware)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TupleBsonCollectingTest(unittest.TestCase):
|
|
256
|
+
def testSingleBadScalarCollectsErrorAndPopulatesOtherFields(self):
|
|
257
|
+
bsonDict = _validBsonDict()
|
|
258
|
+
bsonDict["aIntWithValidator"] = 110
|
|
259
|
+
|
|
260
|
+
result = Tuple.bsonJsonDictToTupleWithValidationResult(
|
|
261
|
+
bsonDict, BsonTestTuple
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
self.assertEqual(len(result.errors), 1)
|
|
265
|
+
self.assertEqual(result.errors[0].field_path, "aIntWithValidator")
|
|
266
|
+
self.assertTrue(
|
|
267
|
+
result.errors[0].message.startswith("At aIntWithValidator:")
|
|
268
|
+
)
|
|
269
|
+
self.assertEqual(result.errors[0].exc_type, ValueError)
|
|
270
|
+
self.assertEqual(result.tuple_.aInt, bsonDict["aInt"])
|
|
271
|
+
self.assertEqual(result.tuple_.aString, bsonDict["aString"])
|
|
272
|
+
self.assertEqual(result.tuple_.aIntWithValidator, None)
|
|
273
|
+
|
|
274
|
+
def testMultipleTopLevelErrors(self):
|
|
275
|
+
bsonDict = _validBsonDict()
|
|
276
|
+
bsonDict["aInt"] = "not-an-int"
|
|
277
|
+
bsonDict["aIntWithValidator"] = -1
|
|
278
|
+
|
|
279
|
+
result = Tuple.bsonJsonDictToTupleWithValidationResult(
|
|
280
|
+
bsonDict, BsonTestTuple
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
self.assertEqual(len(result.errors), 2)
|
|
284
|
+
paths = {err.field_path for err in result.errors}
|
|
285
|
+
self.assertEqual(paths, {"aInt", "aIntWithValidator"})
|
|
286
|
+
for err in result.errors:
|
|
287
|
+
self.assertTrue(err.message.startswith(f"At {err.field_path}:"))
|
|
288
|
+
self.assertEqual(result.tuple_.aString, bsonDict["aString"])
|
|
289
|
+
self.assertEqual(result.tuple_.aFloat, bsonDict["aFloat"])
|
|
290
|
+
|
|
291
|
+
def testListPartialSkipsBadItem(self):
|
|
292
|
+
bsonDict = _validBsonDict()
|
|
293
|
+
bsonDict["aListOfSubTuples"] = [
|
|
294
|
+
{"subInt": 1},
|
|
295
|
+
"not-a-tuple",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
result = Tuple.bsonJsonDictToTupleWithValidationResult(
|
|
299
|
+
bsonDict, BsonTestTuple
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self.assertEqual(len(result.tuple_.aListOfSubTuples), 1)
|
|
303
|
+
self.assertEqual(result.tuple_.aListOfSubTuples[0].subInt, 1)
|
|
304
|
+
self.assertEqual(len(result.errors), 1)
|
|
305
|
+
self.assertEqual(result.errors[0].field_path, "aListOfSubTuples[1]")
|
|
306
|
+
self.assertIn("aListOfSubTuples[1]", result.errors[0].message)
|
|
307
|
+
|
|
308
|
+
def testNestedTuplePartialError(self):
|
|
309
|
+
bsonDict = _validBsonDict()
|
|
310
|
+
bsonDict["aSubTuple"] = {"subInt": "bad"}
|
|
311
|
+
|
|
312
|
+
result = Tuple.bsonJsonDictToTupleWithValidationResult(
|
|
313
|
+
bsonDict, BsonTestTuple
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
self.assertEqual(result.tuple_.aInt, bsonDict["aInt"])
|
|
317
|
+
self.assertEqual(result.tuple_.aString, bsonDict["aString"])
|
|
318
|
+
self.assertIsNone(result.tuple_.aSubTuple.subInt)
|
|
319
|
+
self.assertEqual(len(result.errors), 1)
|
|
320
|
+
self.assertEqual(result.errors[0].field_path, "aSubTuple.subInt")
|
|
321
|
+
self.assertTrue(
|
|
322
|
+
result.errors[0].message.startswith("At aSubTuple.subInt:")
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def testInvalidStrEnumValueCollectsError(self):
|
|
326
|
+
bsonDict = {
|
|
327
|
+
"objectType": "not-a-valid-object-type",
|
|
328
|
+
"optionalObjectType": None,
|
|
329
|
+
"priority": 1,
|
|
330
|
+
"priorities": [],
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
result = Tuple.bsonJsonDictToTupleWithValidationResult(
|
|
334
|
+
bsonDict, BsonEnumTestTuple
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
self.assertEqual(len(result.errors), 1)
|
|
338
|
+
self.assertEqual(result.errors[0].field_path, "objectType")
|
|
339
|
+
self.assertEqual(result.errors[0].exc_type, ValueError)
|
|
340
|
+
|
|
341
|
+
def testStrictWrapperRaisesFirstCollectingError(self):
|
|
342
|
+
bsonDict = _validBsonDict()
|
|
343
|
+
bsonDict["aInt"] = "not-an-int"
|
|
344
|
+
bsonDict["aIntWithValidator"] = -1
|
|
345
|
+
|
|
346
|
+
result = Tuple.bsonJsonDictToTupleWithValidationResult(
|
|
347
|
+
bsonDict, BsonTestTuple
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
with self.assertRaises(result.errors[0].exc_type) as ctx:
|
|
351
|
+
Tuple.bsonJsonDictToTupleWithValidation(bsonDict, BsonTestTuple)
|
|
352
|
+
|
|
353
|
+
self.assertEqual(str(ctx.exception), result.errors[0].message)
|