vortexpy 4.1.2__tar.gz → 4.1.5__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.
- {vortexpy-4.1.2 → vortexpy-4.1.5}/PKG-INFO +1 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/pyproject.toml +1 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/DeferUtil.py +1 -3
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadEndpoint.py +22 -8
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadEndpointTest.py +1 -3
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadEnvelope.py +9 -9
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/SerialiseUtil.py +7 -0
- vortexpy-4.1.5/vortex/SerialiseUtil_test.py +106 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/Tuple.py +2 -3
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +4 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TupleRestful.py +25 -8
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TupleRestfulTest.py +15 -7
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexAccessControlContextABC.py +1 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexFactory.py +13 -4
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexPayloadProtocol.py +6 -2
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexPermission.py +1 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexPermissionError.py +1 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexServer.py +6 -5
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexServerConnection.py +2 -2
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexServerWebsocket.py +0 -7
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexSession.py +4 -2
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexTcpMemoryLeakPayloadTest.py +24 -24
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexWritePushProducer.py +0 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/__init__.py +1 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/data_loader/TupleDataLoader.py +28 -16
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/data_loader/TupleDataLoaderDelegate.py +14 -4
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/event_bus/TupleChangeEventBus.py +7 -8
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleActionProcessor.py +73 -37
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleActionProcessorProxy.py +6 -5
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleActionProcessorTestHandler.py +3 -3
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataObservableHandler.py +27 -16
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataObservableProxyHandler.py +13 -4
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataProviderABC.py +3 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/rpc/RPC.py +10 -4
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/sqla_orm/OrmCrudHandler.py +1 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/storage/TupleStorageSqlite.py +3 -3
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/tuple_json_schema/json_schema_node_tuple.py +1 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/tuple_json_schema/tuple_json_schema_builder.py +3 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/tuple_json_schema/tuple_json_schema_test.py +3 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortexpy.egg-info/SOURCES.txt +1 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/README.md +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/setup.cfg +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/Jsonable.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/Payload.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadIO.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadResponse.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TestTuple.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TupleAction.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/TupleTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexABC.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexClientTcp.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexClientWebsocketFactory.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/VortexUtil.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataObservableCache.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/TupleDataObserverClient.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/mypy_plugin.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/py.typed +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/RestfulHttpClientTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/RestfulResourceTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.1.2 → vortexpy-4.1.5}/vortexpy.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortexpy"
|
|
7
|
-
version = "4.1.
|
|
7
|
+
version = "4.1.5"
|
|
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."
|
|
@@ -175,9 +175,7 @@ def deferToThreadWrapWithLogger(
|
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
"""
|
|
178
|
-
assert isinstance(
|
|
179
|
-
loggerArg, logging.Logger
|
|
180
|
-
), """Usage:
|
|
178
|
+
assert isinstance(loggerArg, logging.Logger), """Usage:
|
|
181
179
|
import logging
|
|
182
180
|
logger = logging.getLogger(__name__)
|
|
183
181
|
@deferToThreadWrapWithLogger(logger)
|
|
@@ -22,7 +22,9 @@ from vortex.VortexAccessControlContextABC import (
|
|
|
22
22
|
NO_AUTH_ACCESS_CONTROL_CONTEXT,
|
|
23
23
|
VortexAccessControlContextABC,
|
|
24
24
|
)
|
|
25
|
-
from vortex.VortexEndpointPermission import
|
|
25
|
+
from vortex.VortexEndpointPermission import (
|
|
26
|
+
EndpointPermissionsType as PermissionsType,
|
|
27
|
+
)
|
|
26
28
|
from vortex.VortexSession import (
|
|
27
29
|
_callableLabel,
|
|
28
30
|
checkAccessControlContextSignature,
|
|
@@ -81,7 +83,9 @@ class PayloadEndpoint:
|
|
|
81
83
|
explicitly declare "no permissions required". Omitting the
|
|
82
84
|
parameter (``None``) emits a deprecation warning.
|
|
83
85
|
"""
|
|
84
|
-
self._requiredPermissions: Optional[PermissionsType] =
|
|
86
|
+
self._requiredPermissions: Optional[PermissionsType] = (
|
|
87
|
+
requiredPermissions
|
|
88
|
+
)
|
|
85
89
|
|
|
86
90
|
if requiredPermissions is None:
|
|
87
91
|
logger.warning(self._NO_PERMISSIONS_WARNING, filt)
|
|
@@ -89,9 +93,11 @@ class PayloadEndpoint:
|
|
|
89
93
|
callableDesc = (
|
|
90
94
|
f"PayloadEndpoint filt={filt} callback={_callableLabel(callable_)}"
|
|
91
95
|
)
|
|
92
|
-
self._callableAcceptsAccessControlContext =
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
self._callableAcceptsAccessControlContext = (
|
|
97
|
+
checkAccessControlContextSignature(
|
|
98
|
+
callable_,
|
|
99
|
+
callableDescription=callableDesc,
|
|
100
|
+
)
|
|
95
101
|
)
|
|
96
102
|
checkNoHttpSessionInSignature(callable_, callableDesc)
|
|
97
103
|
|
|
@@ -168,7 +174,10 @@ class PayloadEndpoint:
|
|
|
168
174
|
return False
|
|
169
175
|
|
|
170
176
|
# Filter for the backend vortexes.
|
|
171
|
-
if
|
|
177
|
+
if (
|
|
178
|
+
self._acceptOnlyFromVortex
|
|
179
|
+
and vortexName not in self._acceptOnlyFromVortex
|
|
180
|
+
):
|
|
172
181
|
return False
|
|
173
182
|
|
|
174
183
|
def removeUnhashable(filt):
|
|
@@ -212,7 +221,9 @@ class PayloadEndpoint:
|
|
|
212
221
|
if not self.check(payloadEnvelope, vortexName):
|
|
213
222
|
return
|
|
214
223
|
|
|
215
|
-
if self.PERMITTER and not self.PERMITTER.check(
|
|
224
|
+
if self.PERMITTER and not self.PERMITTER.check(
|
|
225
|
+
payloadEnvelope, vortexSession
|
|
226
|
+
):
|
|
216
227
|
logger.debug("Permission failed for %s", payloadEnvelope.filt)
|
|
217
228
|
return
|
|
218
229
|
|
|
@@ -250,7 +261,10 @@ class PayloadEndpoint:
|
|
|
250
261
|
accessControlContext = NO_AUTH_ACCESS_CONTROL_CONTEXT
|
|
251
262
|
if self._callableAcceptsAccessControlContext and auth and vortexSession:
|
|
252
263
|
raw_ctx = yield maybeDeferred(
|
|
253
|
-
auth.getAccessControlContext,
|
|
264
|
+
auth.getAccessControlContext,
|
|
265
|
+
vortexSession,
|
|
266
|
+
vortexName,
|
|
267
|
+
vortexUuid,
|
|
254
268
|
)
|
|
255
269
|
if not isinstance(raw_ctx, VortexAccessControlContextABC):
|
|
256
270
|
logger.error(
|
|
@@ -143,9 +143,7 @@ class PayloadEndpointPyTest(unittest.TestCase):
|
|
|
143
143
|
|
|
144
144
|
def subScope():
|
|
145
145
|
inst = PayloadEndpointPyTestAssignPayload(self)
|
|
146
|
-
PayloadEndpoint(
|
|
147
|
-
filt, inst.process, requiredPermissions=frozenset()
|
|
148
|
-
)
|
|
146
|
+
PayloadEndpoint(filt, inst.process, requiredPermissions=frozenset())
|
|
149
147
|
|
|
150
148
|
self._processEnvelope(payload)
|
|
151
149
|
yield self._flushReactor()
|
|
@@ -57,9 +57,9 @@ class PayloadEnvelope(Jsonable):
|
|
|
57
57
|
) -> None:
|
|
58
58
|
Jsonable.__init__(self)
|
|
59
59
|
"""Constructor"""
|
|
60
|
-
assert not (
|
|
61
|
-
|
|
62
|
-
)
|
|
60
|
+
assert not (data and encodedPayload), (
|
|
61
|
+
"You can not pass encodedData and encodedPayload"
|
|
62
|
+
)
|
|
63
63
|
|
|
64
64
|
self.filt: Dict = {} if filt is None else filt
|
|
65
65
|
self.data: Union[Any, str, None] = data if data else encodedPayload
|
|
@@ -75,18 +75,18 @@ class PayloadEnvelope(Jsonable):
|
|
|
75
75
|
@property
|
|
76
76
|
def encodedPayload(self) -> Optional[str]:
|
|
77
77
|
# self.data could be from VortexJS/Typescript where we allow `str` data
|
|
78
|
-
assert self.data is None or isinstance(
|
|
79
|
-
|
|
80
|
-
)
|
|
78
|
+
assert self.data is None or isinstance(self.data, str), (
|
|
79
|
+
"Encoded payload is not None or a string"
|
|
80
|
+
)
|
|
81
81
|
return self.data
|
|
82
82
|
|
|
83
83
|
@encodedPayload.setter
|
|
84
84
|
def encodedPayload(self, val: Optional[str]):
|
|
85
85
|
# This value should only be set from users of VortexPy and
|
|
86
86
|
# Payload.toEncodedPayload so we only allow str or None
|
|
87
|
-
assert val is None or isinstance(
|
|
88
|
-
|
|
89
|
-
)
|
|
87
|
+
assert val is None or isinstance(val, str), (
|
|
88
|
+
"Encoded payload is not None or a string"
|
|
89
|
+
)
|
|
90
90
|
self.data = val
|
|
91
91
|
|
|
92
92
|
def isEmpty(self):
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# Support : contact@flyingion.com
|
|
8
8
|
|
|
9
9
|
import decimal
|
|
10
|
+
import enum
|
|
10
11
|
import logging
|
|
11
12
|
from base64 import b64encode, b64decode
|
|
12
13
|
from collections import defaultdict
|
|
@@ -267,6 +268,12 @@ def toRapuiType(value):
|
|
|
267
268
|
if isinstance(value, PayloadEnvelope):
|
|
268
269
|
return T_RAPUI_PAYLOAD_ENVELOPE
|
|
269
270
|
|
|
271
|
+
if isinstance(value, enum.StrEnum):
|
|
272
|
+
return T_STR
|
|
273
|
+
|
|
274
|
+
if isinstance(value, enum.IntEnum):
|
|
275
|
+
return T_INT
|
|
276
|
+
|
|
270
277
|
valueClass = value.__class__
|
|
271
278
|
|
|
272
279
|
return TYPE_MAP_PY_TO_RAPUI[valueClass]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from enum import Enum, IntEnum, StrEnum
|
|
11
|
+
|
|
12
|
+
from twisted.trial import unittest
|
|
13
|
+
|
|
14
|
+
from vortex.SerialiseUtil import T_INT, T_STR, fromStr, toRapuiType, toStr
|
|
15
|
+
from vortex.TupleSelector import TupleSelector
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SampleStrEnumE(StrEnum):
|
|
19
|
+
CrewDatum = "crew-datum"
|
|
20
|
+
ViewJobDetail = "view-job-detail-datum"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SampleIntEnumE(IntEnum):
|
|
24
|
+
One = 1
|
|
25
|
+
Two = 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SamplePlainEnumE(Enum):
|
|
29
|
+
Alpha = "alpha"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SerialiseUtilTest(unittest.TestCase):
|
|
33
|
+
def testToRapuiTypeStrEnumMember(self) -> None:
|
|
34
|
+
self.assertEqual(toRapuiType(SampleStrEnumE.CrewDatum), T_STR)
|
|
35
|
+
self.assertEqual(
|
|
36
|
+
toRapuiType(SampleStrEnumE.ViewJobDetail),
|
|
37
|
+
T_STR,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def testToRapuiTypeIntEnumMember(self) -> None:
|
|
41
|
+
self.assertEqual(toRapuiType(SampleIntEnumE.One), T_INT)
|
|
42
|
+
self.assertEqual(toRapuiType(SampleIntEnumE.Two), T_INT)
|
|
43
|
+
|
|
44
|
+
def testToRapuiTypePlainEnumMemberRaises(self) -> None:
|
|
45
|
+
with self.assertRaises(KeyError):
|
|
46
|
+
toRapuiType(SamplePlainEnumE.Alpha)
|
|
47
|
+
|
|
48
|
+
def testToStrStrEnumMember(self) -> None:
|
|
49
|
+
self.assertEqual(toStr(SampleStrEnumE.CrewDatum), "crew-datum")
|
|
50
|
+
|
|
51
|
+
def testFromStrRoundTripStrEnumWireValue(self) -> None:
|
|
52
|
+
wireValue = toStr(SampleStrEnumE.CrewDatum)
|
|
53
|
+
self.assertEqual(fromStr(wireValue, T_STR), "crew-datum")
|
|
54
|
+
|
|
55
|
+
def testTupleSelectorSerialisesStrEnumSelectorValues(self) -> None:
|
|
56
|
+
selector = TupleSelector(
|
|
57
|
+
"peek_core_datum.DatumMappingTuple",
|
|
58
|
+
dict(
|
|
59
|
+
sourcePlugin="peek_plugin_switching_viewer",
|
|
60
|
+
datumPendingKey=SampleStrEnumE.CrewDatum,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
jsonStr = selector.toJsonStr()
|
|
64
|
+
parsed = json.loads(jsonStr)
|
|
65
|
+
self.assertEqual(
|
|
66
|
+
parsed["selector"]["datumPendingKey"],
|
|
67
|
+
"crew-datum",
|
|
68
|
+
)
|
|
69
|
+
self.assertEqual(parsed["selector"]["sourcePlugin"], "peek_plugin_switching_viewer")
|
|
70
|
+
|
|
71
|
+
def testTupleSelectorStrEnumRoundTrip(self) -> None:
|
|
72
|
+
original = TupleSelector(
|
|
73
|
+
"peek_core_datum.DatumMappingTuple",
|
|
74
|
+
dict(
|
|
75
|
+
sourcePlugin="peek_plugin_switching_viewer",
|
|
76
|
+
datumPendingKey=SampleStrEnumE.ViewJobDetail,
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
restored = TupleSelector.fromJsonStr(original.toJsonStr())
|
|
80
|
+
self.assertEqual(restored.name, original.name)
|
|
81
|
+
self.assertEqual(
|
|
82
|
+
restored.selector["datumPendingKey"],
|
|
83
|
+
"view-job-detail-datum",
|
|
84
|
+
)
|
|
85
|
+
self.assertIsInstance(restored.selector["datumPendingKey"], str)
|
|
86
|
+
|
|
87
|
+
def testTupleSelectorSerialisesIntEnumSelectorValues(self) -> None:
|
|
88
|
+
selector = TupleSelector(
|
|
89
|
+
"test.ExampleTuple",
|
|
90
|
+
dict(priority=SampleIntEnumE.Two),
|
|
91
|
+
)
|
|
92
|
+
jsonStr = selector.toJsonStr()
|
|
93
|
+
parsed = json.loads(jsonStr)
|
|
94
|
+
self.assertEqual(
|
|
95
|
+
parsed["selector"]["priority"],
|
|
96
|
+
{"_fd": 2, "_ft": "int"},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def testTupleSelectorIntEnumRoundTrip(self) -> None:
|
|
100
|
+
original = TupleSelector(
|
|
101
|
+
"test.ExampleTuple",
|
|
102
|
+
dict(priority=SampleIntEnumE.One),
|
|
103
|
+
)
|
|
104
|
+
restored = TupleSelector.fromJsonStr(original.toJsonStr())
|
|
105
|
+
self.assertEqual(restored.selector["priority"], 1)
|
|
106
|
+
self.assertIsInstance(restored.selector["priority"], int)
|
|
@@ -354,13 +354,12 @@ class IntTupleFieldValidator(TupleFieldValidatorABC):
|
|
|
354
354
|
def validate(self, fieldName: str, value):
|
|
355
355
|
if self._low is not None and value < self._low:
|
|
356
356
|
raise ValueError(
|
|
357
|
-
f"Field {fieldName},
|
|
357
|
+
f"Field {fieldName}, Value {value} is less than {self._low}"
|
|
358
358
|
)
|
|
359
359
|
|
|
360
360
|
if self._high is not None and self._high < value:
|
|
361
361
|
raise ValueError(
|
|
362
|
-
f"Field {fieldName},"
|
|
363
|
-
f" Value {value} is greater than {self._high}"
|
|
362
|
+
f"Field {fieldName}, Value {value} is greater than {self._high}"
|
|
364
363
|
)
|
|
365
364
|
|
|
366
365
|
|
|
@@ -98,5 +98,8 @@ class PolymorphicTupleTypeFieldArgTest(unittest.TestCase):
|
|
|
98
98
|
any(err.field_path == "concreteType" for err in result.errors)
|
|
99
99
|
)
|
|
100
100
|
self.assertTrue(
|
|
101
|
-
any(
|
|
101
|
+
any(
|
|
102
|
+
err.message.startswith("At concreteType:")
|
|
103
|
+
for err in result.errors
|
|
104
|
+
)
|
|
102
105
|
)
|
|
@@ -160,12 +160,16 @@ class TupleRestfulConverter:
|
|
|
160
160
|
return [convert(item) for item in value]
|
|
161
161
|
|
|
162
162
|
if isinstance(value, dict):
|
|
163
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
convert(key): convert(item) for key, item in value.items()
|
|
165
|
+
}
|
|
164
166
|
|
|
165
167
|
from .Tuple import Tuple
|
|
166
168
|
|
|
167
169
|
if isinstance(value, Tuple):
|
|
168
|
-
return cls.tupleToRestfulJsonDict(
|
|
170
|
+
return cls.tupleToRestfulJsonDict(
|
|
171
|
+
value, includeNones, includeFalse
|
|
172
|
+
)
|
|
169
173
|
|
|
170
174
|
if isinstance(value, WKBElement):
|
|
171
175
|
return convertFromWkbElement(value)
|
|
@@ -202,7 +206,10 @@ class TupleRestfulConverter:
|
|
|
202
206
|
else prefix
|
|
203
207
|
)
|
|
204
208
|
detail = validationError.message
|
|
205
|
-
if
|
|
209
|
+
if (
|
|
210
|
+
validationError.message.startswith("At ")
|
|
211
|
+
and ": " in validationError.message
|
|
212
|
+
):
|
|
206
213
|
detail = validationError.message.split(": ", 1)[1]
|
|
207
214
|
prefixed.append(
|
|
208
215
|
cls._makeError(field_path, detail, validationError.exc_type)
|
|
@@ -334,8 +341,12 @@ class TupleRestfulConverter:
|
|
|
334
341
|
) -> _ConvertResult:
|
|
335
342
|
errors: list[TupleValidationError] = []
|
|
336
343
|
listItemTypeHint = get_args(typeHint)[0]
|
|
337
|
-
listItemExtraHint =
|
|
338
|
-
|
|
344
|
+
listItemExtraHint = (
|
|
345
|
+
get_args(extraTypeHint)[0] if extraTypeHint else None
|
|
346
|
+
)
|
|
347
|
+
listItemTypeValidator = cls._getFieldValidatorFromExtraHint(
|
|
348
|
+
listItemExtraHint
|
|
349
|
+
)
|
|
339
350
|
data: list[Any] = []
|
|
340
351
|
|
|
341
352
|
if hasattr(listItemTypeHint, "__tupleType__"):
|
|
@@ -356,7 +367,9 @@ class TupleRestfulConverter:
|
|
|
356
367
|
item, listItemTypeHint
|
|
357
368
|
)
|
|
358
369
|
data.append(child.tuple_)
|
|
359
|
-
errors.extend(
|
|
370
|
+
errors.extend(
|
|
371
|
+
cls._prefixValidationErrors(child.errors, item_path)
|
|
372
|
+
)
|
|
360
373
|
else:
|
|
361
374
|
for index, item in enumerate(value):
|
|
362
375
|
item_path = f"{field_path}[{index}]"
|
|
@@ -440,7 +453,9 @@ class TupleRestfulConverter:
|
|
|
440
453
|
typeHint: Any,
|
|
441
454
|
) -> _ConvertResult:
|
|
442
455
|
if hasattr(typeHint, "__tupleType__"):
|
|
443
|
-
child = cls.restfulJsonDictToTupleWithValidationResult(
|
|
456
|
+
child = cls.restfulJsonDictToTupleWithValidationResult(
|
|
457
|
+
value, typeHint
|
|
458
|
+
)
|
|
444
459
|
return _ConvertResult(
|
|
445
460
|
child.tuple_,
|
|
446
461
|
cls._prefixValidationErrors(child.errors, field_path),
|
|
@@ -587,7 +602,9 @@ class TupleRestfulConverter:
|
|
|
587
602
|
def restfulJsonDictToTupleWithValidation(
|
|
588
603
|
cls, jsonDict: dict[str, Any], TupleClass: Type[Tuple]
|
|
589
604
|
) -> Tuple:
|
|
590
|
-
result = cls.restfulJsonDictToTupleWithValidationResult(
|
|
605
|
+
result = cls.restfulJsonDictToTupleWithValidationResult(
|
|
606
|
+
jsonDict, TupleClass
|
|
607
|
+
)
|
|
591
608
|
if result.errors:
|
|
592
609
|
firstValidationError = result.errors[0]
|
|
593
610
|
raise firstValidationError.exc_type(firstValidationError.message)
|
|
@@ -30,9 +30,9 @@ class RestfulTestTuple(Tuple):
|
|
|
30
30
|
|
|
31
31
|
__tupleType__ = "flyingion.vortex.RestfulTestTuple"
|
|
32
32
|
aInt: int = TupleField()
|
|
33
|
-
aIntWithValidator: Annotated[
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
aIntWithValidator: Annotated[int, IntTupleFieldValidator(1, 100)] = (
|
|
34
|
+
TupleField()
|
|
35
|
+
)
|
|
36
36
|
aFloat: float = TupleField()
|
|
37
37
|
aString: str = TupleField()
|
|
38
38
|
aBoolTrue: bool = TupleField()
|
|
@@ -150,8 +150,12 @@ class TupleRestfulTest(unittest.TestCase):
|
|
|
150
150
|
def testOptionalDatetimeRoundTrip(self):
|
|
151
151
|
utc = pytz.UTC
|
|
152
152
|
origTuple = RestfulOptionalDatetimeTestTuple()
|
|
153
|
-
origTuple.requiredDate = datetime(
|
|
154
|
-
|
|
153
|
+
origTuple.requiredDate = datetime(
|
|
154
|
+
2024, 6, 15, 10, 30, 45, 123456, tzinfo=utc
|
|
155
|
+
)
|
|
156
|
+
origTuple.optionalDate = datetime(
|
|
157
|
+
2024, 6, 16, 11, 31, 46, 654321, tzinfo=utc
|
|
158
|
+
)
|
|
155
159
|
|
|
156
160
|
jsonDict = origTuple.tupleToRestfulJsonDict()
|
|
157
161
|
jsonDict = json.loads(json.dumps(jsonDict))
|
|
@@ -250,7 +254,9 @@ class TupleRestfulCollectingTest(unittest.TestCase):
|
|
|
250
254
|
self.assertIsNone(result.tuple_.aSubTuple.subInt)
|
|
251
255
|
self.assertEqual(len(result.errors), 1)
|
|
252
256
|
self.assertEqual(result.errors[0].field_path, "aSubTuple.subInt")
|
|
253
|
-
self.assertTrue(
|
|
257
|
+
self.assertTrue(
|
|
258
|
+
result.errors[0].message.startswith("At aSubTuple.subInt:")
|
|
259
|
+
)
|
|
254
260
|
|
|
255
261
|
def testStrictWrapperRaisesFirstCollectingError(self):
|
|
256
262
|
jsonDict = _validRestfulJsonDict()
|
|
@@ -262,6 +268,8 @@ class TupleRestfulCollectingTest(unittest.TestCase):
|
|
|
262
268
|
)
|
|
263
269
|
|
|
264
270
|
with self.assertRaises(result.errors[0].exc_type) as ctx:
|
|
265
|
-
Tuple.restfulJsonDictToTupleWithValidation(
|
|
271
|
+
Tuple.restfulJsonDictToTupleWithValidation(
|
|
272
|
+
jsonDict, RestfulTestTuple
|
|
273
|
+
)
|
|
266
274
|
|
|
267
275
|
self.assertEqual(str(ctx.exception), result.errors[0].message)
|
|
@@ -75,7 +75,7 @@ class _VortexConnectionRateLimit:
|
|
|
75
75
|
def __init__(self):
|
|
76
76
|
self._connections: list[_ConnectionDateSuccess] = []
|
|
77
77
|
self._peerConnectionLimitPerIp = None
|
|
78
|
-
self._newConnectionsPerWindow =
|
|
78
|
+
self._newConnectionsPerWindow = 20
|
|
79
79
|
self._rollingNewConnectionWindowSeconds = 10.0
|
|
80
80
|
|
|
81
81
|
def _hasHitPeerConnectionLimit(
|
|
@@ -109,8 +109,10 @@ class _VortexConnectionRateLimit:
|
|
|
109
109
|
now = datetime.now(pytz.utc)
|
|
110
110
|
self._connections = list(
|
|
111
111
|
filter(
|
|
112
|
-
lambda item: (
|
|
113
|
-
|
|
112
|
+
lambda item: (
|
|
113
|
+
(now - item.dateTime).total_seconds()
|
|
114
|
+
< self._rollingNewConnectionWindowSeconds
|
|
115
|
+
),
|
|
114
116
|
self._connections,
|
|
115
117
|
)
|
|
116
118
|
)
|
|
@@ -355,7 +357,8 @@ class _VortexFactoryVortexInfo:
|
|
|
355
357
|
def getRemoteClientVortexInfos(self) -> List[VortexInfo]:
|
|
356
358
|
return [
|
|
357
359
|
VortexInfo(name=name, uuid=uuid)
|
|
358
|
-
for name,
|
|
360
|
+
for name, infos in self.__remoteVortexUuidsByName.items()
|
|
361
|
+
for _vortex, uuid in infos
|
|
359
362
|
]
|
|
360
363
|
|
|
361
364
|
@property
|
|
@@ -794,6 +797,12 @@ class VortexFactory:
|
|
|
794
797
|
def getRemoteClientVortexInfos(cls) -> List[VortexInfo]:
|
|
795
798
|
return cls.__vortexInfoState.getRemoteClientVortexInfos
|
|
796
799
|
|
|
800
|
+
@classmethod
|
|
801
|
+
def getRemoteVortexInfos(
|
|
802
|
+
cls, name=None, uuid=None
|
|
803
|
+
) -> list[tuple[VortexABC, str]]:
|
|
804
|
+
return cls.__vortexInfoState.getRemoteVortexInfos(name=name, uuid=uuid)
|
|
805
|
+
|
|
797
806
|
@classmethod
|
|
798
807
|
def getRemoteVortexUuids(cls) -> set[str]:
|
|
799
808
|
return cls.__vortexInfoState.getRemoteVortexUuids
|
|
@@ -97,7 +97,9 @@ class VortexPayloadProtocol(Protocol, metaclass=ABCMeta):
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
elif isinstance(reasonFailure.value, ConnectionLost):
|
|
100
|
-
self._logger.info(
|
|
100
|
+
self._logger.info(
|
|
101
|
+
"Connection to other end lost (We may be shutting down)"
|
|
102
|
+
)
|
|
101
103
|
|
|
102
104
|
else:
|
|
103
105
|
self._logger.error("Closed with error")
|
|
@@ -135,7 +137,9 @@ class VortexPayloadProtocol(Protocol, metaclass=ABCMeta):
|
|
|
135
137
|
vortexMsg = self._vortexMsgsQueue.popleft()
|
|
136
138
|
|
|
137
139
|
try:
|
|
138
|
-
payloadEnvelope = yield PayloadEnvelope().fromVortexMsgDefer(
|
|
140
|
+
payloadEnvelope = yield PayloadEnvelope().fromVortexMsgDefer(
|
|
141
|
+
vortexMsg
|
|
142
|
+
)
|
|
139
143
|
|
|
140
144
|
if payloadEnvelope.isEmpty():
|
|
141
145
|
self._processServerInfoPayload(payloadEnvelope)
|
|
@@ -43,7 +43,7 @@ VortexServer
|
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
45
|
HEART_BEAT_PERIOD = 10.0
|
|
46
|
-
HEART_BEAT_TIMEOUT =
|
|
46
|
+
HEART_BEAT_TIMEOUT = 60.0
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class VortexServer(VortexABC):
|
|
@@ -153,13 +153,15 @@ class VortexServer(VortexABC):
|
|
|
153
153
|
lambda: self._sessionExpired(httpSession.uid)
|
|
154
154
|
)
|
|
155
155
|
|
|
156
|
-
# Update the connection dict in the sessions object
|
|
157
156
|
httpSessionConns = VortexSessionI(httpSession).connections
|
|
157
|
+
for oldConn in list(httpSessionConns.values()):
|
|
158
|
+
if oldConn is not vortexConnection:
|
|
159
|
+
oldConn.close()
|
|
158
160
|
httpSessionConns[vortexUuid] = vortexConnection
|
|
159
161
|
|
|
160
|
-
# Get the old connection if it exists
|
|
161
162
|
if vortexUuid in self._connectionByVortexUuid:
|
|
162
|
-
self._connectionByVortexUuid[vortexUuid]
|
|
163
|
+
if self._connectionByVortexUuid[vortexUuid] is not vortexConnection:
|
|
164
|
+
self._connectionByVortexUuid[vortexUuid].close()
|
|
163
165
|
|
|
164
166
|
# Update the _connectionsByvortexUuid
|
|
165
167
|
self._connectionByVortexUuid[vortexUuid] = vortexConnection
|
|
@@ -335,7 +337,6 @@ class VortexServer(VortexABC):
|
|
|
335
337
|
|
|
336
338
|
for conn in conns:
|
|
337
339
|
for vortexMsg in vortexMsgs:
|
|
338
|
-
|
|
339
340
|
# noinspection PyProtectedMember
|
|
340
341
|
if VortexFactory._DEBUG_LOGGING:
|
|
341
342
|
logger.debug(
|
|
@@ -73,12 +73,12 @@ class VortexServerConnection(VortexConnectionABC):
|
|
|
73
73
|
|
|
74
74
|
beatTimeout = (
|
|
75
75
|
datetime.now(pytz.utc) - self._lastHeartBeatTime
|
|
76
|
-
).
|
|
76
|
+
).total_seconds() > HEART_BEAT_TIMEOUT
|
|
77
77
|
|
|
78
78
|
# If we've been asleep, then make note of that (VM suspended)
|
|
79
79
|
checkTimout = (
|
|
80
80
|
datetime.now(pytz.utc) - self._lastHeartBeatCheckTime
|
|
81
|
-
).
|
|
81
|
+
).total_seconds() > HEART_BEAT_TIMEOUT
|
|
82
82
|
|
|
83
83
|
# Mark that we've just checked it
|
|
84
84
|
self._lastHeartBeatCheckTime = datetime.now(pytz.utc)
|
|
@@ -267,13 +267,6 @@ class VortexWebSocketUpgradeResource(resource.Resource):
|
|
|
267
267
|
return b"upgrade" in connection and upgrade == b"websocket"
|
|
268
268
|
|
|
269
269
|
def _renderGet(self, request):
|
|
270
|
-
from vortex.VortexFactory import VortexFactory
|
|
271
|
-
|
|
272
|
-
if not VortexFactory.canConnect(str(request.client.host)):
|
|
273
|
-
request.setResponseCode(503)
|
|
274
|
-
request.setHeader("Retry-After", str(max(10, int(random() * 100))))
|
|
275
|
-
return b"Rejected due to new connections throttling."
|
|
276
|
-
|
|
277
270
|
request.setResponseCode(200)
|
|
278
271
|
request.setHeader(b"Content-Type", b"application/json")
|
|
279
272
|
request.setHeader(b"Cache-Control", b"no-store")
|
|
@@ -54,7 +54,6 @@ class VortexSession:
|
|
|
54
54
|
self.vortexName: str = vortexName
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
|
|
58
57
|
class VortexTcpSession:
|
|
59
58
|
"""Minimal Twisted-session-compatible stub for TCP connections.
|
|
60
59
|
|
|
@@ -101,6 +100,7 @@ class VortexTcpSession:
|
|
|
101
100
|
# Helpers
|
|
102
101
|
# ---------------------------------------------------------------------------
|
|
103
102
|
|
|
103
|
+
|
|
104
104
|
def extractVortexIp(request) -> str:
|
|
105
105
|
"""Return the real client IP from a Twisted HTTP request.
|
|
106
106
|
|
|
@@ -216,5 +216,7 @@ def _callableLabel(callable_) -> str:
|
|
|
216
216
|
return f"{module}.{callable_.__qualname__}"
|
|
217
217
|
if hasattr(callable_, "__self__") and hasattr(callable_, "__func__"):
|
|
218
218
|
cls = callable_.__self__.__class__
|
|
219
|
-
return
|
|
219
|
+
return (
|
|
220
|
+
f"{cls.__module__}.{cls.__qualname__}.{callable_.__func__.__name__}"
|
|
221
|
+
)
|
|
220
222
|
return repr(callable_)
|