vortexpy 6.2.0__tar.gz → 6.2.2__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 (162) hide show
  1. {vortexpy-6.2.0 → vortexpy-6.2.2}/PKG-INFO +1 -1
  2. {vortexpy-6.2.0 → vortexpy-6.2.2}/pyproject.toml +1 -1
  3. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexFactory.py +5 -2
  4. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexFactory_test.py +72 -0
  5. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/__init__.py +1 -1
  6. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataOfflineObserverService.py +9 -39
  7. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataOfflineObserverService_test.py +1 -1
  8. vortexpy-6.2.2/vortex/storage/TupleOfflineStorageSecureService.py +201 -0
  9. vortexpy-6.2.2/vortex/storage/TupleOfflineStorageSecureService_test.py +252 -0
  10. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/TupleStorageServiceABC.py +5 -3
  11. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortexpy.egg-info/PKG-INFO +1 -1
  12. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortexpy.egg-info/SOURCES.txt +2 -2
  13. vortexpy-6.2.0/vortex/handler/TupleDataSecureOfflineObserverService.py +0 -146
  14. vortexpy-6.2.0/vortex/handler/TupleDataSecureOfflineObserverService_test.py +0 -314
  15. {vortexpy-6.2.0 → vortexpy-6.2.2}/LICENSE +0 -0
  16. {vortexpy-6.2.0 → vortexpy-6.2.2}/README.md +0 -0
  17. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/TwistedDeferredTimeout.py +0 -0
  18. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/__init__.py +0 -0
  19. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  20. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  21. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  22. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  23. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/run_websocket_rpc_test_client.py +0 -0
  24. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/run_websocket_rpc_test_server.py +0 -0
  25. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/setup_example_logging.py +0 -0
  26. {vortexpy-6.2.0 → vortexpy-6.2.2}/example/store_and_load_json.py +0 -0
  27. {vortexpy-6.2.0 → vortexpy-6.2.2}/setup.cfg +0 -0
  28. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/DataWrapTuple.py +0 -0
  29. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/DeferUtil.py +0 -0
  30. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/DeferUtil_test.py +0 -0
  31. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/Jsonable.py +0 -0
  32. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  33. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  34. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/MtlsServerSslOptions.py +0 -0
  35. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/Payload.py +0 -0
  36. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadEndpoint.py +0 -0
  37. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadEndpointTest.py +0 -0
  38. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadEnvelope.py +0 -0
  39. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadFilterKeys.py +0 -0
  40. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadIO.py +0 -0
  41. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadPriority.py +0 -0
  42. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadResponse.py +0 -0
  43. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadResponseTest.py +0 -0
  44. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadResponse_test.py +0 -0
  45. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PayloadTest.py +0 -0
  46. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/PerformTestActionTuple.py +0 -0
  47. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/SerialiseUtil.py +0 -0
  48. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/SerialiseUtil_test.py +0 -0
  49. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TestTuple.py +0 -0
  50. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/Tuple.py +0 -0
  51. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleAction.py +0 -0
  52. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleBson.py +0 -0
  53. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleBson_test.py +0 -0
  54. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleDataForTest.py +0 -0
  55. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleDocumentConverter.py +0 -0
  56. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TuplePerfTest.py +0 -0
  57. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  58. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleRestful.py +0 -0
  59. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleRestful_test.py +0 -0
  60. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleSelector.py +0 -0
  61. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/TupleTest.py +0 -0
  62. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexABC.py +0 -0
  63. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexAccessControlContextABC.py +0 -0
  64. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexAuthenticatorABC.py +0 -0
  65. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexClientHttp.py +0 -0
  66. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexClientHttpTest.py +0 -0
  67. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexClientTcp.py +0 -0
  68. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexClientWebsocketFactory.py +0 -0
  69. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexConnectionABC.py +0 -0
  70. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexEndpointPermission.py +0 -0
  71. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexPayloadProtocol.py +0 -0
  72. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexPermission.py +0 -0
  73. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexPermissionError.py +0 -0
  74. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServer.py +0 -0
  75. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServerConnection.py +0 -0
  76. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServerConnection_test.py +0 -0
  77. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServerHttpResource.py +0 -0
  78. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServerTcp.py +0 -0
  79. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServerTcp_test.py +0 -0
  80. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServerWebsocket.py +0 -0
  81. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServerWebsocket_test.py +0 -0
  82. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexServer_test.py +0 -0
  83. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexSession.py +0 -0
  84. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexSession_test.py +0 -0
  85. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  86. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  87. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexTest.py +0 -0
  88. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexUserDetailsABC.py +0 -0
  89. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexUtil.py +0 -0
  90. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexWritePushProducer.py +0 -0
  91. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/VortexWritePushProducer_test.py +0 -0
  92. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  93. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/data_loader/TupleDataLiveLockManager_test.py +0 -0
  94. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/data_loader/TupleDataLoader.py +0 -0
  95. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  96. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  97. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  98. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/data_loader/__init__.py +0 -0
  99. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  100. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  101. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  102. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/event_bus/__init__.py +0 -0
  103. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleActionProcessor.py +0 -0
  104. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  105. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  106. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  107. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleActionProcessor_test.py +0 -0
  108. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleActionPushOffline.py +0 -0
  109. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  110. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  111. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataActionClient.py +0 -0
  112. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataActionClient_test.py +0 -0
  113. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObservableCache.py +0 -0
  114. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  115. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObservableHandler.py +0 -0
  116. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObservableHandler_test.py +0 -0
  117. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
  118. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  119. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  120. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataObserverClient.py +0 -0
  121. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/TupleDataProviderABC.py +0 -0
  122. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  123. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/handler/__init__.py +0 -0
  124. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/mypy_plugin.py +0 -0
  125. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  126. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/py.typed +0 -0
  127. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/GzippedDataHttpClient.py +0 -0
  128. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  129. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  130. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/RestfulHttpClient.py +0 -0
  131. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/RestfulResource.py +0 -0
  132. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/__init__.py +0 -0
  133. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/restful_http_client_test.py +0 -0
  134. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/restful/restful_resource_test.py +0 -0
  135. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/rpc/RPC.py +0 -0
  136. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/rpc/RPCTest.py +0 -0
  137. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/rpc/RPC_test.py +0 -0
  138. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/rpc/__init__.py +0 -0
  139. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  140. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  141. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  142. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/sqla_orm/__init__.py +0 -0
  143. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  144. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/TupleOfflineStorageNameService.py +0 -0
  145. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/TupleOfflineStorageService.py +0 -0
  146. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/TupleStorageSqlite.py +0 -0
  147. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/TupleStorageSqliteService.py +0 -0
  148. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/TupleStorageSqliteService_test.py +0 -0
  149. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/storage/__init__.py +0 -0
  150. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/tuple_json_schema/__init__.py +0 -0
  151. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/tuple_json_schema/constants.py +0 -0
  152. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  153. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  154. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  155. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  156. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/tuple_test_fixtures.py +0 -0
  157. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/twisted_runtime_metrics.py +0 -0
  158. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortex/twisted_runtime_metrics_test.py +0 -0
  159. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortexpy.egg-info/dependency_links.txt +0 -0
  160. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortexpy.egg-info/entry_points.txt +0 -0
  161. {vortexpy-6.2.0 → vortexpy-6.2.2}/vortexpy.egg-info/requires.txt +0 -0
  162. {vortexpy-6.2.0 → vortexpy-6.2.2}/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.0
