vortexpy 4.3.7__tar.gz → 4.3.9__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 (146) hide show
  1. {vortexpy-4.3.7 → vortexpy-4.3.9}/PKG-INFO +1 -1
  2. {vortexpy-4.3.7 → vortexpy-4.3.9}/pyproject.toml +1 -1
  3. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/SerialiseUtil.py +8 -0
  4. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/Tuple.py +71 -0
  5. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleBson.py +7 -25
  6. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleBson_test.py +27 -0
  7. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleDocumentConverter.py +3 -4
  8. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleTest.py +79 -1
  9. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/__init__.py +1 -1
  10. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortexpy.egg-info/PKG-INFO +1 -1
  11. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortexpy.egg-info/SOURCES.txt +0 -1
  12. vortexpy-4.3.7/LICENSE +0 -22
  13. {vortexpy-4.3.7 → vortexpy-4.3.9}/README.md +0 -0
  14. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/TwistedDeferredTimeout.py +0 -0
  15. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/__init__.py +0 -0
  16. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  17. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  18. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  19. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  20. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/run_websocket_rpc_test_client.py +0 -0
  21. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/run_websocket_rpc_test_server.py +0 -0
  22. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/setup_example_logging.py +0 -0
  23. {vortexpy-4.3.7 → vortexpy-4.3.9}/example/store_and_load_json.py +0 -0
  24. {vortexpy-4.3.7 → vortexpy-4.3.9}/setup.cfg +0 -0
  25. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/DataWrapTuple.py +0 -0
  26. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/DeferUtil.py +0 -0
  27. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/DeferUtil_test.py +0 -0
  28. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/Jsonable.py +0 -0
  29. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  30. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  31. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/MtlsServerSslOptions.py +0 -0
  32. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/Payload.py +0 -0
  33. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadEndpoint.py +0 -0
  34. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadEndpointTest.py +0 -0
  35. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadEnvelope.py +0 -0
  36. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadFilterKeys.py +0 -0
  37. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadIO.py +0 -0
  38. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadPriority.py +0 -0
  39. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadResponse.py +0 -0
  40. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadResponseTest.py +0 -0
  41. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadResponse_test.py +0 -0
  42. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PayloadTest.py +0 -0
  43. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/PerformTestActionTuple.py +0 -0
  44. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/SerialiseUtil_test.py +0 -0
  45. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TestTuple.py +0 -0
  46. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleAction.py +0 -0
  47. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleDataForTest.py +0 -0
  48. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TuplePerfTest.py +0 -0
  49. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  50. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleRestful.py +0 -0
  51. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleRestful_test.py +0 -0
  52. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/TupleSelector.py +0 -0
  53. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexABC.py +0 -0
  54. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexAccessControlContextABC.py +0 -0
  55. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexAuthenticatorABC.py +0 -0
  56. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexClientHttp.py +0 -0
  57. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexClientHttpTest.py +0 -0
  58. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexClientTcp.py +0 -0
  59. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexClientWebsocketFactory.py +0 -0
  60. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexConnectionABC.py +0 -0
  61. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexEndpointPermission.py +0 -0
  62. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexFactory.py +0 -0
  63. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexFactory_test.py +0 -0
  64. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexPayloadProtocol.py +0 -0
  65. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexPermission.py +0 -0
  66. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexPermissionError.py +0 -0
  67. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServer.py +0 -0
  68. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServerConnection.py +0 -0
  69. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServerConnection_test.py +0 -0
  70. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServerHttpResource.py +0 -0
  71. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServerTcp.py +0 -0
  72. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServerWebsocket.py +0 -0
  73. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServerWebsocket_test.py +0 -0
  74. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexServer_test.py +0 -0
  75. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexSession.py +0 -0
  76. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexSession_test.py +0 -0
  77. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  78. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  79. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexTest.py +0 -0
  80. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexUtil.py +0 -0
  81. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexWritePushProducer.py +0 -0
  82. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/VortexWritePushProducer_test.py +0 -0
  83. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  84. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoader.py +0 -0
  85. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  86. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  87. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  88. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/data_loader/__init__.py +0 -0
  89. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  90. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  91. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  92. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/event_bus/__init__.py +0 -0
  93. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessor.py +0 -0
  94. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  95. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  96. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  97. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleActionPushOffline.py +0 -0
  98. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  99. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  100. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataActionClient.py +0 -0
  101. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableCache.py +0 -0
  102. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  103. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableHandler.py +0 -0
  104. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableHandler_test.py +0 -0
  105. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
  106. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  107. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  108. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataObserverClient.py +0 -0
  109. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/TupleDataProviderABC.py +0 -0
  110. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  111. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/handler/__init__.py +0 -0
  112. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/mypy_plugin.py +0 -0
  113. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  114. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/py.typed +0 -0
  115. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/GzippedDataHttpClient.py +0 -0
  116. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  117. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  118. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/RestfulHttpClient.py +0 -0
  119. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/RestfulResource.py +0 -0
  120. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/__init__.py +0 -0
  121. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/restful_http_client_test.py +0 -0
  122. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/restful/restful_resource_test.py +0 -0
  123. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/rpc/RPC.py +0 -0
  124. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/rpc/RPCTest.py +0 -0
  125. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/rpc/RPC_test.py +0 -0
  126. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/rpc/__init__.py +0 -0
  127. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  128. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  129. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  130. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/sqla_orm/__init__.py +0 -0
  131. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  132. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/storage/TupleStorageSqlite.py +0 -0
  133. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/storage/__init__.py +0 -0
  134. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/tuple_json_schema/__init__.py +0 -0
  135. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/tuple_json_schema/constants.py +0 -0
  136. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  137. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  138. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  139. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  140. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/tuple_test_fixtures.py +0 -0
  141. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/twisted_runtime_metrics.py +0 -0
  142. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortex/twisted_runtime_metrics_test.py +0 -0
  143. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortexpy.egg-info/dependency_links.txt +0 -0
  144. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortexpy.egg-info/entry_points.txt +0 -0
  145. {vortexpy-4.3.7 → vortexpy-4.3.9}/vortexpy.egg-info/requires.txt +0 -0
  146. {vortexpy-4.3.7 → vortexpy-4.3.9}/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.3.7
