vortexpy 6.2.6__tar.gz → 6.3.1__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 (161) hide show
  1. {vortexpy-6.2.6 → vortexpy-6.3.1}/PKG-INFO +1 -1
  2. {vortexpy-6.2.6 → vortexpy-6.3.1}/pyproject.toml +1 -1
  3. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/Jsonable.py +58 -15
  4. vortexpy-6.3.1/vortex/Jsonable_tuple_dict_coercion_test.py +144 -0
  5. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/SerialiseUtil.py +29 -8
  6. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleBson_test.py +23 -0
  7. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleDocumentConverter.py +13 -6
  8. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServer.py +0 -21
  9. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServer_test.py +0 -38
  10. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexSession.py +2 -7
  11. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/__init__.py +1 -1
  12. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObservableHandler_test.py +16 -19
  13. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObservableProxyHandler.py +2 -3
  14. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataOfflineObserverService.py +21 -0
  15. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataOfflineObserverService_test.py +49 -2
  16. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/tuple_test_fixtures.py +6 -0
  17. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortexpy.egg-info/PKG-INFO +1 -1
  18. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortexpy.egg-info/SOURCES.txt +1 -0
  19. {vortexpy-6.2.6 → vortexpy-6.3.1}/README.md +0 -0
  20. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/TwistedDeferredTimeout.py +0 -0
  21. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/__init__.py +0 -0
  22. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  23. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  24. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  25. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  26. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/run_websocket_rpc_test_client.py +0 -0
  27. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/run_websocket_rpc_test_server.py +0 -0
  28. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/setup_example_logging.py +0 -0
  29. {vortexpy-6.2.6 → vortexpy-6.3.1}/example/store_and_load_json.py +0 -0
  30. {vortexpy-6.2.6 → vortexpy-6.3.1}/setup.cfg +0 -0
  31. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/DataWrapTuple.py +0 -0
  32. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/DeferUtil.py +0 -0
  33. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/DeferUtil_test.py +0 -0
  34. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  35. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  36. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/MtlsServerSslOptions.py +0 -0
  37. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/Payload.py +0 -0
  38. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadEndpoint.py +0 -0
  39. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadEndpointTest.py +0 -0
  40. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadEnvelope.py +0 -0
  41. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadFilterKeys.py +0 -0
  42. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadIO.py +0 -0
  43. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadPriority.py +0 -0
  44. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadResponse.py +0 -0
  45. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadResponseTest.py +0 -0
  46. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadResponse_test.py +0 -0
  47. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PayloadTest.py +0 -0
  48. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/PerformTestActionTuple.py +0 -0
  49. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/SerialiseUtil_test.py +0 -0
  50. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TestTuple.py +0 -0
  51. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/Tuple.py +0 -0
  52. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleAction.py +0 -0
  53. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleBson.py +0 -0
  54. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleDataForTest.py +0 -0
  55. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TuplePerfTest.py +0 -0
  56. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  57. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleRestful.py +0 -0
  58. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleRestful_test.py +0 -0
  59. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleSelector.py +0 -0
  60. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/TupleTest.py +0 -0
  61. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexABC.py +0 -0
  62. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexAccessControlContextABC.py +0 -0
  63. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexAuthenticatorABC.py +0 -0
  64. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexClientHttp.py +0 -0
  65. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexClientHttpTest.py +0 -0
  66. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexClientTcp.py +0 -0
  67. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexClientWebsocketFactory.py +0 -0
  68. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexConnectionABC.py +0 -0
  69. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexEndpointPermission.py +0 -0
  70. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexFactory.py +0 -0
  71. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexFactory_test.py +0 -0
  72. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexPayloadProtocol.py +0 -0
  73. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexPermission.py +0 -0
  74. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexPermissionError.py +0 -0
  75. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServerConnection.py +0 -0
  76. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServerConnection_test.py +0 -0
  77. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServerHttpResource.py +0 -0
  78. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServerTcp.py +0 -0
  79. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServerTcp_test.py +0 -0
  80. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServerWebsocket.py +0 -0
  81. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexServerWebsocket_test.py +0 -0
  82. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexSession_test.py +0 -0
  83. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  84. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  85. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexTest.py +0 -0
  86. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexUserDetailsABC.py +0 -0
  87. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexUtil.py +0 -0
  88. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexUtil_test.py +0 -0
  89. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexWritePushProducer.py +0 -0
  90. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/VortexWritePushProducer_test.py +0 -0
  91. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  92. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/data_loader/TupleDataLiveLockManager_test.py +0 -0
  93. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/data_loader/TupleDataLoader.py +0 -0
  94. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  95. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  96. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  97. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/data_loader/__init__.py +0 -0
  98. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  99. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  100. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  101. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/event_bus/__init__.py +0 -0
  102. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleActionProcessor.py +0 -0
  103. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  104. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  105. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  106. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleActionProcessor_test.py +0 -0
  107. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleActionPushOffline.py +0 -0
  108. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  109. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  110. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataActionClient.py +0 -0
  111. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataActionClient_test.py +0 -0
  112. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObservableCache.py +0 -0
  113. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  114. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObservableHandler.py +0 -0
  115. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  116. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  117. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataObserverClient.py +0 -0
  118. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/TupleDataProviderABC.py +0 -0
  119. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  120. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/handler/__init__.py +0 -0
  121. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/mypy_plugin.py +0 -0
  122. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  123. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/py.typed +0 -0
  124. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/GzippedDataHttpClient.py +0 -0
  125. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  126. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  127. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/RestfulHttpClient.py +0 -0
  128. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/RestfulResource.py +0 -0
  129. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/__init__.py +0 -0
  130. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/restful_http_client_test.py +0 -0
  131. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/restful/restful_resource_test.py +0 -0
  132. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/rpc/RPC.py +0 -0
  133. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/rpc/RPCTest.py +0 -0
  134. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/rpc/RPC_test.py +0 -0
  135. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/rpc/__init__.py +0 -0
  136. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  137. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  138. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  139. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/sqla_orm/__init__.py +0 -0
  140. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  141. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleOfflineStorageNameService.py +0 -0
  142. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleOfflineStorageSecureService.py +0 -0
  143. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleOfflineStorageSecureService_test.py +0 -0
  144. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleOfflineStorageService.py +0 -0
  145. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleStorageServiceABC.py +0 -0
  146. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleStorageSqlite.py +0 -0
  147. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleStorageSqliteService.py +0 -0
  148. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/TupleStorageSqliteService_test.py +0 -0
  149. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/storage/__init__.py +0 -0
  150. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/tuple_json_schema/__init__.py +0 -0
  151. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/tuple_json_schema/constants.py +0 -0
  152. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  153. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  154. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  155. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  156. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/twisted_runtime_metrics.py +0 -0
  157. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortex/twisted_runtime_metrics_test.py +0 -0
  158. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortexpy.egg-info/dependency_links.txt +0 -0
  159. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortexpy.egg-info/entry_points.txt +0 -0
  160. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortexpy.egg-info/requires.txt +0 -0
  161. {vortexpy-6.2.6 → vortexpy-6.3.1}/vortexpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 6.2.6
