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.
Files changed (123) hide show
  1. {vortexpy-4.2.0/vortexpy.egg-info → vortexpy-4.2.1}/PKG-INFO +1 -1
  2. {vortexpy-4.2.0 → vortexpy-4.2.1}/pyproject.toml +1 -1
  3. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/DeferUtil.py +7 -24
  4. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/Jsonable.py +4 -2
  5. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadEndpoint.py +26 -8
  6. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadEndpointTest.py +1 -3
  7. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadEnvelope.py +9 -9
  8. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadIO.py +9 -2
  9. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadResponse.py +7 -3
  10. vortexpy-4.2.1/vortex/PayloadResponse_test.py +42 -0
  11. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/SerialiseUtil.py +8 -0
  12. vortexpy-4.2.1/vortex/SerialiseUtil_test.py +123 -0
  13. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/Tuple.py +2 -3
  14. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +4 -1
  15. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleRestful.py +110 -8
  16. vortexpy-4.2.0/vortex/TupleRestfulTest.py → vortexpy-4.2.1/vortex/TupleRestful_test.py +69 -7
  17. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexAccessControlContextABC.py +1 -0
  18. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientTcp.py +4 -19
  19. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientWebsocketFactory.py +5 -29
  20. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexFactory.py +20 -56
  21. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexPayloadProtocol.py +6 -2
  22. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexPermission.py +1 -0
  23. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexPermissionError.py +1 -0
  24. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServer.py +0 -1
  25. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexSession.py +4 -2
  26. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexTcpMemoryLeakPayloadTest.py +24 -24
  27. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexWritePushProducer.py +0 -1
  28. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/__init__.py +1 -1
  29. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLiveLockManager.py +1 -5
  30. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoader.py +28 -16
  31. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoaderDelegate.py +14 -4
  32. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/TupleChangeEventBus.py +7 -8
  33. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleActionProcessor.py +74 -41
  34. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleActionProcessorProxy.py +8 -10
  35. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleActionProcessorTestHandler.py +3 -3
  36. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableCache.py +3 -5
  37. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableHandler.py +28 -20
  38. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableProxyHandler.py +14 -5
  39. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObserverClient.py +3 -6
  40. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataProviderABC.py +3 -1
  41. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/rpc/RPC.py +70 -38
  42. vortexpy-4.2.1/vortex/rpc/RPC_test.py +114 -0
  43. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/OrmCrudHandler.py +2 -5
  44. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/storage/TupleStorageSqlite.py +3 -3
  45. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/tuple_json_schema_builder.py +3 -1
  46. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/tuple_json_schema_test.py +3 -1
  47. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/twisted_runtime_metrics.py +76 -31
  48. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/twisted_runtime_metrics_test.py +6 -4
  49. {vortexpy-4.2.0 → vortexpy-4.2.1/vortexpy.egg-info}/PKG-INFO +1 -1
  50. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/SOURCES.txt +4 -11
  51. vortexpy-4.2.0/LICENSE +0 -22
  52. vortexpy-4.2.0/vortex/DeferUtil_test.py +0 -86
  53. vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry.py +0 -150
  54. vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry_test.py +0 -75
  55. vortexpy-4.2.0/vortex/MtlsServerSslOptions.py +0 -176
  56. vortexpy-4.2.0/vortex/VortexFactory_test.py +0 -114
  57. vortexpy-4.2.0/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -20
  58. vortexpy-4.2.0/vortex/handler/TupleActionPushOffline.py +0 -180
  59. vortexpy-4.2.0/vortex/handler/TupleActionPushOffline_test.py +0 -133
  60. vortexpy-4.2.0/vortex/storage/TupleActionStorageSqlite.py +0 -156
  61. {vortexpy-4.2.0 → vortexpy-4.2.1}/README.md +0 -0
  62. {vortexpy-4.2.0 → vortexpy-4.2.1}/setup.cfg +0 -0
  63. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/DataWrapTuple.py +0 -0
  64. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/Payload.py +0 -0
  65. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadFilterKeys.py +0 -0
  66. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadPriority.py +0 -0
  67. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadResponseTest.py +0 -0
  68. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PayloadTest.py +0 -0
  69. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/PerformTestActionTuple.py +0 -0
  70. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TestTuple.py +0 -0
  71. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleAction.py +0 -0
  72. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleDataForTest.py +0 -0
  73. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TuplePerfTest.py +0 -0
  74. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleSelector.py +0 -0
  75. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/TupleTest.py +0 -0
  76. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexABC.py +0 -0
  77. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexAuthenticatorABC.py +0 -0
  78. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientHttp.py +0 -0
  79. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexClientHttpTest.py +0 -0
  80. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexConnectionABC.py +0 -0
  81. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexEndpointPermission.py +0 -0
  82. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerConnection.py +0 -0
  83. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerHttpResource.py +0 -0
  84. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerTcp.py +0 -0
  85. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexServerWebsocket.py +0 -0
  86. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  87. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexTest.py +0 -0
  88. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/VortexUtil.py +0 -0
  89. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  90. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  91. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/data_loader/__init__.py +0 -0
  92. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  93. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  94. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/event_bus/__init__.py +0 -0
  95. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  96. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataActionClient.py +0 -0
  97. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  98. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  99. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/handler/__init__.py +0 -0
  100. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/mypy_plugin.py +0 -0
  101. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/py.typed +0 -0
  102. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/GzippedDataHttpClient.py +0 -0
  103. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  104. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  105. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulHttpClient.py +0 -0
  106. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulHttpClientTest.py +0 -0
  107. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulResource.py +0 -0
  108. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/RestfulResourceTest.py +0 -0
  109. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/restful/__init__.py +0 -0
  110. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/rpc/RPCTest.py +0 -0
  111. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/rpc/__init__.py +0 -0
  112. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  113. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  114. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/sqla_orm/__init__.py +0 -0
  115. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/storage/__init__.py +0 -0
  116. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/__init__.py +0 -0
  117. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/constants.py +0 -0
  118. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  119. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  120. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/dependency_links.txt +0 -0
  121. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/entry_points.txt +0 -0
  122. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/requires.txt +0 -0
  123. {vortexpy-4.2.0 → vortexpy-4.2.1}/vortexpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 4.2.0
