vortexpy 4.3.6__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 (145) hide show
  1. {vortexpy-4.3.6 → vortexpy-4.3.9}/PKG-INFO +1 -1
  2. {vortexpy-4.3.6 → vortexpy-4.3.9}/pyproject.toml +1 -1
  3. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/SerialiseUtil.py +8 -0
  4. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/Tuple.py +71 -0
  5. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleBson.py +7 -25
  6. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleBson_test.py +27 -0
  7. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleDocumentConverter.py +3 -4
  8. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleTest.py +79 -1
  9. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServer.py +3 -3
  10. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServer_test.py +15 -1
  11. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/__init__.py +1 -1
  12. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortexpy.egg-info/PKG-INFO +1 -1
  13. {vortexpy-4.3.6 → vortexpy-4.3.9}/README.md +0 -0
  14. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/TwistedDeferredTimeout.py +0 -0
  15. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/__init__.py +0 -0
  16. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  17. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  18. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  19. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  20. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/run_websocket_rpc_test_client.py +0 -0
  21. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/run_websocket_rpc_test_server.py +0 -0
  22. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/setup_example_logging.py +0 -0
  23. {vortexpy-4.3.6 → vortexpy-4.3.9}/example/store_and_load_json.py +0 -0
  24. {vortexpy-4.3.6 → vortexpy-4.3.9}/setup.cfg +0 -0
  25. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/DataWrapTuple.py +0 -0
  26. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/DeferUtil.py +0 -0
  27. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/DeferUtil_test.py +0 -0
  28. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/Jsonable.py +0 -0
  29. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  30. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  31. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/MtlsServerSslOptions.py +0 -0
  32. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/Payload.py +0 -0
  33. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadEndpoint.py +0 -0
  34. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadEndpointTest.py +0 -0
  35. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadEnvelope.py +0 -0
  36. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadFilterKeys.py +0 -0
  37. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadIO.py +0 -0
  38. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadPriority.py +0 -0
  39. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadResponse.py +0 -0
  40. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadResponseTest.py +0 -0
  41. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadResponse_test.py +0 -0
  42. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PayloadTest.py +0 -0
  43. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/PerformTestActionTuple.py +0 -0
  44. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/SerialiseUtil_test.py +0 -0
  45. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TestTuple.py +0 -0
  46. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleAction.py +0 -0
  47. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleDataForTest.py +0 -0
  48. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TuplePerfTest.py +0 -0
  49. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  50. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleRestful.py +0 -0
  51. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleRestful_test.py +0 -0
  52. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/TupleSelector.py +0 -0
  53. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexABC.py +0 -0
  54. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexAccessControlContextABC.py +0 -0
  55. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexAuthenticatorABC.py +0 -0
  56. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexClientHttp.py +0 -0
  57. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexClientHttpTest.py +0 -0
  58. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexClientTcp.py +0 -0
  59. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexClientWebsocketFactory.py +0 -0
  60. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexConnectionABC.py +0 -0
  61. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexEndpointPermission.py +0 -0
  62. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexFactory.py +0 -0
  63. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexFactory_test.py +0 -0
  64. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexPayloadProtocol.py +0 -0
  65. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexPermission.py +0 -0
  66. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexPermissionError.py +0 -0
  67. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServerConnection.py +0 -0
  68. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServerConnection_test.py +0 -0
  69. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServerHttpResource.py +0 -0
  70. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServerTcp.py +0 -0
  71. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServerWebsocket.py +0 -0
  72. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexServerWebsocket_test.py +0 -0
  73. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexSession.py +0 -0
  74. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexSession_test.py +0 -0
  75. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  76. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  77. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexTest.py +0 -0
  78. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexUtil.py +0 -0
  79. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexWritePushProducer.py +0 -0
  80. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/VortexWritePushProducer_test.py +0 -0
  81. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  82. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoader.py +0 -0
  83. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  84. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  85. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  86. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/data_loader/__init__.py +0 -0
  87. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  88. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  89. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  90. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/event_bus/__init__.py +0 -0
  91. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessor.py +0 -0
  92. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  93. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  94. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  95. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleActionPushOffline.py +0 -0
  96. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  97. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  98. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataActionClient.py +0 -0
  99. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableCache.py +0 -0
  100. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  101. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableHandler.py +0 -0
  102. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableHandler_test.py +0 -0
  103. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
  104. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  105. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  106. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataObserverClient.py +0 -0
  107. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/TupleDataProviderABC.py +0 -0
  108. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  109. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/handler/__init__.py +0 -0
  110. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/mypy_plugin.py +0 -0
  111. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  112. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/py.typed +0 -0
  113. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/GzippedDataHttpClient.py +0 -0
  114. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  115. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  116. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/RestfulHttpClient.py +0 -0
  117. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/RestfulResource.py +0 -0
  118. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/__init__.py +0 -0
  119. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/restful_http_client_test.py +0 -0
  120. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/restful/restful_resource_test.py +0 -0
  121. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/rpc/RPC.py +0 -0
  122. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/rpc/RPCTest.py +0 -0
  123. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/rpc/RPC_test.py +0 -0
  124. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/rpc/__init__.py +0 -0
  125. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  126. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  127. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  128. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/sqla_orm/__init__.py +0 -0
  129. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  130. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/storage/TupleStorageSqlite.py +0 -0
  131. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/storage/__init__.py +0 -0
  132. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/tuple_json_schema/__init__.py +0 -0
  133. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/tuple_json_schema/constants.py +0 -0
  134. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  135. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  136. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  137. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  138. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/tuple_test_fixtures.py +0 -0
  139. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/twisted_runtime_metrics.py +0 -0
  140. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortex/twisted_runtime_metrics_test.py +0 -0
  141. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortexpy.egg-info/SOURCES.txt +0 -0
  142. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortexpy.egg-info/dependency_links.txt +0 -0
  143. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortexpy.egg-info/entry_points.txt +0 -0
  144. {vortexpy-4.3.6 → vortexpy-4.3.9}/vortexpy.egg-info/requires.txt +0 -0
  145. {vortexpy-4.3.6 → 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.6
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.6"
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)
@@ -154,9 +154,9 @@ class VortexServer(VortexABC):
154
154
  )