3
+ Version: 4.3.9
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.3.7"
7
+ version = "4.3.9"
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."
@@ -290,10 +290,18 @@ def fromRapuiType(typeName: str):
290
290
  def utilTypeHintsForClass(cls: type) -> dict[str, typing.Any]:
291
291
  """Resolved type hints for a class, cached per class.
292
292
 
293
+ Tuple classes resolve through ``Tuple.typeHints()`` — the canonical
294
+ per-class store; other classes are cached by this function's
295
+ ``lru_cache``.
296
+
293
297
  Returns an empty dict when hints can not be resolved (for example
294
298
  unresolvable forward references) — enum coercion degrades to no-op.
295
299
  """
300
+ from vortex.Tuple import Tuple
301
+
296
302
  try:
303
+ if issubclass(cls, Tuple):
304
+ return cls.typeHints()
297
305
  return typing.get_type_hints(cls)
298
306
  except Exception:
299
307
  return {}
@@ -17,6 +17,7 @@ from datetime import datetime
17
17
  from typing import Dict, Optional, Any
18
18
  from typing import List
19
19
  from typing import Type
20
+ from typing import get_type_hints
20
21
 
21
22
  from sqlalchemy.orm.attributes import InstrumentedAttribute
22
23
  from sqlalchemy.orm.properties import RelationshipProperty
@@ -28,6 +29,9 @@ from .SerialiseUtil import ISO8601
28
29
  from .SerialiseUtil import T_RAPUI_TUPLE
