vortexpy 4.3.6__tar.gz → 6.0.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 (150) hide show
  1. vortexpy-6.0.1/LICENSE +22 -0
  2. {vortexpy-4.3.6 → vortexpy-6.0.1}/PKG-INFO +10 -5
  3. {vortexpy-4.3.6 → vortexpy-6.0.1}/README.md +9 -4
  4. {vortexpy-4.3.6 → vortexpy-6.0.1}/pyproject.toml +1 -1
  5. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadEndpoint.py +26 -16
  6. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadEndpointTest.py +0 -1
  7. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadIO.py +9 -4
  8. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadPriority.py +1 -0
  9. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadResponse.py +0 -1
  10. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/Tuple.py +6 -0
  11. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexAccessControlContextABC.py +47 -13
  12. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexAuthenticatorABC.py +6 -1
  13. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexFactory.py +0 -1
  14. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexPayloadProtocol.py +2 -3
  15. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServer.py +20 -4
  16. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServerConnection.py +2 -2
  17. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServerWebsocket.py +4 -2
  18. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServer_test.py +15 -1
  19. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexSession.py +62 -9
  20. vortexpy-6.0.1/vortex/VortexUserDetailsABC.py +40 -0
  21. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexWritePushProducer_test.py +2 -2
  22. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/__init__.py +1 -1
  23. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/data_loader/TupleDataLiveLockManager.py +147 -65
  24. vortexpy-6.0.1/vortex/data_loader/TupleDataLiveLockManager_test.py +336 -0
  25. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/data_loader/TupleDataLoader.py +32 -41
  26. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/data_loader/TupleDataLoaderTuples.py +13 -9
  27. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleActionProcessor.py +51 -34
  28. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleActionProcessorProxy.py +7 -6
  29. vortexpy-6.0.1/vortex/handler/TupleActionProcessor_test.py +147 -0
  30. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataObservableHandler.py +69 -69
  31. vortexpy-6.0.1/vortex/handler/TupleDataObservableHandler_test.py +488 -0
  32. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataObservableProxyHandler.py +40 -36
  33. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataObserverClient.py +0 -1
  34. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/rpc/RPC.py +5 -3
  35. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/sqla_orm/OrmCrudHandler.py +30 -7
  36. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortexpy.egg-info/PKG-INFO +10 -5
  37. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortexpy.egg-info/SOURCES.txt +4 -0
  38. vortexpy-4.3.6/vortex/handler/TupleDataObservableHandler_test.py +0 -392
  39. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/TwistedDeferredTimeout.py +0 -0
  40. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/__init__.py +0 -0
  41. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  42. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  43. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  44. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  45. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/run_websocket_rpc_test_client.py +0 -0
  46. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/run_websocket_rpc_test_server.py +0 -0
  47. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/setup_example_logging.py +0 -0
  48. {vortexpy-4.3.6 → vortexpy-6.0.1}/example/store_and_load_json.py +0 -0
  49. {vortexpy-4.3.6 → vortexpy-6.0.1}/setup.cfg +0 -0
  50. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/DataWrapTuple.py +0 -0
  51. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/DeferUtil.py +0 -0
  52. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/DeferUtil_test.py +0 -0
  53. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/Jsonable.py +0 -0
  54. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  55. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  56. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/MtlsServerSslOptions.py +0 -0
  57. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/Payload.py +0 -0
  58. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadEnvelope.py +0 -0
  59. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadFilterKeys.py +0 -0
  60. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadResponseTest.py +0 -0
  61. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadResponse_test.py +0 -0
  62. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PayloadTest.py +0 -0
  63. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/PerformTestActionTuple.py +0 -0
  64. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/SerialiseUtil.py +0 -0
  65. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/SerialiseUtil_test.py +0 -0
  66. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TestTuple.py +0 -0
  67. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleAction.py +0 -0
  68. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleBson.py +0 -0
  69. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleBson_test.py +0 -0
  70. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleDataForTest.py +0 -0
  71. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleDocumentConverter.py +0 -0
  72. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TuplePerfTest.py +0 -0
  73. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  74. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleRestful.py +0 -0
  75. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleRestful_test.py +0 -0
  76. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleSelector.py +0 -0
  77. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/TupleTest.py +0 -0
  78. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexABC.py +0 -0
  79. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexClientHttp.py +0 -0
  80. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexClientHttpTest.py +0 -0
  81. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexClientTcp.py +0 -0
  82. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexClientWebsocketFactory.py +0 -0
  83. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexConnectionABC.py +0 -0
  84. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexEndpointPermission.py +0 -0
  85. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexFactory_test.py +0 -0
  86. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexPermission.py +0 -0
  87. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexPermissionError.py +0 -0
  88. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServerConnection_test.py +0 -0
  89. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServerHttpResource.py +0 -0
  90. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServerTcp.py +0 -0
  91. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexServerWebsocket_test.py +0 -0
  92. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexSession_test.py +0 -0
  93. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  94. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  95. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexTest.py +0 -0
  96. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexUtil.py +0 -0
  97. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/VortexWritePushProducer.py +0 -0
  98. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  99. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  100. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/data_loader/__init__.py +0 -0
  101. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  102. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  103. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  104. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/event_bus/__init__.py +0 -0
  105. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  106. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  107. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleActionPushOffline.py +0 -0
  108. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  109. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  110. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataActionClient.py +0 -0
  111. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataObservableCache.py +0 -0
  112. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  113. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  114. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  115. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/TupleDataProviderABC.py +0 -0
  116. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  117. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/handler/__init__.py +0 -0
  118. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/mypy_plugin.py +0 -0
  119. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  120. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/py.typed +0 -0
  121. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/GzippedDataHttpClient.py +0 -0
  122. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  123. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  124. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/RestfulHttpClient.py +0 -0
  125. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/RestfulResource.py +0 -0
  126. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/__init__.py +0 -0
  127. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/restful_http_client_test.py +0 -0
  128. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/restful/restful_resource_test.py +0 -0
  129. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/rpc/RPCTest.py +0 -0
  130. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/rpc/RPC_test.py +0 -0
  131. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/rpc/__init__.py +0 -0
  132. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  133. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  134. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/sqla_orm/__init__.py +0 -0
  135. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  136. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/storage/TupleStorageSqlite.py +0 -0
  137. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/storage/__init__.py +0 -0
  138. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/tuple_json_schema/__init__.py +0 -0
  139. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/tuple_json_schema/constants.py +0 -0
  140. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  141. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  142. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  143. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  144. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/tuple_test_fixtures.py +0 -0
  145. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/twisted_runtime_metrics.py +0 -0
  146. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortex/twisted_runtime_metrics_test.py +0 -0
  147. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortexpy.egg-info/dependency_links.txt +0 -0
  148. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortexpy.egg-info/entry_points.txt +0 -0
  149. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortexpy.egg-info/requires.txt +0 -0
  150. {vortexpy-4.3.6 → vortexpy-6.0.1}/vortexpy.egg-info/top_level.txt +0 -0
