vortexpy 4.2.0__tar.gz → 4.2.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.
- {vortexpy-4.2.0/vortexpy.egg-info → vortexpy-4.2.1}/PKG-INFO +1 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/pyproject.toml +1 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/DeferUtil.py +7 -24
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/Jsonable.py +4 -2
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadEndpoint.py +26 -8
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadEndpointTest.py +1 -3
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadEnvelope.py +9 -9
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadIO.py +9 -2
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadResponse.py +7 -3
- vortexpy-4.2.1/vortex/PayloadResponse_test.py +42 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/SerialiseUtil.py +8 -0
- vortexpy-4.2.1/vortex/SerialiseUtil_test.py +123 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/Tuple.py +2 -3
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +4 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleRestful.py +110 -8
- vortexpy-4.2.0/vortex/TupleRestfulTest.py → vortexpy-4.2.1/vortex/TupleRestful_test.py +69 -7
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexAccessControlContextABC.py +1 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientTcp.py +4 -19
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientWebsocketFactory.py +5 -29
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexFactory.py +20 -56
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexPayloadProtocol.py +6 -2
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexPermission.py +1 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexPermissionError.py +1 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServer.py +0 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexSession.py +4 -2
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexTcpMemoryLeakPayloadTest.py +24 -24
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexWritePushProducer.py +0 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/__init__.py +1 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLiveLockManager.py +1 -5
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoader.py +28 -16
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoaderDelegate.py +14 -4
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/TupleChangeEventBus.py +7 -8
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleActionProcessor.py +74 -41
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleActionProcessorProxy.py +8 -10
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleActionProcessorTestHandler.py +3 -3
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableCache.py +3 -5
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableHandler.py +28 -20
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableProxyHandler.py +14 -5
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObserverClient.py +3 -6
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataProviderABC.py +3 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/rpc/RPC.py +70 -38
- vortexpy-4.2.1/vortex/rpc/RPC_test.py +114 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/OrmCrudHandler.py +2 -5
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/storage/TupleStorageSqlite.py +3 -3
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/tuple_json_schema_builder.py +3 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/tuple_json_schema_test.py +3 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/twisted_runtime_metrics.py +76 -31
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/twisted_runtime_metrics_test.py +6 -4
- {vortexpy-4.2.0 → vortexpy-4.2.1/vortexpy.egg-info}/PKG-INFO +1 -1
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/SOURCES.txt +4 -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.2.1}/README.md +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/setup.cfg +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/Payload.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TestTuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleAction.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexUtil.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/mypy_plugin.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/py.typed +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulHttpClientTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulResourceTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.2.0 → vortexpy-4.2.1}/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.2.
|
|
7
|
+
version = "4.2.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."
|
|
@@ -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
|
|
|
@@ -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
|
)
|
|
@@ -14,6 +14,7 @@ Implementation module; public interface remains on vortex.Tuple.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import enum
|
|
17
18
|
import re
|
|
18
19
|
from dataclasses import dataclass, field
|
|
19
20
|
from datetime import datetime
|
|
@@ -160,12 +161,16 @@ class TupleRestfulConverter:
|
|
|
160
161
|
return [convert(item) for item in value]
|
|
161
162
|
|
|
162
163
|
if isinstance(value, dict):
|
|
163
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
convert(key): convert(item) for key, item in value.items()
|
|
166
|
+
}
|
|
164
167
|
|
|
165
168
|
from .Tuple import Tuple
|
|
166
169
|
|
|
167
170
|
if isinstance(value, Tuple):
|
|
168
|
-
return cls.tupleToRestfulJsonDict(
|
|
171
|
+
return cls.tupleToRestfulJsonDict(
|
|
172
|
+
value, includeNones, includeFalse
|
|
173
|
+
)
|
|
169
174
|
|
|
170
175
|
if isinstance(value, WKBElement):
|
|
171
176
|
return convertFromWkbElement(value)
|
|
@@ -202,7 +207,10 @@ class TupleRestfulConverter:
|
|
|
202
207
|
else prefix
|
|
203
208
|
)
|
|
204
209
|
detail = validationError.message
|
|
205
|
-
if
|
|
210
|
+
if (
|
|
211
|
+
validationError.message.startswith("At ")
|
|
212
|
+
and ": " in validationError.message
|
|
213
|
+
):
|
|
206
214
|
detail = validationError.message.split(": ", 1)[1]
|
|
207
215
|
prefixed.append(
|
|
208
216
|
cls._makeError(field_path, detail, validationError.exc_type)
|
|
@@ -256,6 +264,73 @@ class TupleRestfulConverter:
|
|
|
256
264
|
|
|
257
265
|
return True
|
|
258
266
|
|
|
267
|
+
@classmethod
|
|
268
|
+
def _isStrEnumType(cls, enumType: type[enum.Enum]) -> bool:
|
|
269
|
+
return issubclass(enumType, str) and issubclass(enumType, enum.Enum)
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def _isIntEnumType(cls, enumType: type[enum.Enum]) -> bool:
|
|
273
|
+
return (
|
|
274
|
+
issubclass(enumType, int)
|
|
275
|
+
and issubclass(enumType, enum.Enum)
|
|
276
|
+
and not issubclass(enumType, bool)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def _enumTypeFromHint(cls, typeHint: Any) -> type[enum.Enum] | None:
|
|
281
|
+
if isinstance(typeHint, type) and issubclass(typeHint, enum.Enum):
|
|
282
|
+
if cls._isStrEnumType(typeHint) or cls._isIntEnumType(typeHint):
|
|
283
|
+
return typeHint
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
for unionArg in get_args(typeHint):
|
|
287
|
+
if unionArg is type(None):
|
|
288
|
+
continue
|
|
289
|
+
if isinstance(unionArg, type) and issubclass(unionArg, enum.Enum):
|
|
290
|
+
if cls._isStrEnumType(unionArg) or cls._isIntEnumType(unionArg):
|
|
291
|
+
return unionArg
|
|
292
|
+
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def _tryCoerceEnumValue(
|
|
297
|
+
cls,
|
|
298
|
+
field_path: str,
|
|
299
|
+
fieldName: str,
|
|
300
|
+
TupleClass: Type[Tuple],
|
|
301
|
+
typeHint: Any,
|
|
302
|
+
value: Any,
|
|
303
|
+
) -> tuple[Any, TupleValidationError | None]:
|
|
304
|
+
if value is None:
|
|
305
|
+
return value, None
|
|
306
|
+
|
|
307
|
+
enumType = cls._enumTypeFromHint(typeHint)
|
|
308
|
+
if enumType is None:
|
|
309
|
+
return value, None
|
|
310
|
+
|
|
311
|
+
if isinstance(value, enumType):
|
|
312
|
+
return value, None
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
if cls._isStrEnumType(enumType) and isinstance(value, str):
|
|
316
|
+
return enumType(value), None
|
|
317
|
+
|
|
318
|
+
if (
|
|
319
|
+
cls._isIntEnumType(enumType)
|
|
320
|
+
and isinstance(value, int)
|
|
321
|
+
and not isinstance(value, bool)
|
|
322
|
+
):
|
|
323
|
+
return enumType(value), None
|
|
324
|
+
except ValueError:
|
|
325
|
+
return value, cls._makeError(
|
|
326
|
+
field_path,
|
|
327
|
+
f"parsing {TupleClass}, field {fieldName}"
|
|
328
|
+
f" is not a valid {enumType.__name__} value, got {value!r}",
|
|
329
|
+
ValueError,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return value, None
|
|
333
|
+
|
|
259
334
|
@classmethod
|
|
260
335
|
def _nestedTupleTypeFromHint(cls, typeHint: Any) -> Any | None:
|
|
261
336
|
"""Return nested tuple class for a field hint, including ``X | None`` unions."""
|
|
@@ -334,8 +409,12 @@ class TupleRestfulConverter:
|
|
|
334
409
|
) -> _ConvertResult:
|
|
335
410
|
errors: list[TupleValidationError] = []
|
|
336
411
|
listItemTypeHint = get_args(typeHint)[0]
|
|
337
|
-
listItemExtraHint =
|
|
338
|
-
|
|
412
|
+
listItemExtraHint = (
|
|
413
|
+
get_args(extraTypeHint)[0] if extraTypeHint else None
|
|
414
|
+
)
|
|
415
|
+
listItemTypeValidator = cls._getFieldValidatorFromExtraHint(
|
|
416
|
+
listItemExtraHint
|
|
417
|
+
)
|
|
339
418
|
data: list[Any] = []
|
|
340
419
|
|
|
341
420
|
if hasattr(listItemTypeHint, "__tupleType__"):
|
|
@@ -356,10 +435,22 @@ class TupleRestfulConverter:
|
|
|
356
435
|
item, listItemTypeHint
|
|
357
436
|
)
|
|
358
437
|
data.append(child.tuple_)
|
|
359
|
-
errors.extend(
|
|
438
|
+
errors.extend(
|
|
439
|
+
cls._prefixValidationErrors(child.errors, item_path)
|
|
440
|
+
)
|
|
360
441
|
else:
|
|
361
442
|
for index, item in enumerate(value):
|
|
362
443
|
item_path = f"{field_path}[{index}]"
|
|
444
|
+
item, enumError = cls._tryCoerceEnumValue(
|
|
445
|
+
item_path,
|
|
446
|
+
fieldName,
|
|
447
|
+
TupleClass,
|
|
448
|
+
listItemTypeHint,
|
|
449
|
+
item,
|
|
450
|
+
)
|
|
451
|
+
if enumError:
|
|
452
|
+
errors.append(enumError)
|
|
453
|
+
continue
|
|
363
454
|
itemError = cls._checkType(
|
|
364
455
|
TupleClass,
|
|
365
456
|
item_path,
|
|
@@ -440,7 +531,9 @@ class TupleRestfulConverter:
|
|
|
440
531
|
typeHint: Any,
|
|
441
532
|
) -> _ConvertResult:
|
|
442
533
|
if hasattr(typeHint, "__tupleType__"):
|
|
443
|
-
child = cls.restfulJsonDictToTupleWithValidationResult(
|
|
534
|
+
child = cls.restfulJsonDictToTupleWithValidationResult(
|
|
535
|
+
value, typeHint
|
|
536
|
+
)
|
|
444
537
|
return _ConvertResult(
|
|
445
538
|
child.tuple_,
|
|
446
539
|
cls._prefixValidationErrors(child.errors, field_path),
|
|
@@ -517,6 +610,13 @@ class TupleRestfulConverter:
|
|
|
517
610
|
):
|
|
518
611
|
value = datetime.strptime(value, ISO8601)
|
|
519
612
|
|
|
613
|
+
value, enumError = cls._tryCoerceEnumValue(
|
|
614
|
+
field_path, fieldName, TupleClass, typeHint, value
|
|
615
|
+
)
|
|
616
|
+
if enumError:
|
|
617
|
+
errors.append(enumError)
|
|
618
|
+
return _ConvertResult(_UNSET, errors)
|
|
619
|
+
|
|
520
620
|
typeError = cls._checkType(
|
|
521
621
|
TupleClass, field_path, fieldName, typeHint, value, validator
|
|
522
622
|
)
|
|
@@ -587,7 +687,9 @@ class TupleRestfulConverter:
|
|
|
587
687
|
def restfulJsonDictToTupleWithValidation(
|
|
588
688
|
cls, jsonDict: dict[str, Any], TupleClass: Type[Tuple]
|
|
589
689
|
) -> Tuple:
|
|
590
|
-
result = cls.restfulJsonDictToTupleWithValidationResult(
|
|
690
|
+
result = cls.restfulJsonDictToTupleWithValidationResult(
|
|
691
|
+
jsonDict, TupleClass
|
|
692
|
+
)
|
|
591
693
|
if result.errors:
|
|
592
694
|
firstValidationError = result.errors[0]
|
|
593
695
|
raise firstValidationError.exc_type(firstValidationError.message)
|