29
30
  from .SerialiseUtil import WKBElement
30
31
  from .SerialiseUtil import convertFromWkbElement
32
+ from .SerialiseUtil import datetimeToBson
33
+ from .SerialiseUtil import datetimeToBsonMicroseconds
34
+ from .SerialiseUtil import requireAwareDatetime
31
35
 
32
36
  logger = logging.getLogger(__name__)
33
37
 
@@ -312,6 +316,28 @@ class TupleField:
312
316
  self.jsonExclude = jsonExclude
313
317
  self.bsonDatetimeFormat = bsonDatetimeFormat
314
318
 
319
+ def toBson(self, value: Any) -> Any:
320
+ """Convert a datetime to this field's BSON storage format.
321
+
322
+ Non-datetime values pass through unchanged, so every Mongo
323
+ filter value can be wrapped uniformly. Naive datetimes raise
324
+ ``TypeError``.
325
+ """
326
+ if not isinstance(value, datetime):
327
+ return value
328
+ aware = requireAwareDatetime(value)
329
+ if self.bsonDatetimeFormat is None:
330
+ return datetimeToBson(aware)
331
+ if self.bsonDatetimeFormat is BsonDatetimeFormatE.standardMsIntEpoch:
332
+ return datetimeToBson(aware)
333
+ if self.bsonDatetimeFormat is BsonDatetimeFormatE.standardUsIntEpoch:
334
+ return datetimeToBsonMicroseconds(aware)
335
+ if self.bsonDatetimeFormat is BsonDatetimeFormatE.standardMsBsonDate:
336
+ return aware
337
+ raise NotImplementedError(
338
+ f"Unsupported BsonDatetimeFormatE {self.bsonDatetimeFormat!r}"
339
+ )
340
+
315
341
 
316
342
  class PolymorphicTupleTypeFieldArg:
317
343
  def __init__(self, fieldName: str):
@@ -642,6 +668,51 @@ class Tuple(Jsonable, _TupleToPlainJsonMixin, _TupleToSqlaJsonMixin):
642
668
  def tupleFieldNames(cls) -> List[str]:
643
669
  return cls.__fieldNames__
644
670
 
671
+ @classmethod
672
+ def typeHints(cls) -> Dict[str, Any]:
673
+ """Type Hints
674
+
675
+ Resolved type hints for this class, computed once and stored on the
676
+ class itself.
677
+
678
+ Resolution is lazy: tuples reference each other with forward
679
+ references, so hints can only be resolved after all imports
680
+ complete. Failures are not stored — if the hints are unresolvable
681
+ the exception propagates and the next call retries.
682
+
683
+ The hints are stored in the class's own ``__dict__`` so a subclass
684
+ always resolves and stores its own hints, never a base class's.
685
+
686
+ :return: The resolved type hints for this class.
687
+ """
688
+ cachedHints = cls.__dict__.get("__cachedTypeHints__")
689
+ if cachedHints is not None:
690
+ return cachedHints
691
+
692
+ hints = get_type_hints(cls)
693
+ cls.__cachedTypeHints__ = hints
694
+ return hints
695
+
696
+ @classmethod
697
+ def typeHintsWithExtras(cls) -> Dict[str, Any]:
698
+ """Type Hints With Extras
699
+
700
+ Resolved type hints for this class, including ``Annotated`` extras,
701
+ computed once and stored on the class itself.
702
+
703
+ Resolution is lazy and failures are not stored, same as
704
+ :meth:`typeHints`.
705
+
706
+ :return: The resolved type hints for this class, with extras.
707
+ """
708
+ cachedHints = cls.__dict__.get("__cachedTypeHintsWithExtras__")
709
+ if cachedHints is not None:
710
+ return cachedHints
711
+
712
+ hints = get_type_hints(cls, include_extras=True)
713
+ cls.__cachedTypeHintsWithExtras__ = hints
714
+ return hints
715
+
645
716
  @classmethod