vortexpy-6.0.1/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016-2026 Flying Ion Pty Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 4.3.6
3
+ Version: 6.0.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
@@ -32,7 +32,7 @@ Requires-Dist: txhttputil>=1.2.0
32
32
  Flying Ion's observable, routable, data serialisation and transport code.
33
33
 
34
34
  See [README_PERMISSIONS.md](README_PERMISSIONS.md) for the two-level permission
35
- system (endpoint guards + access control context) introduced in v5.0.
35
+ system (endpoint guards + access control context) introduced in v6.0.
36
36
 
37
37
  **Tuple and Payload tutorials:** [docs/README.md](docs/README.md) (simple → full features, encode/decode, threaded decode).
38
38
 
@@ -40,9 +40,14 @@ Why PY at the end? To be consistent with VortexJS, which is the Browser side pac
40
40
  See https://gitlab.com/flyingion/vortexjs
41
41
 
42
42
  Requirements:
43
- * Python 3.5
44
- * Twisted 16+
45
- * SQLAlchemy 1, 1.1
43
+ * Python 3.12 (development target)
44
+ * Twisted (autobahn / txwebsocket)
45
+ * SQLAlchemy >= 2.0
46
+
47
+ See `pyproject.toml` for the authoritative dependency list.
48
+
49
+ See [README_MIGRATION.md](README_MIGRATION.md) for breaking changes and
50
+ migration guides (4.4.0, 6.0.0).
46
51
 