3
+ Version: 6.3.1
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 = "6.2.6"
7
+ version = "6.3.1"
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."
@@ -9,10 +9,9 @@
9
9
  import logging
10
10
  import traceback
11
11
  import typing
12
+ from _collections import defaultdict
12
13
  from copy import copy, deepcopy
13
14
  from json import JSONEncoder
14
- from typing import List
15
- from _collections import defaultdict
16
15
 
17
16
  logger = logging.getLogger(__name__)
18
17
 
@@ -27,15 +26,15 @@ JSONEncoder.item_separator = ","
27
26
  JSONEncoder.key_separator = ":"
28
27
 
29
28
 
30
- class Jsonable(object):
29
+ class Jsonable:
31
30
  """Xmlable
32
31
  Inherit from this class if you want to serialise to XML.
33
32
  """
34
33
 
35
- __fieldNames__: List[str] = None
34
+ __fieldNames__: list[str] = None
36
35
  __rapuiSerialiseType__ = T_GENERIC_CLASS
37
36
 
38
- __rawJonableFields__: List[str] = None
37
+ __rawJonableFields__: list[str] = None
39
38
 
40
39
  JSON_CLASS_TYPE = "_ct"
41
40
  JSON_CLASS = "_c"
@@ -52,9 +51,7 @@ class Jsonable(object):
52
51
  cls.__memoryLoggingEnabled = True
53
52
 
54
53
  @classmethod
55
- def memoryLoggingDump(
56
- cls, top=10, over=100
57
- ) -> List[typing.Tuple[str, int]]:
54
+ def memoryLoggingDump(cls, top=10, over=100) -> list[tuple[str, int]]:
58
55
  if not Jsonable.__memoryLoggingRefs:
59
56
  return []
60
57
 
@@ -67,10 +64,10 @@ class Jsonable(object):
67
64
  return list(filter(lambda x: x[1] >= over, data))[:top]
68
65
 
69
66
  def __memLoggingKey(self):
