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.
Files changed (116) hide show
  1. {vortexpy-4.2.1 → vortexpy-4.3.0}/PKG-INFO +1 -1
  2. {vortexpy-4.2.1 → vortexpy-4.3.0}/pyproject.toml +1 -1
  3. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/Tuple.py +32 -0
  4. vortexpy-4.3.0/vortex/TupleBson.py +182 -0
  5. vortexpy-4.3.0/vortex/TupleBson_test.py +353 -0
  6. vortexpy-4.2.1/vortex/TupleRestful.py → vortexpy-4.3.0/vortex/TupleDocumentConverter.py +50 -89
  7. vortexpy-4.3.0/vortex/TupleRestful.py +134 -0
  8. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexUtil.py +17 -2
  9. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/__init__.py +1 -1
  10. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics.py +156 -1
  11. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics_test.py +69 -1
  12. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/PKG-INFO +1 -1
  13. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/SOURCES.txt +3 -0
  14. {vortexpy-4.2.1 → vortexpy-4.3.0}/README.md +0 -0
  15. {vortexpy-4.2.1 → vortexpy-4.3.0}/setup.cfg +0 -0
  16. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/DataWrapTuple.py +0 -0
  17. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/DeferUtil.py +0 -0
  18. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/Jsonable.py +0 -0
  19. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/Payload.py +0 -0
  20. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadEndpoint.py +0 -0
  21. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadEndpointTest.py +0 -0
  22. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadEnvelope.py +0 -0
  23. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadFilterKeys.py +0 -0
  24. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadIO.py +0 -0
  25. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadPriority.py +0 -0
  26. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadResponse.py +0 -0
  27. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadResponseTest.py +0 -0
  28. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadResponse_test.py +0 -0
  29. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PayloadTest.py +0 -0
  30. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/PerformTestActionTuple.py +0 -0
  31. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/SerialiseUtil.py +0 -0
  32. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/SerialiseUtil_test.py +0 -0
  33. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TestTuple.py +0 -0
  34. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleAction.py +0 -0
  35. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleDataForTest.py +0 -0
  36. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TuplePerfTest.py +0 -0
  37. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  38. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleRestful_test.py +0 -0
  39. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleSelector.py +0 -0
  40. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/TupleTest.py +0 -0
  41. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexABC.py +0 -0
  42. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexAccessControlContextABC.py +0 -0
  43. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexAuthenticatorABC.py +0 -0
  44. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientHttp.py +0 -0
  45. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientHttpTest.py +0 -0
  46. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientTcp.py +0 -0
  47. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexClientWebsocketFactory.py +0 -0
  48. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexConnectionABC.py +0 -0
  49. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexEndpointPermission.py +0 -0
  50. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexFactory.py +0 -0
  51. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexPayloadProtocol.py +0 -0
  52. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexPermission.py +0 -0
  53. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexPermissionError.py +0 -0
  54. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServer.py +0 -0
  55. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerConnection.py +0 -0
  56. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerHttpResource.py +0 -0
  57. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerTcp.py +0 -0
  58. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexServerWebsocket.py +0 -0
  59. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexSession.py +0 -0
  60. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  61. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  62. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexTest.py +0 -0
  63. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/VortexWritePushProducer.py +0 -0
  64. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  65. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoader.py +0 -0
  66. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  67. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  68. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  69. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/data_loader/__init__.py +0 -0
  70. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  71. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  72. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  73. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/event_bus/__init__.py +0 -0
  74. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessor.py +0 -0
  75. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  76. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  77. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  78. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataActionClient.py +0 -0
  79. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableCache.py +0 -0
  80. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableHandler.py +0 -0
  81. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
  82. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  83. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataObserverClient.py +0 -0
  84. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/TupleDataProviderABC.py +0 -0
  85. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  86. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/handler/__init__.py +0 -0
  87. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/mypy_plugin.py +0 -0
  88. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/py.typed +0 -0
  89. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
  90. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  91. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  92. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClient.py +0 -0
  93. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClientTest.py +0 -0
  94. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulResource.py +0 -0
  95. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/RestfulResourceTest.py +0 -0
  96. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/restful/__init__.py +0 -0
  97. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/RPC.py +0 -0
  98. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/RPCTest.py +0 -0
  99. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/RPC_test.py +0 -0
  100. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/rpc/__init__.py +0 -0
  101. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  102. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  103. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  104. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/sqla_orm/__init__.py +0 -0
  105. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/storage/TupleStorageSqlite.py +0 -0
  106. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/storage/__init__.py +0 -0
  107. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/__init__.py +0 -0
  108. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/constants.py +0 -0
  109. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  110. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  111. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  112. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  113. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/dependency_links.txt +0 -0
  114. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/entry_points.txt +0 -0
  115. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/requires.txt +0 -0
  116. {vortexpy-4.2.1 → vortexpy-4.3.0}/vortexpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 4.2.1
3
+ Version: 4.3.0
4
4
  Summary: Flying Ion's observable, routable, data serialisation and transport code.
5
5
  Author-email: Flying Ion Pty Ltd <contact@flyingion.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortexpy"
7
- version = "4.2.1"
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)