47
52
  The "vortex" is designed to transport "Payloads" from a web browser (VortexJS) to a
48
53
  twisted web server (VortexPY). There is also a python client for python client to python server
@@ -3,7 +3,7 @@
3
3
  Flying Ion's observable, routable, data serialisation and transport code.
4
4
 
5
5
  See [README_PERMISSIONS.md](README_PERMISSIONS.md) for the two-level permission
6
- system (endpoint guards + access control context) introduced in v5.0.
6
+ system (endpoint guards + access control context) introduced in v6.0.
7
7
 
8
8
  **Tuple and Payload tutorials:** [docs/README.md](docs/README.md) (simple → full features, encode/decode, threaded decode).
9
9
 
@@ -11,9 +11,14 @@ Why PY at the end? To be consistent with VortexJS, which is the Browser side pac
11
11
  See https://gitlab.com/flyingion/vortexjs
12
12
 
13
13
  Requirements:
14
- * Python 3.5
15
- * Twisted 16+
16
- * SQLAlchemy 1, 1.1
14
+ * Python 3.12 (development target)
15
+ * Twisted (autobahn / txwebsocket)
16
+ * SQLAlchemy >= 2.0
17
+
18
+ See `pyproject.toml` for the authoritative dependency list.
19
+
20
+ See [README_MIGRATION.md](README_MIGRATION.md) for breaking changes and
21
+ migration guides (4.4.0, 6.0.0).
17
22
 
18
23
  The "vortex" is designed to transport "Payloads" from a web browser (VortexJS) to a
19
24
  twisted web server (VortexPY). There is also a python client for python client to python server
