vortexpy 4.2.0__tar.gz → 4.3.0__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.2.0/vortexpy.egg-info → vortexpy-4.3.0}/PKG-INFO +1 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/pyproject.toml +1 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/DeferUtil.py +7 -24
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/Jsonable.py +4 -2
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadEndpoint.py +26 -8
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadEndpointTest.py +1 -3
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadEnvelope.py +9 -9
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadIO.py +9 -2
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadResponse.py +7 -3
- vortexpy-4.3.0/vortex/PayloadResponse_test.py +42 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/SerialiseUtil.py +8 -0
- vortexpy-4.3.0/vortex/SerialiseUtil_test.py +123 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/Tuple.py +34 -3
- vortexpy-4.3.0/vortex/TupleBson.py +182 -0
- vortexpy-4.3.0/vortex/TupleBson_test.py +353 -0
- vortexpy-4.2.0/vortex/TupleRestful.py → vortexpy-4.3.0/vortex/TupleDocumentConverter.py +152 -89
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +4 -1
- vortexpy-4.3.0/vortex/TupleRestful.py +134 -0
- vortexpy-4.2.0/vortex/TupleRestfulTest.py → vortexpy-4.3.0/vortex/TupleRestful_test.py +69 -7
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexAccessControlContextABC.py +1 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientTcp.py +4 -19
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientWebsocketFactory.py +5 -29
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexFactory.py +20 -56
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexPayloadProtocol.py +6 -2
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexPermission.py +1 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexPermissionError.py +1 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServer.py +0 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexSession.py +4 -2
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +24 -24
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexUtil.py +17 -2
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexWritePushProducer.py +0 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/__init__.py +1 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLiveLockManager.py +1 -5
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoader.py +28 -16
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderDelegate.py +14 -4
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBus.py +7 -8
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessor.py +74 -41
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorProxy.py +8 -10
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorTestHandler.py +3 -3
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableCache.py +3 -5
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableHandler.py +28 -20
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableProxyHandler.py +14 -5
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObserverClient.py +3 -6
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataProviderABC.py +3 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/rpc/RPC.py +70 -38
- vortexpy-4.3.0/vortex/rpc/RPC_test.py +114 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/OrmCrudHandler.py +2 -5
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/storage/TupleStorageSqlite.py +3 -3
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +3 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +3 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics.py +227 -27
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics_test.py +75 -5
- {vortexpy-4.2.0 → vortexpy-4.3.0/vortexpy.egg-info}/PKG-INFO +1 -1
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/SOURCES.txt +7 -11
- vortexpy-4.2.0/LICENSE +0 -22
- vortexpy-4.2.0/vortex/DeferUtil_test.py +0 -86
- vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry.py +0 -150
- vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry_test.py +0 -75
- vortexpy-4.2.0/vortex/MtlsServerSslOptions.py +0 -176
- vortexpy-4.2.0/vortex/VortexFactory_test.py +0 -114
- vortexpy-4.2.0/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -20
- vortexpy-4.2.0/vortex/handler/TupleActionPushOffline.py +0 -180
- vortexpy-4.2.0/vortex/handler/TupleActionPushOffline_test.py +0 -133
- vortexpy-4.2.0/vortex/storage/TupleActionStorageSqlite.py +0 -156
- {vortexpy-4.2.0 → vortexpy-4.3.0}/README.md +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/setup.cfg +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/Payload.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TestTuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleAction.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/mypy_plugin.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/py.typed +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClientTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulResourceTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.2.0 → vortexpy-4.3.0}/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.
|
|
7
|
+
version = "4.3.0"
|
|
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."
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import inspect
|
|
10
10
|
import logging
|
|
11
|
-
import time
|
|
12
11
|
from collections.abc import Callable
|
|
13
12
|
from typing import ParamSpec, TypeVar
|
|
14
13
|
|
|
@@ -210,27 +209,7 @@ def deferToThreadWrapWithLogger(
|
|
|
210
209
|
"Deferring to a thread can only be done from the main thread"
|
|
211
210
|
)
|
|
212
211
|
|
|
213
|
-
|
|
214
|
-
startMonotonic = time.monotonic()
|
|
215
|
-
try:
|
|
216
|
-
return funcToWrap(*runArgs, **runKwargs)
|
|
217
|
-
finally:
|
|
218
|
-
durationMs = (time.monotonic() - startMonotonic) * 1000.0
|
|
219
|
-
from vortex.twisted_runtime_metrics import (
|
|
220
|
-
STACK_CAPTURE_THRESHOLD_MS,
|
|
221
|
-
TwistedRuntimeMetrics,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
methodFrame = None
|
|
225
|
-
if durationMs >= STACK_CAPTURE_THRESHOLD_MS:
|
|
226
|
-
methodFrame = TwistedRuntimeMetrics._captureWorkerThreadFrame()
|
|
227
|
-
TwistedRuntimeMetrics._collector()._recordThreadedCall(
|
|
228
|
-
funcToWrap.__qualname__,
|
|
229
|
-
durationMs,
|
|
230
|
-
methodFrame,
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
d: Deferred[_R] = deferToThread(timedFuncToRun, *args, **kwargs)
|
|
212
|
+
d: Deferred[_R] = deferToThread(funcToWrap, *args, **kwargs)
|
|
234
213
|
d.addCallback(checkForInlineCallbackYield)
|
|
235
214
|
d.addErrback(vortexLogFailure, loggerArg, consumeError=consumeError)
|
|
236
215
|
return d
|
|
@@ -246,9 +225,13 @@ def isMainThread():
|
|
|
246
225
|
|
|
247
226
|
def noMainThread():
|
|
248
227
|
if isMainThread():
|
|
249
|
-
raise Exception(
|
|
228
|
+
raise Exception(
|
|
229
|
+
"Blocking operations shouldn't occur in the reactors thread."
|
|
230
|
+
)
|
|
250
231
|
|
|
251
232
|
|
|
252
233
|
def yesMainThread():
|
|
253
234
|
if not isMainThread():
|
|
254
|
-
raise Exception(
|
|
235
|
+
raise Exception(
|
|
236
|
+
"Async operations must occur in the reactors main thread."
|
|
237
|
+
)
|
|
@@ -107,9 +107,11 @@ class Jsonable(object):
|
|
|
107
107
|
try:
|
|
108
108
|
valueType = V_NULL if value is None else toRapuiType(value)
|
|
109
109
|
except KeyError as e:
|
|
110
|
+
unsupportedType = e.args[0] if e.args else value.__class__
|
|
110
111
|
raise KeyError(
|
|
111
|
-
"%
|
|
112
|
-
|
|
112
|
+
"%r field name is %r, type is %r"
|
|
113
|
+
% (unsupportedType, name, value.__class__)
|
|
114
|
+
) from e
|
|
113
115
|
|
|
114
116
|
# Payloads and Tuples
|
|
115
117
|
if self.__isRawJsonableField(name):
|
|
@@ -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,
|
|
@@ -69,6 +71,7 @@ class PayloadEndpoint:
|
|
|
69
71
|
acceptOnlyFromVortex: Optional[Union[str, tuple]] = None,
|
|
70
72
|
ignoreFromVortex: Optional[Union[str, tuple]] = None,
|
|
71
73
|
requiredPermissions: Optional[PermissionsType] = None,
|
|
74
|
+
logProcessException: bool = True,
|
|
72
75
|
) -> None:
|
|
73
76
|
"""
|
|
74
77
|
:param filt: The filter to match against payloads
|
|
@@ -80,8 +83,13 @@ class PayloadEndpoint:
|
|
|
80
83
|
the caller's session must hold. Pass an empty frozenset to
|
|
81
84
|
explicitly declare "no permissions required". Omitting the
|
|
82
85
|
parameter (``None``) emits a deprecation warning.
|
|
86
|
+
:param logProcessException: When True, handler exceptions log at ERROR.
|
|
87
|
+
When False, the same events log at DEBUG with neutral wording.
|
|
83
88
|
"""
|
|
84
|
-
self._requiredPermissions: Optional[PermissionsType] =
|
|
89
|
+
self._requiredPermissions: Optional[PermissionsType] = (
|
|
90
|
+
requiredPermissions
|
|
91
|
+
)
|
|
92
|
+
self._logProcessException = logProcessException
|
|
85
93
|
|
|
86
94
|
if requiredPermissions is None:
|
|
87
95
|
logger.warning(self._NO_PERMISSIONS_WARNING, filt)
|
|
@@ -89,9 +97,11 @@ class PayloadEndpoint:
|
|
|
89
97
|
callableDesc = (
|
|
90
98
|
f"PayloadEndpoint filt={filt} callback={_callableLabel(callable_)}"
|
|
91
99
|
)
|
|
92
|
-
self._callableAcceptsAccessControlContext =
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
self._callableAcceptsAccessControlContext = (
|
|
101
|
+
checkAccessControlContextSignature(
|
|
102
|
+
callable_,
|
|
103
|
+
callableDescription=callableDesc,
|
|
104
|
+
)
|
|
95
105
|
)
|
|
96
106
|
checkNoHttpSessionInSignature(callable_, callableDesc)
|
|
97
107
|
|
|
@@ -168,7 +178,10 @@ class PayloadEndpoint:
|
|
|
168
178
|
return False
|
|
169
179
|
|
|
170
180
|
# Filter for the backend vortexes.
|
|
171
|
-
if
|
|
181
|
+
if (
|
|
182
|
+
self._acceptOnlyFromVortex
|
|
183
|
+
and vortexName not in self._acceptOnlyFromVortex
|
|
184
|
+
):
|
|
172
185
|
return False
|
|
173
186
|
|
|
174
187
|
def removeUnhashable(filt):
|
|
@@ -212,7 +225,9 @@ class PayloadEndpoint:
|
|
|
212
225
|
if not self.check(payloadEnvelope, vortexName):
|
|
213
226
|
return
|
|
214
227
|
|
|
215
|
-
if self.PERMITTER and not self.PERMITTER.check(
|
|
228
|
+
if self.PERMITTER and not self.PERMITTER.check(
|
|
229
|
+
payloadEnvelope, vortexSession
|
|
230
|
+
):
|
|
216
231
|
logger.debug("Permission failed for %s", payloadEnvelope.filt)
|
|
217
232
|
return
|
|
218
233
|
|
|
@@ -250,7 +265,10 @@ class PayloadEndpoint:
|
|
|
250
265
|
accessControlContext = NO_AUTH_ACCESS_CONTROL_CONTEXT
|
|
251
266
|
if self._callableAcceptsAccessControlContext and auth and vortexSession:
|
|
252
267
|
raw_ctx = yield maybeDeferred(
|
|
253
|
-
auth.getAccessControlContext,
|
|
268
|
+
auth.getAccessControlContext,
|
|
269
|
+
vortexSession,
|
|
270
|
+
vortexName,
|
|
271
|
+
vortexUuid,
|
|
254
272
|
)
|
|
255
273
|
if not isinstance(raw_ctx, VortexAccessControlContextABC):
|
|
256
274
|
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):
|
|
@@ -115,8 +115,15 @@ class PayloadIO(object):
|
|
|
115
115
|
except Exception as e:
|
|
116
116
|
logger.exception(e)
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
if endpoint._logProcessException:
|
|
119
|
+
vortexLogFailure(failure, logger)
|
|
120
|
+
logger.error(payloadEnvelope.filt)
|
|
121
|
+
else:
|
|
122
|
+
logger.debug(
|
|
123
|
+
"Payload handler response for filt=%s: %s",
|
|
124
|
+
payloadEnvelope.filt,
|
|
125
|
+
failure.getErrorMessage(),
|
|
126
|
+
)
|
|
120
127
|
|
|
121
128
|
def callback(value, blocking=False):
|
|
122
129
|
# A blocking call taking more than 100ms is BAD
|
|
@@ -67,7 +67,7 @@ class PayloadResponse(Deferred):
|
|
|
67
67
|
destVortexUuid: Optional[str] = None,
|
|
68
68
|
timeout: Optional[float] = None,
|
|
69
69
|
resultCheck=True,
|
|
70
|
-
|
|
70
|
+
logTimeoutException=True,
|
|
71
71
|
) -> None:
|
|
72
72
|
"""Constructor
|
|
73
73
|
|
|
@@ -81,6 +81,8 @@ class PayloadResponse(Deferred):
|
|
|
81
81
|
:param timeout The timeout to wait for a response
|
|
82
82
|
:param resultCheck Should the response payload.result be checked, if it fails
|
|
83
83
|
it errback will be called.
|
|
84
|
+
:param logTimeoutException: When True, payload timeouts log at ERROR.
|
|
85
|
+
When False, timeouts log at DEBUG with neutral wording.
|
|
84
86
|
"""
|
|
85
87
|
Deferred.__init__(self)
|
|
86
88
|
|
|
@@ -88,7 +90,7 @@ class PayloadResponse(Deferred):
|
|
|
88
90
|
timeout = self.TIMEOUT
|
|
89
91
|
|
|
90
92
|
self._resultCheck = resultCheck
|
|
91
|
-
self.
|
|
93
|
+
self._logTimeoutException = logTimeoutException
|
|
92
94
|
|
|
93
95
|
# uuid4 can have collisions
|
|
94
96
|
self._messageId = str(uuid4()) + str(PayloadResponse.__SEQ)
|
|
@@ -142,8 +144,10 @@ class PayloadResponse(Deferred):
|
|
|
142
144
|
self._endpoint.shutdown()
|
|
143
145
|
self._endpoint = None
|
|
144
146
|
|
|
145
|
-
if self.
|
|
147
|
+
if self._logTimeoutException:
|
|
146
148
|
logger.error("Timed out for payload %s", self._filt)
|
|
149
|
+
else:
|
|
150
|
+
logger.debug("Payload response timed out for %s", self._filt)
|
|
147
151
|
self._status = self.TIMED_OUT
|
|
148
152
|
return failure
|
|
149
153
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from twisted.internet.defer import TimeoutError
|
|
5
|
+
from twisted.python.failure import Failure
|
|
6
|
+
|
|
7
|
+
from vortex.PayloadEnvelope import PayloadEnvelope
|
|
8
|
+
from vortex.PayloadResponse import PayloadResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PayloadResponseLoggingTest(unittest.TestCase):
|
|
12
|
+
@patch("vortex.PayloadResponse.VortexFactory.sendVortexMsg")
|
|
13
|
+
@patch("vortex.PayloadResponse.logger")
|
|
14
|
+
def testTimeoutLogsDebugWhenLogTimeoutExceptionFalse(
|
|
15
|
+
self, mockLogger: MagicMock, _mockSend: MagicMock
|
|
16
|
+
) -> None:
|
|
17
|
+
pr = PayloadResponse(
|
|
18
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
19
|
+
timeout=30.0,
|
|
20
|
+
logTimeoutException=False,
|
|
21
|
+
)
|
|
22
|
+
pr._timedOut(Failure(TimeoutError("timed out")))
|
|
23
|
+
mockLogger.error.assert_not_called()
|
|
24
|
+
mockLogger.debug.assert_called_once()
|
|
25
|
+
|
|
26
|
+
@patch("vortex.PayloadResponse.VortexFactory.sendVortexMsg")
|
|
27
|
+
@patch("vortex.PayloadResponse.logger")
|
|
28
|
+
def testTimeoutLogsErrorWhenLogTimeoutExceptionTrue(
|
|
29
|
+
self, mockLogger: MagicMock, _mockSend: MagicMock
|
|
30
|
+
) -> None:
|
|
31
|
+
pr = PayloadResponse(
|
|
32
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
33
|
+
timeout=30.0,
|
|
34
|
+
logTimeoutException=True,
|
|
35
|
+
)
|
|
36
|
+
pr._timedOut(Failure(TimeoutError("timed out")))
|
|
37
|
+
mockLogger.debug.assert_not_called()
|
|
38
|
+
mockLogger.error.assert_called_once()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
unittest.main()
|
|
@@ -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,13 @@ def toRapuiType(value):
|
|
|
267
268
|
if isinstance(value, PayloadEnvelope):
|
|
268
269
|
return T_RAPUI_PAYLOAD_ENVELOPE
|
|
269
270
|
|
|
271
|
+
if isinstance(value, enum.Enum):
|
|
272
|
+
if isinstance(value, str):
|
|
273
|
+
return T_STR
|
|
274
|
+
if isinstance(value, int):
|
|
275
|
+
return T_INT
|
|
276
|
+
raise KeyError(value.__class__)
|
|
277
|
+
|
|
270
278
|
valueClass = value.__class__
|
|
271
279
|
|
|
272
280
|
return TYPE_MAP_PY_TO_RAPUI[valueClass]
|
|
@@ -0,0 +1,123 @@
|
|
|
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) as errorInfo:
|
|
46
|
+
toRapuiType(SamplePlainEnumE.Alpha)
|
|
47
|
+
|
|
48
|
+
self.assertEqual(errorInfo.exception.args, (SamplePlainEnumE,))
|
|
49
|
+
|
|
50
|
+
def testToStrStrEnumMember(self) -> None:
|
|
51
|
+
self.assertEqual(toStr(SampleStrEnumE.CrewDatum), "crew-datum")
|
|
52
|
+
|
|
53
|
+
def testFromStrRoundTripStrEnumWireValue(self) -> None:
|
|
54
|
+
wireValue = toStr(SampleStrEnumE.CrewDatum)
|
|
55
|
+
self.assertEqual(fromStr(wireValue, T_STR), "crew-datum")
|
|
56
|
+
|
|
57
|
+
def testTupleSelectorSerialisesStrEnumSelectorValues(self) -> None:
|
|
58
|
+
selector = TupleSelector(
|
|
59
|
+
"peek_core_datum.DatumMappingTuple",
|
|
60
|
+
dict(
|
|
61
|
+
sourcePlugin="peek_plugin_switching_viewer",
|
|
62
|
+
datumPendingKey=SampleStrEnumE.CrewDatum,
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
jsonStr = selector.toJsonStr()
|
|
66
|
+
parsed = json.loads(jsonStr)
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
parsed["selector"]["datumPendingKey"],
|
|
69
|
+
"crew-datum",
|
|
70
|
+
)
|
|
71
|
+
self.assertEqual(parsed["selector"]["sourcePlugin"], "peek_plugin_switching_viewer")
|
|
72
|
+
|
|
73
|
+
def testTupleSelectorStrEnumRoundTrip(self) -> None:
|
|
74
|
+
original = TupleSelector(
|
|
75
|
+
"peek_core_datum.DatumMappingTuple",
|
|
76
|
+
dict(
|
|
77
|
+
sourcePlugin="peek_plugin_switching_viewer",
|
|
78
|
+
datumPendingKey=SampleStrEnumE.ViewJobDetail,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
restored = TupleSelector.fromJsonStr(original.toJsonStr())
|
|
82
|
+
self.assertEqual(restored.name, original.name)
|
|
83
|
+
self.assertEqual(
|
|
84
|
+
restored.selector["datumPendingKey"],
|
|
85
|
+
"view-job-detail-datum",
|
|
86
|
+
)
|
|
87
|
+
self.assertIsInstance(restored.selector["datumPendingKey"], str)
|
|
88
|
+
|
|
89
|
+
def testTupleSelectorSerialisesIntEnumSelectorValues(self) -> None:
|
|
90
|
+
selector = TupleSelector(
|
|
91
|
+
"test.ExampleTuple",
|
|
92
|
+
dict(priority=SampleIntEnumE.Two),
|
|
93
|
+
)
|
|
94
|
+
jsonStr = selector.toJsonStr()
|
|
95
|
+
parsed = json.loads(jsonStr)
|
|
96
|
+
self.assertEqual(
|
|
97
|
+
parsed["selector"]["priority"],
|
|
98
|
+
{"_fd": 2, "_ft": "int"},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def testTupleSelectorIntEnumRoundTrip(self) -> None:
|
|
102
|
+
original = TupleSelector(
|
|
103
|
+
"test.ExampleTuple",
|
|
104
|
+
dict(priority=SampleIntEnumE.One),
|
|
105
|
+
)
|
|
106
|
+
restored = TupleSelector.fromJsonStr(original.toJsonStr())
|
|
107
|
+
self.assertEqual(restored.selector["priority"], 1)
|
|
108
|
+
self.assertIsInstance(restored.selector["priority"], int)
|
|
109
|
+
|
|
110
|
+
def testTupleSelectorUnsupportedEnumFieldNameInError(self) -> None:
|
|
111
|
+
selector = TupleSelector(
|
|
112
|
+
"DashboardDataTimespanUpdatedTupleSelector",
|
|
113
|
+
dict(objectType=SamplePlainEnumE.Alpha),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
with self.assertRaises(KeyError) as errorInfo:
|
|
117
|
+
selector.toJsonStr()
|
|
118
|
+
|
|
119
|
+
self.assertEqual(
|
|
120
|
+
errorInfo.exception.args[0],
|
|
121
|
+
"%r field name is %r, type is %r"
|
|
122
|
+
% (SamplePlainEnumE, "objectType", SamplePlainEnumE),
|
|
123
|
+
)
|
|
@@ -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
|
|
|
@@ -384,6 +383,13 @@ class _TupleToPlainJsonMixin:
|
|
|
384
383
|
self, includeNones, includeFalse
|
|
385
384
|
)
|
|
386
385
|
|
|
386
|
+
def tupleToBsonJsonDict(self, includeNones=True, includeFalse=True):
|
|
387
|
+
from vortex.TupleBson import TupleBsonConverter
|
|
388
|
+
|
|
389
|
+
return TupleBsonConverter.tupleToBsonJsonDict(
|
|
390
|
+
self, includeNones, includeFalse
|
|
391
|
+
)
|
|
392
|
+
|
|
387
393
|
def __tupleToJsonDict(
|
|
388
394
|
self, includeNones=True, includeFalse=True, useShortNames=True
|
|
389
395
|
):
|
|
@@ -507,6 +513,31 @@ class _TupleToPlainJsonMixin:
|
|
|
507
513
|
jsonDict, TupleClass
|
|
508
514
|
)
|
|
509
515
|
|
|
516
|
+
@staticmethod
|
|
517
|
+
def bsonJsonDictToTupleWithValidation(
|
|
518
|
+
jsonDict: dict, TupleClass: Type["Tuple"]
|
|
519
|
+
) -> "Tuple":
|
|
520
|
+
"""BSON document dict to Tuple with validation.
|
|
521
|
+
|
|
522
|
+
This method uses the typings on the fields in a tuple to validate the
|
|
523
|
+
types of data.
|
|
524
|
+
"""
|
|
525
|
+
from vortex.TupleBson import TupleBsonConverter
|
|
526
|
+
|
|
527
|
+
return TupleBsonConverter.bsonJsonDictToTupleWithValidation(
|
|
528
|
+
jsonDict, TupleClass
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def bsonJsonDictToTupleWithValidationResult(
|
|
533
|
+
jsonDict: dict, TupleClass: Type["Tuple"]
|
|
534
|
+
) -> "TupleValidationResult":
|
|
535
|
+
from vortex.TupleBson import TupleBsonConverter
|
|
536
|
+
|
|
537
|
+
return TupleBsonConverter.bsonJsonDictToTupleWithValidationResult(
|
|
538
|
+
jsonDict, TupleClass
|
|
539
|
+
)
|
|
540
|
+
|
|
510
541
|
|
|
511
542
|
class Tuple(Jsonable, _TupleToPlainJsonMixin, _TupleToSqlaJsonMixin):
|
|
512
543
|
"""Tuple
|