70
- if getattr(self, "__rapuiSerialiseType__") == T_RAPUI_TUPLE:
71
- key = "Tuple: " + getattr(self, "__tupleType__")
67
+ if self.__rapuiSerialiseType__ == T_RAPUI_TUPLE:
68
+ key = "Tuple: " + self.__tupleType__
72
69
 
73
- elif getattr(self, "__rapuiSerialiseType__") == T_RAPUI_PAYLOAD:
70
+ elif self.__rapuiSerialiseType__ == T_RAPUI_PAYLOAD:
74
71
  assert hasattr(self, "filt"), "Payload is missing filt"
75
72
  if "key" in self.filt:
76
73
  key = "Payload: key=" + self.filt["key"]
@@ -78,7 +75,7 @@ class Jsonable(object):
78
75
  key = "Payload: " + str(self.filt)
79
76
 
80
77
  else:
81
- key = "Jsonable: " + getattr(self, "__rapuiSerialiseType__")
78
+ key = "Jsonable: " + self.__rapuiSerialiseType__
82
79
 
83
80
  return key
84
81
 
@@ -100,9 +97,7 @@ class Jsonable(object):
100
97
  from .Tuple import TupleField
101
98
 
102
99
  if isinstance(value, TupleField):
103
- value = (
104
- None if value.defaultValue == None else copy(value.defaultValue)
105
- )
100
+ value = None if value.defaultValue == None else copy(value.defaultValue)
106
101
 
107
102
  try:
108
103
  valueType = V_NULL if value is None else toRapuiType(value)
@@ -316,6 +311,54 @@ class Jsonable(object):
316
311
  ):
317
312
  value = enumClass(value)
318
313
 
314
+ # Reconstruct Tuple subclasses for fields annotated with a Tuple
315
+ # type. Serialised tuples may arrive as plain dicts (no _ct/_c
316
+ # markers) when the sender has no Tuple class for the value; the
317
+ # receiving class's annotation drives the coercion.
318
+ value = self._coerceValueToTypeHint(value, hints.get(name))
319
+
319
320
  setattr(self, name, value)
320
321
 
321
322
  return self