3
+ Version: 4.2.1
4
4
  Summary: Flying Ion's observable, routable, data serialisation and transport code.
5
5
  Author-email: Flying Ion Pty Ltd <contact@flyingion.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortexpy"
7
- version = "4.2.0"
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
- def timedFuncToRun(*runArgs: _P.args, **runKwargs: _P.kwargs) -> _R:
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("Blocking operations shouldn't occur in the reactors thread.")
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("Async operations must occur in the reactors main thread.")
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
- "%s field name is %s, type is %s", e, name, value.__class__
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 EndpointPermissionsType as PermissionsType
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] = requiredPermissions
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 = checkAccessControlContextSignature(
93
- callable_,
94
- callableDescription=callableDesc,
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 self._acceptOnlyFromVortex and vortexName not in self._acceptOnlyFromVortex:
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(payloadEnvelope, vortexSession):
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, vortexSession, vortexName, vortexUuid
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
- data and encodedPayload
62
- ), "You can not pass encodedData and encodedPayload"
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
- self.data, str
80
- ), "Encoded payload is not None or a string"
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
- val, str
89
- ), "Encoded payload is not None or a string"
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
- vortexLogFailure(failure, logger)
119
- logger.error(payloadEnvelope.filt)
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
- logTimeoutError=True,
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._logTimeoutError = logTimeoutError
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._logTimeoutError:
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}," f" Value {value} is less than {self._low}"
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(err.message.startswith("At concreteType:") for err in result.errors)
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 {convert(key): convert(item) for key, item in value.items()}
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(value, includeNones, includeFalse)
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 validationError.message.startswith("At ") and ": " in validationError.message:
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 = get_args(extraTypeHint)[0] if extraTypeHint else None
338
- listItemTypeValidator = cls._getFieldValidatorFromExtraHint(listItemExtraHint)
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(cls._prefixValidationErrors(child.errors, item_path))
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(value, typeHint)
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(jsonDict, TupleClass)
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)