3
+ Version: 6.2.2
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.0"
7
+ version = "6.2.2"
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."
@@ -995,7 +995,10 @@ class VortexFactory:
995
995
  "Vortex %s went %s", vortexName, ("online" if online else "offline")
996
996
  )
997
997
 
998
+ # Rebuild the connection structs before notifying: handlers react to
999
+ # the status change synchronously (e.g. the offline observer re-tell)
1000
+ # and read getRemoteVortexName() from these structs.
1001
+ cls.connectionChanged()
1002
+
998
1003
  if vortexName in cls.__vortexStatusChangeSubjectsByName:
999
1004
  cls.__vortexStatusChangeSubjectsByName[vortexName].on_next(online)
1000
-
1001
- cls.connectionChanged()
@@ -112,3 +112,75 @@ def test_closeExistingVortexClientsForName_closesRegisteredClients() -> None:
112
112
  assert (
113
113
  VortexFactory.getLocalVortexClients("codie-mcp-local") == []
114
114
  )
115
+
116
+
117
+ def _resetVortexFactoryClassState() -> None:
118
+ VortexFactory._VortexFactory__vortexInfoState = _VortexFactoryVortexInfo()
119
+ VortexFactory._VortexFactory__vortexStatusChangeSubjectsByName = {}
120
+ VortexFactory._VortexFactory__isShutdown = False
121
+
122
+
123
+ def test_notifyOfVortexStatusChange_rebuildsStructsBeforeNotify() -> None:
124
+ _resetVortexFactoryClassState()
125
+ try:
126
+ client = _StubVortexClient("codie-mcp-local", "local-uuid-1")
127
+ VortexFactory._VortexFactory__vortexInfoState.addVortexClient(
128
+ "codie-mcp-local", client
129
+ )
130
+
131
+ # Mirror the handshake: _setNameAndUuid sets the remote info on the
132
+ # client before _notifyOfVortexStatusChange runs.
133
+ client._remote_name = "codie-hub"
134
+ client._remote_uuid = "hub-uuid-1"
135
+
136
+ notifications = []
137
+ VortexFactory.subscribeToVortexStatusChange("codie-hub").subscribe(
138
+ lambda online: notifications.append(
139
+ (online, VortexFactory.getRemoteVortexName())
140
+ )
141
+ )
142
+
143
+ VortexFactory._notifyOfVortexStatusChange("codie-hub", online=True)
144
+
145
+ assert len(notifications) == 1
146
+ online, remoteNames = notifications[0]
147
+ assert online is True
148
+ assert "codie-hub" in remoteNames
149
+ finally:
150
+ _resetVortexFactoryClassState()
151
+
152
+
153
+ def test_notifyOfVortexStatusChange_offlineAfterClearDropsName() -> None:
154
+ _resetVortexFactoryClassState()
155
+ try:
156
+ client = _StubVortexClient(
157
+ "codie-mcp-local",
158
+ "local-uuid-1",
159
+ remote_name="codie-hub",
160
+ remote_uuid="hub-uuid-1",
161
+ )
162
+ VortexFactory._VortexFactory__vortexInfoState.addVortexClient(
163
+ "codie-mcp-local", client
164
+ )
165
+ assert "codie-hub" in VortexFactory.getRemoteVortexName()
166
+
167
+ # Mirror connectionLost: _clearRemoteVortexInfo clears the remote
168
+ # info on the client before _notifyOfVortexStatusChange runs.
169
+ client._remote_name = None
170
+ client._remote_uuid = None
171
+
172
+ notifications = []
173
+ VortexFactory.subscribeToVortexStatusChange("codie-hub").subscribe(
174
+ lambda online: notifications.append(
175
+ (online, VortexFactory.getRemoteVortexName())
176
+ )
177
+ )
178
+
179
+ VortexFactory._notifyOfVortexStatusChange("codie-hub", online=False)
180
+
181
+ assert len(notifications) == 1
182
+ online, remoteNames = notifications[0]
183
+ assert online is False
184
+ assert "codie-hub" not in remoteNames
185
+ finally:
186
+ _resetVortexFactoryClassState()
@@ -6,4 +6,4 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
- __version__ = '6.2.0'
9
+ __version__ = '6.2.2'
@@ -432,7 +432,10 @@ class TupleDataOfflineObserverService:
432
432
  self, cachedData: CachedSubscribedData, tupleSelector: TupleSelector