323
+
324
+ def _coerceValueToTypeHint(
325
+ self, value: typing.Any, typeHint: typing.Any
326
+ ) -> typing.Any:
327
+ """Coerce a plain JSON value into the type declared by *typeHint*.
328
+
329
+ Currently handles ``vortex.Tuple`` subclasses (and lists of them);
330
+ other hints pass through unchanged. Plain dicts are validated:
331
+ unknown keys raise ``KeyError``. Values that are neither a dict nor
332
+ ``None`` for a Tuple-typed field raise ``ValueError``.
333
+ """
334
+ from .Tuple import Tuple
335
+
336
+ tupleClass = utilTupleClassFromTypeHint(typeHint)
337
+ if tupleClass is not None:
338
+ if isinstance(value, tupleClass):
339
+ return value
340
+ if isinstance(value, dict):
341
+ extraKeys = set(value) - set(tupleClass.__fieldNames__)
342
+ if extraKeys:
343
+ raise KeyError(
344
+ f"Unexpected keys for {tupleClass.__name__}:"
345
+ f" {sorted(extraKeys)}"
346
+ )
347
+ return Tuple.restfulJsonDictToTupleWithValidation(value, tupleClass)
348
+ if value is None:
349
+ return None
350
+ raise ValueError(
351
+ f"Expected dict for {tupleClass.__name__}, got {type(value).__name__}"
352
+ )
353
+
354
+ origin = typing.get_origin(typeHint)
355
+ if origin is list and isinstance(value, list):
356
+ args = typing.get_args(typeHint)
357
+ if args:
358
+ elementClass = utilTupleClassFromTypeHint(args[0])
359
+ if elementClass is not None:
360
+ return [
361
+ self._coerceValueToTypeHint(item, args[0]) for item in value
362
+ ]
363
+
364
+ return value
@@ -0,0 +1,144 @@
1
+ # ruff: noqa: C408, N999
2
+ # Copyright (c) 2016-2026 Flying Ion Pty Ltd
3
+ #
4
+ # This software is open source; the MIT license applies.
5
+ # See LICENSE for the full license text.
6
+
7
+ """Tests for Jsonable.fromJsonDict coercion of plain dicts into Tuple fields."""
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import UTC, datetime
12
+ from enum import StrEnum
13
+
14
+ import pytest
15
+
16
+ from vortex.SerialiseUtil import ISO8601
17
+ from vortex.Tuple import Tuple, TupleField, addTupleType
18
+
19
+
20
+ class _ColorE(StrEnum):
21
+ RED = "red"
22
+ BLUE = "blue"
23
+
24
+
25
+ @addTupleType
26
+ class _InnerTuple(Tuple):
27
+ __tupleType__ = "flyingion.vortex.JsonableCoercionInnerTuple"
28
+ fieldMap: dict[str, str] = TupleField()
29
+ color: _ColorE = TupleField()
30
+
31
+
32
+ @addTupleType
33
+ class _OuterTuple(Tuple):
34
+ __tupleType__ = "flyingion.vortex.JsonableCoercionOuterTuple"
35
+ inner: _InnerTuple = TupleField()
36
+ inners: list[_InnerTuple] = TupleField()
37
+ optInner: _InnerTuple | None = TupleField()
38
+
39
+
40
+ @addTupleType
41
+ class _DatetimeInnerTuple(Tuple):
42
+ __tupleType__ = "flyingion.vortex.JsonableCoercionDatetimeInnerTuple"
43
+ when: datetime = TupleField()
44
+
45
+
46
+ @addTupleType
47
+ class _DatetimeOuterTuple(Tuple):
48
+ __tupleType__ = "flyingion.vortex.JsonableCoercionDatetimeOuterTuple"
49
+ inner: _DatetimeInnerTuple = TupleField()
50
+
51
+
52
+ def test_from_json_dict_coerces_nested_tuple_field() -> None:
53
+ data = dict(
54
+ inner=dict(fieldMap=dict(payload="body"), color="blue"),
55
+ inners=[dict(fieldMap=dict(x="y"), color="red")],
56
+ optInner=None,
57
+ )
58
+ result = _OuterTuple().fromJsonDict(data)
59
+
60
+ assert isinstance(result.inner, _InnerTuple), (
61
+ "plain dict inner must coerce to declared Tuple type"
62
+ )
63
+ assert result.inner.fieldMap == dict(payload="body"), (
64
+ "fieldMap must coerce nested dict values"
65
+ )
66
+ assert result.inner.color == _ColorE.BLUE, (
67
+ "enum field must coerce from string value"
68
+ )
69
+ assert isinstance(result.inners, list), "inners must be a list"
70
+ assert len(result.inners) == 1, "inners must contain one item"
71
+ assert isinstance(result.inners[0], _InnerTuple), (
72
+ "list item must coerce to declared Tuple type"
73
+ )
74
+ assert result.inners[0].color == _ColorE.RED, (
75
+ "list item enum must coerce from string value"
76
+ )
77
+ assert result.optInner is None, "optInner None must be preserved"
78
+
79
+
80
+ def test_from_json_dict_coerces_optional_nested_tuple_field() -> None:
81
+ data = dict(
82
+ inner=dict(fieldMap=dict(), color="red"),
83
+ inners=[],
84
+ optInner=dict(fieldMap=dict(k="v"), color="blue"),
85
+ )
86
+ result = _OuterTuple().fromJsonDict(data)
87
+
88
+ assert isinstance(result.optInner, _InnerTuple), (
89
+ "optional inner dict must coerce to declared Tuple type"
90
+ )
91
+ assert result.optInner.color == _ColorE.BLUE, (
92
+ "optional inner enum must coerce from string value"
93
+ )
94
+
95
+
96
+ def test_from_json_dict_coerces_nested_tuple_with_iso_datetime() -> None:
97
+ when = datetime(2024, 6, 15, 10, 30, 45, 123456, tzinfo=UTC)
98
+ data = dict(
99
+ inner=dict(when=when.strftime(ISO8601)),
100
+ )
101
+ result = _DatetimeOuterTuple().fromJsonDict(data)
102
+
103
+ assert isinstance(result.inner.when, datetime), (
104
+ "nested tuple datetime field must decode to datetime"
105
+ )
106
+ assert result.inner.when == when, "nested tuple datetime field must preserve value"
107
+
108
+
109
+ def test_from_json_dict_enum_inside_nested_tuple_reconstructs() -> None:
110
+ data = dict(inner=dict(fieldMap=dict(), color="blue"), inners=[], optInner=None)
111
+ result = _OuterTuple().fromJsonDict(data)
112
+
113
+ assert result.inner.color == _ColorE.BLUE, (
114
+ "nested enum field must reconstruct from string"
115
+ )
116
+ assert isinstance(result.inner.color, _ColorE), (
117
+ "nested enum field must be the enum type"
118
+ )
119
+
120
+
121
+ def test_from_json_dict_missing_required_key_raises_validation_error() -> None:
122
+ data = dict(
123
+ inner=dict(fieldMap=dict(payload="body")), # missing color
124
+ inners=[],
125
+ optInner=None,
126
+ )
127
+ with pytest.raises((KeyError, TypeError)):
128
+ _OuterTuple().fromJsonDict(data)
129
+
130
+
131
+ def test_from_json_dict_extra_key_raises_validation_error() -> None:
132
+ data = dict(
133
+ inner=dict(fieldMap=dict(), color="red", extra=1),
134
+ inners=[],
135
+ optInner=None,
136
+ )
137
+ with pytest.raises((KeyError, TypeError)):
138
+ _OuterTuple().fromJsonDict(data)
139
+
140
+
141
+ def test_from_json_dict_non_dict_for_tuple_typed_field_raises() -> None:
142
+ data = dict(inner="not-a-dict", inners=[], optInner=None)
143
+ with pytest.raises(ValueError):
144
+ _OuterTuple().fromJsonDict(data)
@@ -12,9 +12,9 @@ import functools
12
12
  import logging