@@ -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 = "6.0.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."
@@ -28,26 +28,14 @@ from vortex.VortexEndpointPermission import (
28
28
  from vortex.VortexSession import (
29
29
  _callableLabel,
30
30
  checkAccessControlContextSignature,
31
+ checkEndpointPermissionsSignature,
31
32
  checkNoHttpSessionInSignature,
33
+ checkNoVortexSessionInSignature,
32
34
  )
33
35
 
34
36
  logger = logging.getLogger(__name__)
35
37
 
36
38
 
37
- # TODO: typings needs to support keyword args before tbis will work.
38
- # PayloadEndpointProcessCallable = Callable[
39
- # [
40
- # ...
41
- # # PayloadEnvelope, # payloadEnvelope
42
- # # str, # vortexUuid
43
- # # str, # vortexName
44
- # # Any, # vortexSession
45
- # # SendVortexMsgResponseCallable # sendResponse
46
- # ],
47
- # Optional[Deferred] # Return
48
- # ]
49
-
50
-
51
39
  class PayloadEndpoint:
52
40
  """
53
41
  The payload endpoint is responsible for matching payloads filters against
@@ -103,7 +91,14 @@ class PayloadEndpoint:
103
91
  callableDescription=callableDesc,
104
92
  )
105
93
  )
94
+ self._callableAcceptsEndpointPermissions = (
95
+ checkEndpointPermissionsSignature(
96
+ callable_,
97
+ callableDescription=callableDesc,
98
+ )
99
+ )
106
100
  checkNoHttpSessionInSignature(callable_, callableDesc)
101
+ checkNoVortexSessionInSignature(callable_, callableDesc)
107
102
 
108
103
  self._acceptOnlyFromVortex = acceptOnlyFromVortex
109
104
  if isinstance(self._acceptOnlyFromVortex, str):
@@ -233,6 +228,7 @@ class PayloadEndpoint:
233
228
 
234
229
  # Level 1: Endpoint permission check
235
230
  auth = VortexFactory.getAuthenticator()
231
+ endpointPermissions = None
236
232
  if self._requiredPermissions:
237
233
  if not auth:
238
234
  logger.error(
@@ -259,6 +255,19 @@ class PayloadEndpoint:
259
255
  )
260
256
  return
261
257
 
258
+ # Level 1 injection: when the callable declares endpointPermissions,
259
+ # compute the caller's permissions once here at the boundary so
260
+ # handlers never see the session or the authenticator.
261
+ if (
262
+ self._callableAcceptsEndpointPermissions
263
+ and endpointPermissions is None
264
+ and auth
265
+ and vortexSession
266
+ ):
267
+ endpointPermissions = yield maybeDeferred(
268
+ auth.getEndpointPermissions, vortexSession
269
+ )
270
+
262
271
  # Level 2: Get access control context, but only if the callback uses it.
263
272
  # Default to the sentinel so handlers always receive a non-None value
264
273
  # with a clear error on attribute access when auth is unavailable.
@@ -283,15 +292,16 @@ class PayloadEndpoint:
283
292
 
284
293
  callable_ = self._wref()
285
294
  if callable_:
286
- kwargs = dict(
295
+ kwargs: Dict[str, object] = dict(
287
296
  payloadEnvelope=payloadEnvelope,
288
297
  vortexUuid=vortexUuid,
289
298
  vortexName=vortexName,
290
299
  sendResponse=sendResponse,
291
- vortexSession=vortexSession,
292
300
  )
293
301
  if self._callableAcceptsAccessControlContext:
294
302
  kwargs["accessControlContext"] = accessControlContext
303
+ if self._callableAcceptsEndpointPermissions:
304
+ kwargs["endpointPermissions"] = endpointPermissions
295
305
  return (yield maybeDeferred(callable_, **kwargs))
296
306
  else:
297
307
  PayloadIO().remove(self)
@@ -54,7 +54,6 @@ class PayloadEndpointPyTest(unittest.TestCase):
54
54
  payloadEnvelope=PayloadEnvelope(filt=payload.filt),
55
55
  vortexUuid="test",
56
56
  vortexName="test",
57
- vortexSession=None,
58
57
  sendResponse=lambda x: x,
59
58
  )
60
59
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  import logging
10
10
  from datetime import datetime
11
+ from typing import Optional
11
12
 
12
13
  import pytz
13
14
  from twisted.internet import reactor
@@ -59,8 +60,8 @@ class PayloadIO(object):
59
60
  payloadEnvelope: PayloadEnvelope,
60
61
  vortexUuid: str,
61
62
  vortexName: str,
62
- vortexSession,
63
- sendResponse: SendVortexMsgResponseCallable,
63
+ vortexSession=None,
64
+ sendResponse: Optional[SendVortexMsgResponseCallable] = None,
64
65
  ):
65
66
  from vortex.VortexFactory import VortexFactory
66
67
 
@@ -95,8 +96,8 @@ class PayloadIO(object):
95
96
  payloadEnvelope: PayloadEnvelope,
96
97
  vortexUuid: str,
97
98
  vortexName: str,
98
- vortexSession,
99
- sendResponse: SendVortexMsgResponseCallable,
99
+ vortexSession=None,
100
+ sendResponse: Optional[SendVortexMsgResponseCallable] = None,
100
101
  ):
101
102
  startDate = datetime.now(pytz.utc)
102
103
 
@@ -106,6 +107,10 @@ class PayloadIO(object):
106
107
  common function
107
108
  """
108
109
  try:
110
+ assert sendResponse is not None, (
111
+ "PayloadIO._processLater: sendResponse must be set"
112
+ " to respond to an exception"
113
+ )
109
114
  sendResponse(
110
115
  PayloadEnvelope(
111
116
  filt=payloadEnvelope.filt,
@@ -7,4 +7,5 @@
7
7
  # Support : contact@flyingion.com
8
8
 
9
9
  RPC_PRIORITY = 10
10
+ HEARTBEAT_PRIORITY = 20
10
11
  DEFAULT_PRIORITY = 100
@@ -157,7 +157,6 @@ class PayloadResponse(Deferred):
157
157
  vortexUuid: str,
158
158
  vortexName: str,
159
159
  sendResponse=None,
160
- vortexSession=None,
161
160
  ):
162
161
  if self._endpoint:
163
162
  self._endpoint.shutdown()
@@ -288,6 +288,12 @@ class TupleField:
288
288
  class _Map:
289
289
  pass
290
290
 
291
+ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
292
+ # Tuple classes declare fields as `field: str = TupleField()`.
293
+ # The Any return keeps those declarations type-correct without
294
+ # per-field type-ignore comments. Runtime behaviour is unchanged.
295
+ return super().__new__(cls)
296
+
291
297
  def __init__(
292
298
  self,
293
299
  defaultValue=None,
@@ -6,8 +6,15 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
+ # ruff: noqa: N999
10
+ # Module names use PascalCase to match the primary class.
9
11
 
10
- class VortexAccessControlContextABC:
12
+ from abc import ABC, abstractmethod
13
+
14
+ from vortex.VortexUserDetailsABC import VortexUserDetailsABC
15
+
16
+
17
+ class VortexAccessControlContextABC(ABC):
11
18
  """Abstract base class for application-level access control contexts.
12
19
 
13
20
  This is a **Level 2** permission mechanism. Where
@@ -16,23 +23,27 @@ class VortexAccessControlContextABC:
16
23
  ``VortexAccessControlContextABC`` answers "given access, what specific data
17
24
  or actions is this user allowed to interact with?".
18
25
 
19
- Subclass it and add domain-specific check methods. Always call
20
- ``super().__init__(vortexName, vortexUuid)`` to store the transport
21
- metadata that the library passes through::
26
+ Subclass it, implement :meth:`userDetails`, and add domain-specific check
27
+ methods. Always call ``super().__init__(vortexName, vortexUuid)`` to
28
+ store the transport metadata that the library passes through::
22
29
 
23
- class AttuneAccessControlContext(VortexAccessControlContextABC):
30
+ class ExampleAccessControlContext(VortexAccessControlContextABC):
24
31
  def __init__(self, user, vortexName: str, vortexUuid: str):
25
32
  super().__init__(vortexName=vortexName, vortexUuid=vortexUuid)
26
33
  self._user = user
27
34
 
35
+ @property
36
+ def userDetails(self):
37
+ return None
38
+
28
39
  def canExecuteJob(self, job_id: int) -> bool:
29
40
  return job_id in self._user.allowedJobIds
30
41
 
31
42
  Obtain the context inside a processing method via the ``accessControlContext``
32
43
  keyword argument that the library injects::
33
44
 
34
- def processTupleAction(self, tupleAction, accessControlContext: AttuneAccessControlContext):
35
- assert isinstance(accessControlContext, AttuneAccessControlContext)
45
+ def processTupleAction(self, tupleAction, accessControlContext: ExampleAccessControlContext):
46
+ assert isinstance(accessControlContext, ExampleAccessControlContext)
36
47
  if not accessControlContext.canExecuteJob(tupleAction.jobId):
37
48
  raise PermissionError("Not allowed")
38
49
 
@@ -46,10 +57,18 @@ class VortexAccessControlContextABC:
46
57
  * ``vortexName`` — the name of the remote vortex that sent the request.
47
58
  * ``vortexUuid`` — the per-connection UUID of that vortex.
48
59
 
60
+ **User identity**
61
+
62
+ Contexts must implement ``userDetails``, the identity of the logged-in
63
+ user provided by the host application (``None`` when no identity is
64
+ available). The library reads it to display who holds a live record
65
+ lock; it never mutates it.
66
+
49
67
  When no authenticator is registered, or the session is unavailable, the
50
68
  library passes :data:`NO_AUTH_ACCESS_CONTROL_CONTEXT` — a sentinel whose
51
- ``vortexName`` and ``vortexUuid`` are empty strings and any other attribute
52
- access raises :class:`RuntimeError`.
69
+ ``vortexName`` and ``vortexUuid`` are empty strings, whose ``userDetails``
70
+ is ``None``, and where any other attribute access raises
71
+ :class:`RuntimeError`.
53
72
 
54
73
  To detect the no-auth case explicitly::
55
74
 
@@ -76,15 +95,26 @@ class VortexAccessControlContextABC:
76
95
  def vortexUuid(self) -> str:
77
96
  return self._vortexUuid
78
97
 
98
+ @property
99
+ @abstractmethod
100
+ def userDetails(self) -> VortexUserDetailsABC | None:
101
+ """Identity of the logged-in user for this session.
102
+
103
+ The library reads this to display who holds a live record lock; it
104
+ never mutates it. Return ``None`` when the host application does
105
+ not provide user identity.
106
+ """
107
+
79
108
 
80
109
  class _NoAuthAccessControlContext(VortexAccessControlContextABC):
81
110
  """Sentinel passed as ``accessControlContext`` when no authenticator is
82
111
  registered or the session is unavailable.
83
112
 
84
- ``vortexName`` and ``vortexUuid`` return empty strings. Any other
85
- attribute access raises a descriptive :class:`RuntimeError` so that bugs
86
- in handlers are caught immediately with an actionable message rather than
87
- a bare ``AttributeError`` on ``None``.
113
+ ``vortexName`` and ``vortexUuid`` return empty strings and ``userDetails``
114
+ returns ``None``. Any other attribute access raises a descriptive
115
+ :class:`RuntimeError` so that bugs in handlers are caught immediately
116
+ with an actionable message rather than a bare ``AttributeError`` on
117
+ ``None``.
88
118
  """
89
119
 
90
120
  def __init__(self):
@@ -93,6 +123,10 @@ class _NoAuthAccessControlContext(VortexAccessControlContextABC):
93
123
  object.__setattr__(self, "_vortexName", "")
94
124
  object.__setattr__(self, "_vortexUuid", "")
95
125
 
126
+ @property
127
+ def userDetails(self) -> None:
128
+ return None
129
+
96
130
  def __getattr__(self, name: str):
97
131
  raise RuntimeError(
98
132
  f"accessControlContext.{name} was accessed, but no authenticator is "
@@ -6,11 +6,13 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
+ # ruff: noqa: N999
10
+ # Module names use PascalCase to match the primary class.
11
+
9
12
  from abc import ABC, abstractmethod
10
13
 
11
14
  from twisted.internet.defer import Deferred
12
15
 
13
- from vortex.VortexEndpointPermission import EndpointPermissionsType
14
16
  from vortex.VortexSession import VortexSession
15
17
 
16
18
 
@@ -82,6 +84,9 @@ class VortexAuthenticatorABC(ABC):
82
84
  def __init__(self, user, vortexName: str, vortexUuid: str):
83
85
  super().__init__(vortexName=vortexName, vortexUuid=vortexUuid)
84
86
  self._user = user
87
+ @property
88
+ def userDetails(self):
89
+ return None
85
90
  def canEditRecord(self, record_id: int) -> bool:
86
91
  return record_id in self._user.editableRecordIds
87
92
 
@@ -966,7 +966,6 @@ class VortexFactory:
966
966
  payloadEnvelope=payloadEnvelope,
967
967
  vortexUuid=vortexUuid,
968
968
  vortexName=vortexName,
969
- vortexSession=None,
970
969
  sendResponse=sendResponse,
971
970
  )
972
971
  return succeed(True)
@@ -155,14 +155,13 @@ class VortexPayloadProtocol(Protocol, metaclass=ABCMeta):
155
155
  def _deliverPayload(self, payloadEnvelope):
156
156
  """Deliver a decoded payload envelope to the PayloadIO for processing.
157
157
 
158
- Subclasses (e.g. VortexTcpServerProtocol) may override this to inject
159
- an vortexSession or wait for authentication before dispatching.
158
+ Subclasses (e.g. VortexTcpServerProtocol) may override this to pass the
159
+ connection vortexSession or wait for authentication before dispatching.
160
160
  """
161
161
  PayloadIO().process(
162
162
  payloadEnvelope,
163
163
  vortexUuid=self._serverVortexUuid,
164
164
  vortexName=self._serverVortexName,
165
- vortexSession=None,
166
165
  sendResponse=self._createResponseSenderCallable(),
167
166
  )
168
167
 
@@ -138,6 +138,23 @@ class VortexServer(VortexABC):
138
138
 
139
139
  return "%s:%s" % (connection.ip, connection.port)
140
140
 
141
+ def httpSessionForVortexUuid(self, vortexUuid: str) -> Optional[Session]:
142
+ """HTTP Session for Vortex UUID
143
+
144
+ Return the HTTP session associated with the connection for
145
+ *vortexUuid*, or ``None`` when the vortex is not connected or has no
146
+ registered session. For TCP connections this is the
147
+ :class:`~vortex.VortexSession.VortexTcpSession` stub stored at
148
+ connection time.
149
+
150
+ :param vortexUuid: The per-connection UUID of the remote vortex.
151
+ :return: The session for the connection, or ``None``.
152
+ """
153
+ connection = self._connectionByVortexUuid.get(vortexUuid)
154
+ if connection is None:
155
+ return None
156
+ return self._httpSessionsBySessionUuid.get(connection.httpSessionUuid)
157
+
141
158
  def connectionOpened(self, httpSession, vortexConnection):
142
159
  if self._DEBUG_LOGGING:
143
160
  logger.debug("VortexServer - connectionOpened")
@@ -154,9 +171,9 @@ class VortexServer(VortexABC):
154
171
  )
155
172
 
156
173
  httpSessionConns = VortexSessionI(httpSession).connections
157
- for oldConn in list(httpSessionConns.values()):
158
- if oldConn is not vortexConnection:
159
- oldConn.close()
174
+ existingConn = httpSessionConns.get(vortexUuid)
175
+ if existingConn is not None and existingConn is not vortexConnection:
176
+ existingConn.close()
160
177
  httpSessionConns[vortexUuid] = vortexConnection
161
178
 
162
179
  if vortexUuid in self._connectionByVortexUuid:
@@ -304,7 +321,6 @@ class VortexServer(VortexABC):
304
321
  payloadEnvelope,
305
322
  vortexUuid=self._uuid,
306
323
  vortexName=self._name,
307
- vortexSession=None,
308
324
  sendResponse=sendResponse,
309
325
  )
310
326
 
@@ -12,7 +12,7 @@ from datetime import datetime
12
12
  import pytz
13
13
  from twisted.internet import task
14
14
 
15
- from .PayloadPriority import DEFAULT_PRIORITY
15
+ from .PayloadPriority import DEFAULT_PRIORITY, HEARTBEAT_PRIORITY
16
16
  from .VortexConnectionABC import VortexConnectionABC
17
17
  from .VortexServer import VortexServer, HEART_BEAT_PERIOD, HEART_BEAT_TIMEOUT
18
18
  from .VortexWritePushProducer import VortexWritePushProducer
@@ -94,7 +94,7 @@ class VortexServerConnection(VortexConnectionABC):
94
94
  self.close()
95
95
  return
96
96
 
97
- self._write(b".", DEFAULT_PRIORITY)
97
+ self._write(b".", HEARTBEAT_PRIORITY)
98
98
 
99
99
  @property
100
100
  def ip(self):
@@ -336,7 +336,6 @@ class VortexWebSocketUpgradeResource(resource.Resource):
336
336
  request.channel.upgradeToWebsocket(websocketProtocol)
337
337
 
338
338
  def unregister():
339
- request.channel.unregisterProducer()
340
339
  websocketProtocol.unregisterProducer()
341
340
 
342
341
  producer = VortexWritePushProducer(
@@ -351,7 +350,10 @@ class VortexWebSocketUpgradeResource(resource.Resource):
351
350
  producer.ERROR_DATA_LENGTH = 50 * 1024 * 1024
352
351
 
353
352
  unregister()
354
- request.channel.registerProducer(producer, streaming=True)
353
+ # Register only on the WebSocket protocol. Registering the same producer
354
+ # on HTTPChannel._requestProducer as well lets HTTPChannel.pauseProducing
355
+ # stall outbound traffic without a matching resume while the websocket
356
+ # request is still open (NOT_DONE_YET).
355
357
  websocketProtocol.registerProducer(producer, streaming=True)
356
358
 
357
359
  def closedError(failure):
@@ -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
 
@@ -181,28 +181,81 @@ def checkAccessControlContextSignature(
181
181
  return True
182
182
 
183
183
 
184
- def checkNoHttpSessionInSignature(callable_, callableDescription: str = ""):
185
- """Log an ERROR if *callable_* declares httpSession as an explicit parameter.
184
+ def checkEndpointPermissionsSignature(
185
+ callable_,
186
+ callableDescription: str = "",
187
+ ) -> bool:
188
+ """Check whether *callable_* accepts an ``endpointPermissions`` parameter.
186
189
 
187
- Called at registration time to detect callbacks that still reference the
188
- removed httpSession parameter.
190
+ Called at **registration time** (not call time) so that the boundary knows
191
+ whether to compute and inject the caller's endpoint permissions frozenset.
192
+
193
+ :param callable_: The callback to inspect.
194
+ :param callableDescription: Human-readable description for log messages.
195
+ :return: ``True`` if the callable declares an explicit
196
+ ``endpointPermissions`` parameter, ``False`` otherwise.
189
197
  """
198
+ return checkAccessControlContextSignature(
199
+ callable_,
200
+ paramName="endpointPermissions",
201
+ callableDescription=callableDescription,
202
+ )
203
+
204
+
205
+ def _checkNoRemovedParamInSignature(
206
+ callable_,
207
+ paramName: str,
208
+ replacementHint: str,
209
+ callableDescription: str = "",
210
+ ):
211
+ """Log an ERROR if *callable_* declares a removed parameter explicitly."""
190
212
  try:
191
213
  sig = inspect.signature(callable_)
192
214
  except (ValueError, TypeError):
193
215
  return
194
216
 
195
- if "httpSession" in sig.parameters:
196
- param = sig.parameters["httpSession"]
217
+ if paramName in sig.parameters:
218
+ param = sig.parameters[paramName]
197
219
  if param.kind != inspect.Parameter.VAR_KEYWORD:
198
220
  logger.error(
199
- "CRITICAL FIX REQUIRED: %s declares 'httpSession' which has been "
200
- "removed in v5.0. Use 'accessControlContext' instead. "
201
- "This callback will NOT receive 'httpSession'.",
221
+ "CRITICAL FIX REQUIRED: %s declares '%s' which has been "
222
+ "removed in v6.0.0. %s "
223
+ "This callback will NOT receive '%s'.",
202
224
  callableDescription or _callableLabel(callable_),
225
+ paramName,
226
+ replacementHint,
227
+ paramName,
203
228
  )
204
229
 
205
230
 
231
+ def checkNoHttpSessionInSignature(callable_, callableDescription: str = ""):
232
+ """Log an ERROR if *callable_* declares httpSession as an explicit parameter.
233
+
234
+ Called at registration time to detect callbacks that still reference the
235
+ removed httpSession parameter.
236
+ """
237
+ _checkNoRemovedParamInSignature(
238
+ callable_,
239
+ "httpSession",
240
+ "Use 'accessControlContext' instead.",
241
+ callableDescription,
242
+ )
243
+
244
+
245
+ def checkNoVortexSessionInSignature(callable_, callableDescription: str = ""):
246
+ """Log an ERROR if *callable_* declares vortexSession as an explicit parameter.
247
+
248
+ Called at registration time to detect callbacks that still reference the
249
+ removed vortexSession parameter.
250
+ """
251
+ _checkNoRemovedParamInSignature(
252
+ callable_,
253
+ "vortexSession",
254
+ "Use 'accessControlContext' or 'endpointPermissions' instead.",
255
+ callableDescription,
256
+ )
257
+
258
+
206
259
  def _callableLabel(callable_) -> str:
207
260
  """Return a human-readable label for a callable for error messages."""
208
261
  if hasattr(callable_, "__qualname__"):