433
433
  ) -> Generator[Any, Any, None]:
434
434
  """Load the stored payload, decode it, and notify (TS :373-409)."""
435
- vortexMsg = yield self._storageLoadTuplesEncoded(tupleSelector)
435
+ assert self._offlineStorage is not None, (
436
+ "offlineStorage must be set before loading tuples from storage"
437
+ )
438
+ vortexMsg = yield self._offlineStorage.loadTuplesEncoded(tupleSelector)
436
439
 
437
440
  # There is no data
438
441
  if vortexMsg is None:
@@ -444,9 +447,9 @@ class TupleDataOfflineObserverService:
444
447
  return
445
448
 
446
449
  assert isinstance(vortexMsg, str), (
447
- f"Expected a plaintext str payload from storage, got"
448
- f" {type(vortexMsg).__name__} — ciphered storage must be read via"
449
- f" TupleDataSecureOfflineObserverService"
450
+ f"Expected a plaintext str payload from offline storage, got"
451
+ f" {type(vortexMsg).__name__} — ciphered storage must be wrapped"
452
+ f" in TupleOfflineStorageSecureService so reads return plaintext"
450
453
  )
451
454
  payload = yield Payload().fromEncodedPayloadDefer(vortexMsg)
452
455
 
@@ -460,39 +463,6 @@ class TupleDataOfflineObserverService:
460
463
  cachedData.tuples = payload.tuples
461
464
  self._notifyObservers(cachedData, tupleSelector, payload.tuples)
462
465
 
463
- def _storageLoadTuplesEncoded(self, tupleSelector: TupleSelector) -> Deferred:
464
- """Storage load hook.
465
-
466
- ``TupleDataSecureOfflineObserverService`` overrides this to run the
467
- payload through the cipher delegate after reading from storage.
468
- """
469
- assert self._offlineStorage is not None, (
470
- "offlineStorage must be set before loading tuples from storage"
471
- )
472
- return self._offlineStorage.loadTuplesEncoded(tupleSelector)
473
-
474
- def _storageSaveTuplesEncoded(
475
- self, tupleSelector: TupleSelector, vortexMsg: str
476
- ) -> Deferred:
477
- """Storage save hook.
478
-
479
- ``TupleDataSecureOfflineObserverService`` overrides this to run the
480
- payload through the cipher delegate before writing to storage.
481
- """
482
- assert self._offlineStorage is not None, (
483
- "offlineStorage must be set before saving encoded tuples to storage"
484
- )
485
- return self._offlineStorage.saveTuplesEncoded(tupleSelector, vortexMsg)
486
-
487
- def _storageSaveTuples(
488
- self, tupleSelector: TupleSelector, tuples: list[Tuple]
489
- ) -> Deferred:
490
- """Storage save hook for unencoded tuples."""
491
- assert self._offlineStorage is not None, (
492
- "offlineStorage must be set before saving tuples to storage"
493
- )
494
- return self._offlineStorage.saveTuples(tupleSelector, tuples)
495
-
496
466
  def _cleanupDeadCaches(self) -> None:
497
467
  for tsStr in list(self._cacheByTupleSelector.keys()):
498
468
  cachedData = self._cacheByTupleSelector[tsStr]
@@ -738,6 +708,6 @@ class TupleDataOfflineObserverService:
738
708
  return succeed(None)
739
709
 
740
710
  if encodedPayload is None:
741
- return self._storageSaveTuples(tupleSelector, tuples)
711
+ return self._offlineStorage.saveTuples(tupleSelector, tuples)
742
712
 
743
- return self._storageSaveTuplesEncoded(tupleSelector, encodedPayload)
713
+ return self._offlineStorage.saveTuplesEncoded(tupleSelector, encodedPayload)
@@ -250,7 +250,7 @@ async def test_stored_data_loses_to_live_payload(tmp_path, monkeypatch) -> None:
250
250
  loadChains.append(deferred)
251
251
  return deferred
252
252
 
253
- monkeypatch.setattr(service, "_storageLoadTuplesEncoded", gatedLoad)
253
+ monkeypatch.setattr(storage, "loadTuplesEncoded", gatedLoad)
254
254
  monkeypatch.setattr(
255
255
  service, "_loadStoredTuplesDefer", capturingLoadStoredTuplesDefer
256
256
  )
@@ -0,0 +1,201 @@
1
+ # ruff: noqa: N999
2
+ # Module names use PascalCase for vortexjs member-for-member parity.
3
+ # Copyright (c) 2016-2026 Flying Ion Pty Ltd
4
+ #
5
+ # This software is open source; the MIT license applies.
6
+ # See LICENSE for the full license text.
7
+ #
8
+ # Website : https://flyingion.com
9
+ # Support : contact@flyingion.com
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import time
15
+ from abc import ABCMeta, abstractmethod
16
+ from collections.abc import Generator
17
+ from typing import Any
18
+
19
+ from twisted.internet.defer import Deferred, inlineCallbacks
20
+
21
+ from vortex.Payload import Payload
22
+ from vortex.storage.TupleOfflineStorageService import TupleOfflineStorageService
23
+ from vortex.storage.TupleStorageServiceABC import TupleStorageBatchSaveArguments
24
+ from vortex.storage.TupleStorageSqliteService import TupleStorageSqliteService
25
+ from vortex.Tuple import Tuple
26
+ from vortex.TupleSelector import TupleSelector
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class OfflineStorageCipherABC(metaclass=ABCMeta):
32
+ """Offline Storage Cipher ABC
33
+
34
+ Cipher delegate for executor-local storage encryption. The concrete cipher
35
+ is supplied by the consumer — every agentic holds the only key to its own
36
+ cached data and there is no shared or hub-side key to protect.
37
+ """
38
+
39
+ @abstractmethod
40
+ def encrypt(self, plaintext: bytes) -> bytes:
41
+ """Encrypt plaintext bytes.
42
+
43
+ :param plaintext: The plaintext to encrypt.
44
+ :return: The ciphertext bytes.
45
+ """
46
+
47
+ @abstractmethod
48
+ def decrypt(self, ciphertext: bytes) -> bytes:
49
+ """Decrypt ciphertext bytes.
50
+
51
+ :param ciphertext: The ciphertext to decrypt.
52
+ :return: The plaintext bytes.
53
+ :raises Exception: when the ciphertext is not readable by this key
54
+ (cert rotated, wrong key, corrupt row).
55
+ """
56
+
57
+
58
+ class TupleOfflineStorageSecureService(TupleOfflineStorageService):
59
+ """Tuple Offline Storage Secure Service
60
+
61
+ A ``TupleOfflineStorageService`` facade implementation that runs payloads
62
+ through the cipher delegate before writing to and after reading from the
63
+ inner storage backend. This is executor-local storage encryption — no
64
+ hub-side transport encryption; the payload format on the transport is
65
+ untouched (mTLS already secures transport), only the bytes landing in
66
+ SQLite are ciphertext.
67
+
68
+ Decrypt failure on load (cert rotated, wrong key, corrupt row) is a cache
69
+ miss: the row is deleted and ``None`` is returned — no plaintext fallback,
70
+ no partial decrypt.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ storage: TupleStorageSqliteService,
76
+ cipher: OfflineStorageCipherABC,
77
+ ) -> None:
78
+ """Constructor
79
+
80
+ :param storage: The constructed inner storage backend; ciphertext
81
+ rows are written to and read from it.
82
+ :param cipher: The cipher delegate for storage encryption.
83
+ """
84
+ TupleOfflineStorageService.__init__(self, storage)
85
+ self._cipher = cipher
86
+
87
+ @inlineCallbacks
88
+ def loadTuples(
89
+ self, tupleSelector: TupleSelector
90
+ ) -> Generator[Any, Any, list[Tuple]]:
91
+ """Load and decode the tuples stored for a selector.
92
+
93
+ Mirrors ``TupleStorageServiceABC.loadTuples`` minus the plaintext-str
94
+ assert — this facade's own :meth:`loadTuplesEncoded` already owns the
95
+ decrypt, so the decoded payload is always plaintext ``str`` here.
96
+
97
+ :param tupleSelector: The selector to load tuples for.
98
+ :return: A ``Deferred`` firing the stored tuples, or an empty list on
99
+ a lookup miss.
100
+ """
101
+ startTime = time.monotonic()
102
+
103
+ vortexMsg = yield self.loadTuplesEncoded(tupleSelector)
104
+ if vortexMsg is None:
105
+ return []
106
+
107
+ payload = yield Payload().fromEncodedPayloadDefer(vortexMsg)
108
+
109
+ logger.debug(
110
+ "Secure storage: loadTuples took %sms",
111
+ round((time.monotonic() - startTime) * 1000),
112
+ )
113
+ return payload.tuples
114
+
115
+ @inlineCallbacks
116
+ def loadTuplesEncoded(
117
+ self, tupleSelector: TupleSelector
118
+ ) -> Generator[Any, Any, str | None]:
119
+ """Load, decrypt, and return the stored payload for a selector.
120
+
121
+ A decrypt failure (cert rotated, wrong key, corrupt row) is a cache
122
+ miss: the row is deleted from the inner storage and ``None`` is
123
+ returned.
124
+
125
+ :param tupleSelector: The selector to load the payload for.
126
+ :return: A ``Deferred`` firing the decrypted encoded payload, or
127
+ ``None`` on a lookup miss.
128
+ """
129
+ stored = yield self._storage.loadTuplesEncoded(tupleSelector)
130
+ if stored is None:
131
+ return None
132
+
133
+ ciphertext = stored.encode("utf-8") if isinstance(stored, str) else stored
134
+ try:
135
+ plaintextBytes = self._cipher.decrypt(ciphertext)
136
+ except Exception as exception: # noqa: BLE001 — OfflineStorageCipherABC.decrypt raises Exception by contract
137
+ logger.warning(
138
+ "Deleting undecryptable offline cache row for %s: %s",
139
+ tupleSelector.toJsonStr(),
140
+ exception,
141
+ )
142
+ yield self._storage.deleteTuples(tupleSelector)
143
+ return None
144
+
145
+ return plaintextBytes.decode("utf-8")
146
+
147
+ @inlineCallbacks
148
+ def saveTuples(
149
+ self, tupleSelector: TupleSelector, tuples: list[Tuple]
150
+ ) -> Generator[Any, Any, None]:
151
+ """Encode, encrypt, and store tuples for a selector.
152
+
153
+ :param tupleSelector: The selector to store tuples for.
154
+ :param tuples: The tuples to store.
155
+ :return: A ``Deferred`` that fires when the tuples are stored.
156
+ """
157
+ startTime = time.monotonic()
158
+
159
+ vortexMsg = yield Payload(tuples=tuples).toEncodedPayloadDefer()
160
+ yield self.saveTuplesEncoded(tupleSelector, vortexMsg)
161
+
162
+ logger.debug(
163
+ "Secure storage: saveTuples took %sms",
164
+ round((time.monotonic() - startTime) * 1000),
165
+ )
166
+
167
+ def saveTuplesEncoded(
168
+ self, tupleSelector: TupleSelector, vortexMsg: str | bytes
169
+ ) -> Deferred:
170
+ """Encrypt and store an encoded payload for a selector.
171
+
172
+ :param tupleSelector: The selector to store the payload for.
173
+ :param vortexMsg: The plaintext encoded payload.
174
+ :return: A ``Deferred`` that fires when the payload is stored.
175
+ """
176
+ plaintext = (
177
+ vortexMsg.encode("utf-8") if isinstance(vortexMsg, str) else vortexMsg
178
+ )
179
+ ciphertext = self._cipher.encrypt(plaintext)
180
+ return self._storage.saveTuplesEncoded(tupleSelector, ciphertext)
181
+
182
+ def batchSaveTuplesEncoded(
183
+ self, data: list[TupleStorageBatchSaveArguments]
184
+ ) -> Deferred:
185
+ """Encrypt and store multiple encoded payloads in one batch.
186
+
187
+ :param data: The selectors and plaintext encoded payloads to store.
188
+ :return: A ``Deferred`` that fires when the batch is stored.
189
+ """
190
+ encryptedData = [
191
+ TupleStorageBatchSaveArguments(
192
+ tupleSelector=saveArguments.tupleSelector,
193
+ vortexMsg=self._cipher.encrypt(
194
+ saveArguments.vortexMsg.encode("utf-8")
195
+ if isinstance(saveArguments.vortexMsg, str)
196
+ else saveArguments.vortexMsg
197
+ ),
198
+ )
199
+ for saveArguments in data
200
+ ]
201
+ return self._storage.batchSaveTuplesEncoded(encryptedData)
@@ -0,0 +1,252 @@
1
+ # ruff: noqa: N999 C408
2
+ # C408: dict() with identifier keys per coding_standards/py_dict_constructor.
3
+ # Copyright (c) 2016-2026 Flying Ion Pty Ltd
4
+ #
5
+ # This software is open source; the MIT license applies.
6
+ # See LICENSE for the full license text.
7
+ #
8
+ # Website : https://flyingion.com
9
+ # Support : contact@flyingion.com
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+ from pytest_twisted import ensureDeferred
17
+
18
+ from vortex.Payload import Payload
19
+ from vortex.storage.TupleOfflineStorageNameService import (
20
+ TupleOfflineStorageNameService,
21
+ )
22
+ from vortex.storage.TupleOfflineStorageSecureService import (
23
+ OfflineStorageCipherABC,
24
+ TupleOfflineStorageSecureService,
25
+ )
26
+ from vortex.storage.TupleStorageServiceABC import TupleStorageBatchSaveArguments
27
+ from vortex.storage.TupleStorageSqliteService import TupleStorageSqliteService
28
+ from vortex.Tuple import Tuple, TupleField, addTupleType
29
+ from vortex.TupleSelector import TupleSelector
30
+
31
+
32
+ @addTupleType
33
+ class OfflineSecureStorageTestTuple(Tuple):
34
+ __tupleType__ = "vortex.test.OfflineSecureStorageTestTuple"
35
+ anInt: int = TupleField()
36
+ aString: str = TupleField()
37
+
38
+
39
+ class _XorCipher(OfflineStorageCipherABC):
40
+ """Test cipher: a key byte prefix followed by an XOR'd body.
41
+
42
+ Decrypt raises when the prefix does not match this cipher's key, standing
43
+ in for an authenticated cipher rejecting a rotated or wrong key.
44
+ """
45
+
46
+ def __init__(self, key: int) -> None:
47
+ self._key = key
48
+
49
+ def encrypt(self, plaintext: bytes) -> bytes:
50
+ return bytes([self._key]) + bytes(byte ^ self._key for byte in plaintext)
51
+
52
+ def decrypt(self, ciphertext: bytes) -> bytes:
53
+ if not ciphertext or ciphertext[0] != self._key:
54
+ raise ValueError("ciphertext is not readable by this key")
55
+ return bytes(byte ^ self._key for byte in ciphertext[1:])
56
+
57
+
58
+ def _makeSelector(key: str = "a") -> TupleSelector:
59
+ return TupleSelector(OfflineSecureStorageTestTuple, dict(key=key))
60
+
61
+
62
+ def _makeTuples(*strings: str) -> list[OfflineSecureStorageTestTuple]:
63
+ return [
64
+ OfflineSecureStorageTestTuple(anInt=index, aString=value)
65
+ for index, value in enumerate(strings)
66
+ ]
67
+
68
+
69
+ async def _makeInnerStorage(tmp_path: Path) -> TupleStorageSqliteService:
70
+ storage = TupleStorageSqliteService(
71
+ TupleOfflineStorageNameService("hub-credential"),
72
+ tmp_path / "vortex-offline",
73
+ )
74
+ await storage.open()
75
+ return storage
76
+
77
+
78
+ @pytest.mark.twisted
79
+ @ensureDeferred
80
+ async def test_stored_bytes_are_ciphertext(tmp_path: Path) -> None:
81
+ innerStorage = await _makeInnerStorage(tmp_path)
82
+ cipher = _XorCipher(0x5A)
83
+ secureStorage = TupleOfflineStorageSecureService(innerStorage, cipher)
84
+ tupleSelector = _makeSelector()
85
+
86
+ await secureStorage.saveTuples(tupleSelector, _makeTuples("secret-payload"))
87
+
88
+ rawRow = await innerStorage.loadTuplesEncoded(tupleSelector)
89
+ assert isinstance(rawRow, bytes), (
90
+ "stored payload must be ciphertext bytes, not plaintext str"
91
+ )
92
+
93
+ assert b"secret-payload" not in rawRow, (
94
+ "the stored row must not contain the plaintext payload"
95
+ )
96
+ assert b"OfflineSecureStorageTestTuple" not in rawRow, (
97
+ "the stored row must not contain the plaintext tuple type name"
98
+ )
99
+
100
+ # The row decrypts to a standard encoded payload that decodes to the
101
+ # stored tuples.
102
+ decryptedPayload = await Payload().fromEncodedPayloadDefer(
103
+ cipher.decrypt(rawRow).decode("utf-8")
104
+ )
105
+ assert [t.aString for t in decryptedPayload.tuples] == ["secret-payload"], (
106
+ "the decrypted row decodes back to the stored tuples"
107
+ )
108
+
109
+
110
+ @pytest.mark.twisted
111
+ @ensureDeferred
112
+ async def test_save_tuples_encoded_and_batch_are_ciphertext(
113
+ tmp_path: Path,
114
+ ) -> None:
115
+ innerStorage = await _makeInnerStorage(tmp_path)
116
+ cipher = _XorCipher(0x5A)
117
+ secureStorage = TupleOfflineStorageSecureService(innerStorage, cipher)
118
+
119
+ expectedBySelector: list[tuple[TupleSelector, str]] = []
120
+
121
+ encodedSelector = _makeSelector("encoded")
122
+ encodedPayload = await Payload(
123
+ tuples=_makeTuples("encoded-secret")
124
+ ).toEncodedPayloadDefer()
125
+ await secureStorage.saveTuplesEncoded(encodedSelector, encodedPayload)
126
+ expectedBySelector.append((encodedSelector, encodedPayload))
127
+
128
+ batchData: list[TupleStorageBatchSaveArguments] = []
129
+ for key, value in (
130
+ ("batch-a", "batch-secret-a"),
131
+ ("batch-b", "batch-secret-b"),
132
+ ):
133
+ batchSelector = _makeSelector(key)
134
+ batchPayload = await Payload(tuples=_makeTuples(value)).toEncodedPayloadDefer()
135
+ batchData.append(
136
+ TupleStorageBatchSaveArguments(
137
+ tupleSelector=batchSelector,
138
+ vortexMsg=batchPayload,
139
+ )
140
+ )
141
+ expectedBySelector.append((batchSelector, batchPayload))
142
+ await secureStorage.batchSaveTuplesEncoded(batchData)
143
+
144
+ for tupleSelector, plaintext in expectedBySelector:
145
+ rawRow = await innerStorage.loadTuplesEncoded(tupleSelector)
146
+ assert isinstance(rawRow, bytes), (
147
+ "batch and encoded saves must store ciphertext bytes"
148
+ )
149
+ assert plaintext.encode("utf-8") not in rawRow, (
150
+ "the stored row must not contain the plaintext encoded payload"
151
+ )
152
+ assert cipher.decrypt(rawRow).decode("utf-8") == plaintext, (
153
+ "the stored row decrypts back to the original encoded payload"
154
+ )
155
+
156
+
157
+ @pytest.mark.twisted
158
+ @ensureDeferred
159
+ async def test_round_trip_via_secure_facade(tmp_path: Path) -> None:
160
+ innerStorage = await _makeInnerStorage(tmp_path)
161
+ tupleSelector = _makeSelector()
162
+
163
+ firstSecure = TupleOfflineStorageSecureService(innerStorage, _XorCipher(0x5A))
164
+ await firstSecure.saveTuples(tupleSelector, _makeTuples("persisted"))
165
+
166
+ # A new secure facade with the same key — the service-restart case —
167
+ # reads the cached data back.
168
+ secondSecure = TupleOfflineStorageSecureService(innerStorage, _XorCipher(0x5A))
169
+ tuples = await secondSecure.loadTuples(tupleSelector)
170
+ assert [t.aString for t in tuples] == ["persisted"], (
171
+ "a new facade with the same key reads the cached tuples back"
172
+ )
173
+
174
+ vortexMsg = await secondSecure.loadTuplesEncoded(tupleSelector)
175
+ assert isinstance(vortexMsg, str), (
176
+ "loadTuplesEncoded returns the decrypted plaintext payload str"
177
+ )
178
+ payload = await Payload().fromEncodedPayloadDefer(vortexMsg)
179
+ assert [t.aString for t in payload.tuples] == ["persisted"], (
180
+ "the decrypted encoded payload decodes to the stored tuples"
181
+ )
182
+
183
+
184
+ @pytest.mark.twisted
185
+ @ensureDeferred
186
+ async def test_decrypt_failure_deletes_row_and_returns_none(
187
+ tmp_path: Path,
188
+ ) -> None:
189
+ innerStorage = await _makeInnerStorage(tmp_path)
190
+ tupleSelector = _makeSelector()
191
+
192
+ await TupleOfflineStorageSecureService(innerStorage, _XorCipher(0x5A)).saveTuples(
193
+ tupleSelector, _makeTuples("old")
194
+ )
195
+ assert await innerStorage.loadTuplesEncoded(tupleSelector) is not None, (
196
+ "the seeded row exists before the wrong-key load"
197
+ )
198
+
199
+ # A facade holding the wrong key treats the row as a cache miss
200
+ wrongKeySecure = TupleOfflineStorageSecureService(innerStorage, _XorCipher(0x33))
201
+ assert await wrongKeySecure.loadTuplesEncoded(tupleSelector) is None, (
202
+ "a wrong-key load treats the row as a cache miss"
203
+ )
204
+
205
+ # The miss deleted the undecryptable row
206
+ assert await innerStorage.loadTuplesEncoded(tupleSelector) is None, (
207
+ "the miss deleted the undecryptable row"
208
+ )
209
+
210
+ # loadTuples on a decrypt failure is the same miss: row deleted, []
211
+ await TupleOfflineStorageSecureService(innerStorage, _XorCipher(0x5A)).saveTuples(
212
+ tupleSelector, _makeTuples("old")
213
+ )
214
+ assert await wrongKeySecure.loadTuples(tupleSelector) == [], (
215
+ "loadTuples on a decrypt failure returns an empty list"
216
+ )
217
+ assert await innerStorage.loadTuplesEncoded(tupleSelector) is None, (
218
+ "the loadTuples miss also deleted the undecryptable row"
219
+ )
220
+
221
+
222
+ @pytest.mark.twisted
223
+ @ensureDeferred
224
+ async def test_rotated_key_makes_old_cache_unreadable(
225
+ tmp_path: Path,
226
+ ) -> None:
227
+ innerStorage = await _makeInnerStorage(tmp_path)
228
+ tupleSelector = _makeSelector()
229
+
230
+ # Data cached before the key rotation
231
+ await TupleOfflineStorageSecureService(innerStorage, _XorCipher(0x5A)).saveTuples(
232
+ tupleSelector, _makeTuples("before-rotation")
233
+ )
234
+
235
+ # After the rotation the old cache is a miss, not silently served
236
+ afterRotation = TupleOfflineStorageSecureService(innerStorage, _XorCipher(0x33))
237
+ assert await afterRotation.loadTuplesEncoded(tupleSelector) is None, (
238
+ "the rotated key makes the pre-rotation cache a miss"
239
+ )
240
+ assert await innerStorage.loadTuplesEncoded(tupleSelector) is None, (
241
+ "the rotation miss deleted the pre-rotation row"
242
+ )
243
+
244
+ # Re-cached data is encrypted with the rotated key and reads back
245
+ await afterRotation.saveTuples(tupleSelector, _makeTuples("after-rotation"))
246
+ rawRow = await innerStorage.loadTuplesEncoded(tupleSelector)
247
+ assert isinstance(rawRow, bytes), "the re-cached row is stored as ciphertext bytes"
248
+ assert rawRow[0] == 0x33, "re-cached payload must be encrypted with the rotated key"
249
+ tuples = await afterRotation.loadTuples(tupleSelector)
250
+ assert [t.aString for t in tuples] == ["after-rotation"], (
251
+ "data cached after the rotation reads back with the rotated key"
252
+ )
@@ -33,10 +33,12 @@ class TupleStorageBatchSaveArguments(NamedTuple):
33
33
  """Batch save argument bundle.
34
34
 
35
35
  Mirror of the vortexjs ``TupleStorageBatchSaveArguments`` interface.
36
+ ``vortexMsg`` is ``str`` for plaintext (vortexjs parity), ``bytes``
37
+ when a ciphering consumer stores ciphertext.
36
38
  """
37
39
 
38
40
  tupleSelector: TupleSelector
39
- vortexMsg: str
41
+ vortexMsg: str | bytes
40
42
 
41
43
 
42
44
  class TupleStorageServiceABC(metaclass=ABCMeta):
@@ -97,8 +99,8 @@ class TupleStorageServiceABC(metaclass=ABCMeta):
97
99
 
98
100
  assert isinstance(vortexMsg, str), (
99
101
  f"loadTuples expects a plaintext str payload from storage, got"
100
- f" {type(vortexMsg).__name__} — ciphered storage must be read via"
101
- f" TupleDataSecureOfflineObserverService"
102
+ f" {type(vortexMsg).__name__} — ciphered storage must be wrapped"
103
+ f" in TupleOfflineStorageSecureService so reads return plaintext"
102
104
  )
103
105
  payload = yield Payload().fromEncodedPayloadDefer(vortexMsg)
104
106
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 6.2.0
3
+ Version: 6.2.2
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