13
13
  import types
14
14
  import typing
15
- from base64 import b64encode, b64decode
15
+ from base64 import b64decode, b64encode
16
16
  from collections import defaultdict
17
- from datetime import UTC, datetime, date
17
+ from datetime import UTC, date, datetime
18
18
 
19
19
  import pytz
20
20
  from sqlalchemy.orm.collections import InstrumentedList
@@ -114,15 +114,15 @@ def convertFromWkbElement(value):
114
114
 
115
115
 
116
116
  def serialiseWkb(wkb: WKBElement) -> str:
117
- from geoalchemy2.shape import to_shape
118
117
  import shapely
118
+ from geoalchemy2.shape import to_shape
119
119
 
120
120
  return b64encode(shapely.wkb.dumps(to_shape(wkb))).decode()
121
121
 
122
122
 
123
123
  def deserialiseWkb(wkbB64Data: str) -> WKBElement:
124
- from geoalchemy2.shape import from_shape
125
124
  import shapely
125
+ from geoalchemy2.shape import from_shape
126
126
 
127
127
  return from_shape(shapely.wkb.loads(b64decode(wkbB64Data)))
128
128
 
@@ -253,9 +253,7 @@ def fromStr(val: str, typeName: str):
253
253
  if typeName == T_DATETIME:
254
254
  # TODO: Check with ISO8601_JS_REGEXP
255
255
  if len(val) == len("2017-06-06T09:40:37.097Z"):
256
- return datetime.strptime(val, ISO8601_JS_FORMAT).replace(
257
- tzinfo=pytz.UTC
258
- )
256
+ return datetime.strptime(val, ISO8601_JS_FORMAT).replace(tzinfo=pytz.UTC)
259
257
 
260
258
  if len(val) <= len("2017-06-06 09:40:37.097068"):