155
155
 
156
156
  httpSessionConns = VortexSessionI(httpSession).connections
157
- for oldConn in list(httpSessionConns.values()):
158
- if oldConn is not vortexConnection:
159
- oldConn.close()
157
+ existingConn = httpSessionConns.get(vortexUuid)
158
+ if existingConn is not None and existingConn is not vortexConnection:
159
+ existingConn.close()
160
160
  httpSessionConns[vortexUuid] = vortexConnection
161
161
 
162
162
  if vortexUuid in self._connectionByVortexUuid:
@@ -84,12 +84,26 @@ class VortexServerConnectionClosedTest(SynchronousTestCase):
84
84
  server.connectionOpened(session, conn_a)
85
85
  server.connectionOpened(session, conn_b)
86
86
 
87
+ # A new connection with a different vortexUuid must not close the
88
+ # existing same-session connection (two browser tabs stay live).
87
89
  conns = VortexSessionI(session).connections
90
+ self.assertEqual(conns.get("uuid-a"), conn_a)
91
+ self.assertEqual(conns.get("uuid-b"), conn_b)
92
+
93
+ # Reconnecting with the same vortexUuid replaces the old connection.
94
+ conn_a2 = _MockServerConnection(
95
+ server, "uuid-a", "peek-office-app", session
96
+ )
97
+ server.connectionOpened(session, conn_a2)
98
+
99
+ self.assertIs(conns.get("uuid-a"), conn_a2)
88
100
  self.assertEqual(conns.get("uuid-b"), conn_b)
89
- self.assertNotIn("uuid-a", conns)
90
101
 
102
+ # The replaced connection's teardown is idempotent and does not
103
+ # disturb the surviving connections.
91
104
  server.connectionClosed(conn_a)
92
105
 
106
+ self.assertIs(conns.get("uuid-a"), conn_a2)
93
107
  self.assertEqual(conns.get("uuid-b"), conn_b)
94
108
  self.assertIn("uuid-b", server._connectionByVortexUuid)
95
109
 
@@ -6,4 +6,4 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
- __version__ = '4.3.6'
9
+ __version__ = '4.3.9'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 4.3.6
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
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