646
717
  def tupleJsonSchema(cls) -> "JsonSchemaNodeTuple":
647
718
  """Tuple JSON Schema
@@ -43,7 +43,6 @@ from .SerialiseUtil import WKBElement
43
43
  from .SerialiseUtil import convertFromWkbElement
44
44
  from .SerialiseUtil import datetimeFromBson
45
45
  from .SerialiseUtil import datetimeToBson
46
- from .SerialiseUtil import datetimeToBsonMicroseconds
47
46
  from .SerialiseUtil import requireAwareDatetime
48
47
  from .Tuple import BsonDatetimeFormatE
49
48
  from .Tuple import TupleField
@@ -65,24 +64,6 @@ class TupleBsonConverter(TupleDocumentConverterBase):
65
64
  return tupleField.bsonDatetimeFormat
66
65
  return None
67
66
 
68
- @classmethod
69
- def _utilDatetimeToBsonStorageValue(
70
- cls,
71
- value: datetime,
72
- bsonDatetimeFormat: BsonDatetimeFormatE | None,
73
- ) -> datetime | int:
74
- if bsonDatetimeFormat is None:
75
- return datetimeToBson(value)
76
- if bsonDatetimeFormat is BsonDatetimeFormatE.standardMsIntEpoch:
77
- return datetimeToBson(value)
78
- if bsonDatetimeFormat is BsonDatetimeFormatE.standardUsIntEpoch:
79
- return datetimeToBsonMicroseconds(value)
80
- if bsonDatetimeFormat is BsonDatetimeFormatE.standardMsBsonDate:
81
- return requireAwareDatetime(value)
82
- raise NotImplementedError(
83
- f"Unsupported BsonDatetimeFormatE {bsonDatetimeFormat!r}"
84
- )
85
-
86
67
  @classmethod
87
68
  def _utilCoerceIntOrFloatEpochToDatetime(
88
69
  cls,
@@ -145,7 +126,7 @@ class TupleBsonConverter(TupleDocumentConverterBase):
145
126
 
146
127
  def convert(
147
128
  value: Any,
148
- bsonDatetimeFormat: BsonDatetimeFormatE | None = None,
129
+ tupleField: TupleField | None = None,
149
130
  ) -> Any:
150
131
  if isinstance(value, list):
151
132
  return [convert(item) for item in value]
@@ -165,17 +146,18 @@ class TupleBsonConverter(TupleDocumentConverterBase):
165
146
  return None
166
147
 
167
148
  if isinstance(value, datetime):
168
- return cls._utilDatetimeToBsonStorageValue(value, bsonDatetimeFormat)
149
+ if tupleField is None:
150
+ return datetimeToBson(value)
151
+ return tupleField.toBson(value)
169
152
 
170
153
  return value
171
154
 
172
155
  for shortName, normalName in tuple_.__shortFieldNamesMap__.items():
173
156
  tupleField = getattr(type(tuple_), normalName, None)
174
- fieldFormat: BsonDatetimeFormatE | None = None
175
- if isinstance(tupleField, TupleField):
176
- fieldFormat = tupleField.bsonDatetimeFormat
157
+ if not isinstance(tupleField, TupleField):
158
+ tupleField = None
177
159
 
178
- value = convert(getattr(tuple_, normalName), fieldFormat)
160
+ value = convert(getattr(tuple_, normalName), tupleField)
179
161
  if value is None and not includeNones:
180
162
  continue
181
163
  if value is False and not includeFalse:
@@ -250,6 +250,33 @@ class TupleBsonTest(unittest.TestCase):
250
250
  )
251
251
  self.assertEqual(deserialisedTuple.at, aware)
252
252
 
253
+ def testTupleFieldToBsonConvertsDatetimePerFormat(self) -> None:
254
+ aware = datetime(2020, 1, 2, 3, 4, 5, 678000, tzinfo=UTC)
255
+ self.assertEqual(
256
+ BsonDatetimeFormatTestTuple.defaultMsDate.toBson(aware),
257
+ datetimeToBson(aware),
258
+ )
259
+ self.assertEqual(
260
+ BsonDatetimeFormatTestTuple.microsecondDate.toBson(aware),
261
+ datetimeToBsonMicroseconds(aware),
262
+ )
263
+ self.assertEqual(
264
+ BsonDatetimeFormatTestTuple.bsonDateField.toBson(aware),
265
+ aware,
266
+ )
267
+
268
+ def testTupleFieldToBsonPassesThroughNonDatetimeValues(self) -> None:
269
+ field = BsonDatetimeFormatTestTuple.defaultMsDate
270
+ self.assertEqual(field.toBson("2026-10-23"), "2026-10-23")
271
+ self.assertEqual(field.toBson(1577934245678), 1577934245678)
272
+ self.assertIs(field.toBson(True), True)
273
+ self.assertIsNone(field.toBson(None))
274
+
275
+ def testTupleFieldToBsonRejectsNaiveDatetime(self) -> None:
276
+ naive = datetime(2020, 1, 2, 3, 4, 5, 678000)
277
+ with self.assertRaises(TypeError):
278
+ BsonDatetimeFormatTestTuple.defaultMsDate.toBson(naive)
279
+
253
280
 
254
281
  class TupleBsonCollectingTest(unittest.TestCase):
255
282
  def testSingleBadScalarCollectsErrorAndPopulatesOtherFields(self) -> None:
@@ -20,7 +20,6 @@ from typing import TYPE_CHECKING, Annotated, Any, NamedTuple, Optional, Type
20
20
 
21
21
  from typing import Union, get_args
22
22
  from typing import get_origin
23
- from typing import get_type_hints
24
23
 
25
24
  from .Tuple import BsonDatetimeFormatE
26
25
  from .Tuple import TUPLE_TYPES_BY_NAME
@@ -751,7 +750,7 @@ class TupleDocumentConverterBase:
751
750
  ) -> TupleValidationResult:
752
751
  errors: list[TupleValidationError] = []
753
752
 
754
- if not get_type_hints(TupleClass):
753
+ if not TupleClass.typeHints():
755
754
  errors.append(
756
755
  cls._makeError(_ROOT_PATH, f"{TupleClass} has no annotations")
757
756
  )
@@ -759,8 +758,8 @@ class TupleDocumentConverterBase:
759
758
 
760
759
  TupleClass = cls._resolveConcreteClass(TupleClass, jsonDict, errors)
761
760
 
762
- typeHints = get_type_hints(TupleClass)
763
- extraTypeHits = get_type_hints(TupleClass, include_extras=True)
761
+ typeHints = TupleClass.typeHints()
762
+ extraTypeHits = TupleClass.typeHintsWithExtras()
764
763
 
765
764
  newTuple = TupleClass()
766
765
 
@@ -9,10 +9,13 @@
9
9
  import datetime
10
10
  import json
11
11
 
12
+ from typing import Annotated, get_type_hints
13
+ from unittest import mock
14
+
12
15
  from twisted.trial import unittest
13
16
 
14
17
  from vortex.TestTuple import TestSubTuple, TestTuple
15
- from vortex.Tuple import TupleHash
18
+ from vortex.Tuple import Tuple, TupleField, TupleHash
16
19
 
17
20
 
18
21
  def makeTupleA():
@@ -124,3 +127,78 @@ class TuplePyTest(unittest.TestCase):
124
127
  self.assertFalse(
125
128
  hashA1 in {hashB1: None}, "TupleHash in fail, A1 in {B1:None}"
126
129
  )
130
+
131
+
132
+ class TupleTypeHintsTest(unittest.TestCase):
133
+ """Tests for the lazy per-class type hint storage on ``Tuple``."""
134
+
135
+ def testTypeHintsCachedOnClass(self):
136
+ """Two calls return the same dict; get_type_hints runs once."""
137
+
138
+ class CachedHintsTuple(Tuple):
139
+ aInt: int = TupleField()
140
+ aStr: str = TupleField()
141
+
142
+ countingHints = mock.Mock(side_effect=get_type_hints)
143
+ with mock.patch("vortex.Tuple.get_type_hints", countingHints):
144
+ hints1 = CachedHintsTuple.typeHints()
145
+ hints2 = CachedHintsTuple.typeHints()
146
+
147
+ self.assertIs(hints1, hints2)
148
+ self.assertEqual(countingHints.call_count, 1)
149
+ self.assertIs(hints1["aInt"], int)
150
+ self.assertIs(hints1["aStr"], str)
151
+
152
+ def testTypeHintsWithExtrasCachedOnClass(self):
153
+ """Annotated extras are kept; get_type_hints runs once."""
154
+
155
+ class CachedExtrasTuple(Tuple):
156
+ aInt: Annotated[int, "marker"] = TupleField()
157
+
158
+ countingHints = mock.Mock(side_effect=get_type_hints)
159
+ with mock.patch("vortex.Tuple.get_type_hints", countingHints):
160
+ hints1 = CachedExtrasTuple.typeHintsWithExtras()
161
+ hints2 = CachedExtrasTuple.typeHintsWithExtras()
162
+
163
+ self.assertIs(hints1, hints2)
164
+ self.assertEqual(countingHints.call_count, 1)
165
+ self.assertEqual(
166
+ countingHints.call_args, mock.call(CachedExtrasTuple, include_extras=True)
167
+ )
168
+ self.assertEqual(hints1["aInt"].__metadata__, ("marker",))
169
+
170
+ def testTypeHintsSubclassStoresOwnHints(self):
171
+ """A subclass never returns a base class's cached hints."""
172
+
173
+ class ParentHintsTuple(Tuple):
174
+ aStr: str = TupleField()
175
+
176
+ class ChildHintsTuple(ParentHintsTuple):
177
+ aInt: int = TupleField()
178
+
179
+ parentHints = ParentHintsTuple.typeHints()
180
+ childHints = ChildHintsTuple.typeHints()
181
+
182
+ self.assertIsNot(parentHints, childHints)
183
+ self.assertIs(parentHints["aStr"], str)
184
+ self.assertNotIn("aInt", parentHints)
185
+ self.assertIs(childHints["aStr"], str)
186
+ self.assertIs(childHints["aInt"], int)
187
+
188
+ def testTypeHintsFailureNotCached(self):
189
+ """A failed resolution propagates and the next call retries."""
190
+
191
+ class FailingHintsTuple(Tuple):
192
+ aInt: int = TupleField()
193
+
194
+ flakyHints = mock.Mock(
195
+ side_effect=[NameError("unresolvable"), {"aInt": int}]
196
+ )
197
+ with mock.patch("vortex.Tuple.get_type_hints", flakyHints):
198
+ self.assertRaises(NameError, FailingHintsTuple.typeHints)
199
+ hints = FailingHintsTuple.typeHints()
200
+ again = FailingHintsTuple.typeHints()
201
+
202
+ self.assertEqual(hints, {"aInt": int})
203
+ self.assertIs(hints, again)
204
+ self.assertEqual(flakyHints.call_count, 2)
@@ -6,4 +6,4 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
- __version__ = '4.3.7'
9
+ __version__ = '4.3.9'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 4.3.7
3
+ Version: 4.3.9
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
@@ -1,4 +1,3 @@
1
- LICENSE
2
1
  README.md
3
2
  pyproject.toml
4
3
  example/TwistedDeferredTimeout.py
vortexpy-4.3.7/LICENSE DELETED
@@ -1,22 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2016-2026 Flying Ion Pty Ltd
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
22
-
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