261
259
  raise ValueError(
@@ -286,7 +284,7 @@ def fromRapuiType(typeName: str):
286
284
  return TYPE_MAP_RAPUI_TO_PY[typeName]
287
285
 
288
286
 
289
- @functools.lru_cache(maxsize=None)
287
+ @functools.cache
290
288
  def utilTypeHintsForClass(cls: type) -> dict[str, typing.Any]:
291
289
  """Resolved type hints for a class, cached per class.
292
290
 
@@ -328,6 +326,29 @@ def utilEnumClassFromTypeHint(typeHint: typing.Any) -> type[enum.Enum] | None:
328
326
  return None
329
327
 
330
328
 
329
+ def utilTupleClassFromTypeHint(typeHint: typing.Any) -> type | None:
330
+ """The ``vortex.Tuple`` subclass for a field type hint, or None.
331
+
332
+ Handles bare ``SomeTuple``, ``SomeTuple | None`` and
333
+ ``Optional[SomeTuple]``; every other hint returns None.
334
+ """
335
+ from vortex.Tuple import Tuple
336
+
337
+ if typeHint is None:
338
+ return None
339
+
340
+ if isinstance(typeHint, type) and issubclass(typeHint, Tuple):
341
+ return typeHint
342
+
343
+ origin = typing.get_origin(typeHint)
344
+ if origin in (typing.Union, types.UnionType):
345
+ for arg in typing.get_args(typeHint):
346
+ if isinstance(arg, type) and issubclass(arg, Tuple):
347
+ return arg
348
+
349
+ return None
350
+
351
+
331
352
  def toRapuiType(value):
332
353
  if value is None:
333
354
  return V_NULL
@@ -26,6 +26,7 @@ from vortex.tuple_test_fixtures import (
26
26
  BsonObjectTypeE,
27
27
  BsonOptionalDatetimeTestTuple,
28
28
  BsonOptionalFloatTestTuple,
29
+ BsonOptionalListTestTuple,
29
30
  BsonPriorityE,
30
31
  BsonTestTuple,
31
32
  makeBsonTestTuple,
@@ -71,6 +72,28 @@ class TupleBsonTest(unittest.TestCase):
71
72
  self.assertEqual(deserialisedTuple.requiredFloat, 151.0)
72
73
  self.assertEqual(deserialisedTuple.optionalFloat, 0.0)
73
74
 
75
+ def testOptionalStrListRoundTrip(self) -> None:
76
+ origTuple = BsonOptionalListTestTuple(optionalStrList=["low", "high"])
77
+ bsonDict = origTuple.tupleToBsonJsonDict()
78
+ deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
79
+ bsonDict, BsonOptionalListTestTuple
80
+ )
81
+ self.assertEqual(deserialisedTuple.optionalStrList, ["low", "high"])
82
+
83
+ def testOptionalStrListNoneRoundTrip(self) -> None:
84
+ origTuple = BsonOptionalListTestTuple(optionalStrList=None)
85
+ bsonDict = origTuple.tupleToBsonJsonDict()
86
+ deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
87
+ bsonDict, BsonOptionalListTestTuple
88
+ )
89
+ self.assertIsNone(deserialisedTuple.optionalStrList)
90
+
91
+ def testOptionalStrListAbsentDecodesToNone(self) -> None:
92
+ deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
93
+ {}, BsonOptionalListTestTuple
94
+ )
95
+ self.assertIsNone(deserialisedTuple.optionalStrList)
96
+
74
97
  def testBsonFractionalDoubleParsesAsIntFieldWithWarning(self) -> None:
75
98
  bsonDict = validBsonDict()
76
99
  bsonDict["aInt"] = 151.7
@@ -421,12 +421,8 @@ class TupleDocumentConverterBase:
421
421
  return cls._makeError(field_path, str(exc), ValueError)
422
422
  return None
423
423
 
424
- if isinstance(value, list):
425
- if get_origin(typeHint) is list:
426
- return None
427
- for unionArg in get_args(typeHint):
428
- if get_origin(unionArg) is list:
429
- return None
424
+ if isinstance(value, list) and get_origin(typeHint) is list:
425
+ return None
430
426
 
431
427
  if isinstance(value, dict):
432
428
  if get_origin(typeHint) is dict:
@@ -717,6 +713,17 @@ class TupleDocumentConverterBase:
717
713
  errors.append(intError)
718
714
  return _ConvertResult(_UNSET, errors)
719
715
 
716
+ # Narrow ``list[X] | None`` unions to the list hint for list values
717
+ # so _checkType and _convertListCollecting see the item hint.
718
+ if isinstance(value, list) and get_origin(typeHint) is not list:
719
+ unionListArgs = [
720
+ unionArg
721
+ for unionArg in get_args(typeHint)
722
+ if get_origin(unionArg) is list
723
+ ]
724
+ if unionListArgs:
725
+ typeHint = unionListArgs[0]
726
+
720
727
  typeError = cls._checkType(
721
728
  TupleClass, field_path, fieldName, typeHint, value, validator
722
729
  )
@@ -27,7 +27,6 @@ from zope.interface.declarations import implementer
27
27
  from .DeferUtil import isMainThread
28
28
  from .DeferUtil import vortexLogFailure
29
29
  from .VortexSession import IVortexSession
30
- from .VortexSession import VortexTcpSession
31
30
  from .PayloadEnvelope import PayloadEnvelope
32
31
  from .PayloadEnvelope import VortexMsgList
33
32
  from .PayloadFilterKeys import rapuiServerEcho
@@ -395,23 +394,3 @@ class VortexSession(object):
395
394
 
396
395
 
397
396
  registerAdapter(VortexSession, Session, VortexSessionI)
398
-
399
-
400
- def _tcpSessionVortexSessionIAdapter(session: VortexTcpSession) -> VortexSession:
401
- """Adapt a :class:`VortexTcpSession` to :class:`VortexSessionI`.
402
-
403
- Twisted ``Session`` instances cache adapters through
404
- ``Componentized.__conform__``; the TCP stub is not a ``Componentized``,
405
- so the adapter is cached on the stub's own component store — without
406
- this, every adaptation would return a fresh adapter with an empty
407
- ``connections`` dict and ``connectionOpened`` registrations would be
408
- lost.
409
- """
410
- adapter = session.getComponent(VortexSessionI)
411
- if adapter is None:
412
- adapter = VortexSession(session)
413
- session.setComponent(VortexSessionI, adapter)
414
- return adapter
415
-
416
-
417
- registerAdapter(_tcpSessionVortexSessionIAdapter, VortexTcpSession, VortexSessionI)
@@ -14,7 +14,6 @@ from vortex.VortexServer import (
14
14
  VortexSessionI,
15
15
  )
16
16
  from vortex.VortexServerWebsocket import VortexWebsocketServerProtocol
17
- from vortex.VortexSession import VortexTcpSession
18
17
 
19
18
 
20
19
  class _MockServerConnection:
@@ -218,43 +217,6 @@ class VortexSessionAdapterTimeoutTest(SynchronousTestCase):
218
217
  self.assertEqual(session.sessionTimeout, HEART_BEAT_TIMEOUT)
219
218
 
220
219
 
221
- class VortexTcpSessionAdapterTest(SynchronousTestCase):
222
- def testTcpStubAdaptsToVortexSessionI(self) -> None:
223
- stub = VortexTcpSession(ip="1.2.3.4", vortexName="remote")
224
-
225
- adapter = VortexSessionI(stub)
226
-
227
- self.assertEqual(adapter.connections, {})
228
-
229
- def testTcpStubAdaptationIsCached(self) -> None:
230
- stub = VortexTcpSession(ip="1.2.3.4", vortexName="remote")
231
-
232
- first = VortexSessionI(stub)
233
- first.connections["uuid-a"] = object()
234
-
235
- self.assertIs(VortexSessionI(stub), first)
236
- self.assertIn("uuid-a", VortexSessionI(stub).connections)
237
-
238
- def testTcpStubTimeoutRaisedToHeartbeatFloor(self) -> None:
239
- stub = VortexTcpSession(ip="1.2.3.4", vortexName="remote")
240
- stub.sessionTimeout = 5
241
-
242
- VortexSessionI(stub)
243
-
244
- self.assertEqual(stub.sessionTimeout, HEART_BEAT_TIMEOUT)
245
-
246
- def testConnectionOpenedAndClosedWithTcpStub(self) -> None:
247
- vortexServer = VortexServer("test-server")
248
- stub = VortexTcpSession(ip="1.2.3.4", vortexName="remote")
249
- conn = _MockServerConnection(vortexServer, "uuid-a", "remote", stub)
250
-
251
- vortexServer.connectionOpened(stub, conn)
252
- self.assertIn("uuid-a", VortexSessionI(stub).connections)
253
-
254
- vortexServer.connectionClosed(conn)
255
- self.assertNotIn("uuid-a", VortexSessionI(stub).connections)
256
-
257
-
258
220
  class VortexWebsocketServerProtocolConnectionLostTest(SynchronousTestCase):
259
221
  def testConnectionLostTwiceDoesNotRaise(self) -> None:
260
222
  vortex_server = VortexServer("test-server")
@@ -65,17 +65,12 @@ class VortexTcpSession:
65
65
 
66
66
  TCP connections have no HTTP session. This stub exposes the subset of
67
67
  the Twisted ``Session`` API that vortex code uses (``uid``, ``touch``,
68
- ``notifyOnExpire``, ``expire``, ``getComponent``, ``setComponent``,
69
- ``sessionTimeout``) so the rest of the codebase can treat all sessions
70
- uniformly.
68
+ ``notifyOnExpire``, ``expire``, ``getComponent``, ``setComponent``) so
69
+ the rest of the codebase can treat all sessions uniformly.
71
70
 
72
71
  A :class:`VortexSession` is attached immediately on construction.
73
72
  """
74
73
 
75
- # Mirrors twisted.web.server.Session.sessionTimeout; the
76
- # VortexSessionI adapter factory in VortexServer reads and raises it.
77
- sessionTimeout = 900
78
-
79
74
  def __init__(self, ip: str, vortexName: str):
80
75
  self.uid: bytes = uuid4().bytes
81
76
  self._expireCallbacks = []
@@ -6,4 +6,4 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
- __version__ = '6.2.6'
9
+ __version__ = '6.3.1'
@@ -501,28 +501,25 @@ class _FailingProvider(TupleDataProviderABC):
501
501
 
502
502
  @pytest.mark.twisted
503
503
  @ensureDeferred
504
- async def test_provider_failure_sends_error_response() -> None:
504
+ async def test_provider_failure_sends_error_response(
505
+ monkeypatch: pytest.MonkeyPatch,
506
+ ) -> None:
507
+ _patchFactoryOnline(monkeypatch)
505
508
  handler = TupleDataObservableHandler("testObservable")
506
- handler.addTupleProvider(_TUPLE_NAME, _FailingProvider())
507
- sendResponseCalls: list[bytes] = []
508
-
509
- def sendResponse(vortexMsg: bytes) -> Deferred[None]:
510
- sendResponseCalls.append(vortexMsg)
511
- return succeed(None)
512
-
513
- with patch(
514
- "vortex.handler.TupleDataObservableHandler.VortexFactory.isVortexUuidOnline",
515
- return_value=True,
516
- ):
517
- await handler._process(
518
- _pollEnvelope(), _VORTEX_UUID, "client-vortex", sendResponse
519
- )
509
+ handler.addTupleProvider(TUPLE_NAME, _FailingProvider())
510
+ sent: list[VortexMsgList | bytes] = []
511
+ tupleSelector = TupleSelector(name=TUPLE_NAME)
520
512
 
521
- assert len(sendResponseCalls) == 1, (
522
- "provider failure must send an error response, not silence"
513
+ await handler._process(
514
+ _pollEnvelope(tupleSelector),
515
+ REMOTE_UUID,
516
+ "remote-vortex",
517
+ _sendResponse(sent),
523
518
  )
524
519
 
525
- errorEnvelope = await PayloadEnvelope().fromVortexMsgDefer(sendResponseCalls[0])
520
+ assert len(sent) == 1, "provider failure must send an error response, not silence"
521
+
522
+ errorEnvelope = await PayloadEnvelope().fromVortexMsgDefer(sent[0])
526
523
  assert errorEnvelope.result not in (None, True), (
527
524
  "error response result must fail the client resultCheck"
528
525
  )
@@ -530,5 +527,5 @@ async def test_provider_failure_sends_error_response() -> None:
530
527
  "error response must carry the provider error text"
531
528
  )
532
529
  assert (
533
- errorEnvelope.filt["tupleSelector"].toJsonStr() == _TUPLE_SELECTOR.toJsonStr()
530
+ errorEnvelope.filt["tupleSelector"].toJsonStr() == tupleSelector.toJsonStr()
534
531
  ), "error response must preserve the request filt"
@@ -430,9 +430,8 @@ class TupleDataObservableProxyHandler(TupleDataObservableCache):
430
430
 
431
431
  # Send the vortex messages
432
432
  for vortexUuid in observingUuids:
433
- # This method runs on a thread-pool thread; callLater is
434
- # reactor-thread-only, so schedule with thread-safe callFromThread.
435
- reactor.callFromThread(
433
+ reactor.callLater(
434
+ 0,
436
435
  self._sendVortexMsgInReactor,
437
436
  vortexMsg=vortexMsg,
438
437
  destVortexUuid=vortexUuid,
@@ -108,6 +108,11 @@ class CachedSubscribedData:
108
108
  def tuples(self, tuples: list[Tuple]) -> None:
109
109
  self.touch()
110
110
  self._tuples = tuples
111
+ if self.subject.is_disposed:
112
+ # RxJS parity: emitting after disposal is a silent no-op — a
113
+ # late redelivery after teardown must not crash the payload
114
+ # path with DisposedException.
115
+ return
111
116
  self.subject.on_next(tuples)
112
117
 
113
118
  def touch(self) -> None:
@@ -123,6 +128,8 @@ class CachedSubscribedData:
123
128
  self.lastServerAskDate = None
124
129
  self.lastServerPayloadDate = None
125
130
  self._tuples = None
131
+ if self.subject.is_disposed:
132
+ return
126
133
  self.subject.on_next(None)
127
134
 
128
135
 
@@ -286,6 +293,20 @@ class TupleDataOfflineObserverService:
286
293
  cachedData.flush()
287
294
  self._cleanupDeadCaches()
288
295
 
296
+ def unsubscribeAndEvictCache(self, tupleSelector: TupleSelector) -> None:
297
+ """Evict one selector's cached data and unsubscribe it from the server.
298
+
299
+ The selector's cached data is dropped from the in-memory cache
300
+ entirely — unlike :meth:`flushCache`, which keeps the cache entry
301
+ for later reuse — and an unsubscribe is sent to the server, queued
302
+ while the vortex is offline and flushed on reconnect.
303
+
304
+ :param tupleSelector: The tuple selector to evict and unsubscribe.
305
+ """
306
+ tsStr = tupleSelector.toJsonStr()
307
+ self._cacheByTupleSelector.pop(tsStr, None)
308
+ self._utilSendUnsubscribeToServer([tupleSelector])
309
+
289
310
  def promiseFromTupleSelector(
290
311
  self,
291
312
  tupleSelector: TupleSelector,