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.
Files changed (126) hide show
  1. {vortexpy-4.2.0/vortexpy.egg-info → vortexpy-4.3.0}/PKG-INFO +1 -1
  2. {vortexpy-4.2.0 → vortexpy-4.3.0}/pyproject.toml +1 -1
  3. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/DeferUtil.py +7 -24
  4. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/Jsonable.py +4 -2
  5. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadEndpoint.py +26 -8
  6. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadEndpointTest.py +1 -3
  7. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadEnvelope.py +9 -9
  8. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadIO.py +9 -2
  9. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadResponse.py +7 -3
  10. vortexpy-4.3.0/vortex/PayloadResponse_test.py +42 -0
  11. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/SerialiseUtil.py +8 -0
  12. vortexpy-4.3.0/vortex/SerialiseUtil_test.py +123 -0
  13. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/Tuple.py +34 -3
  14. vortexpy-4.3.0/vortex/TupleBson.py +182 -0
  15. vortexpy-4.3.0/vortex/TupleBson_test.py +353 -0
  16. vortexpy-4.2.0/vortex/TupleRestful.py → vortexpy-4.3.0/vortex/TupleDocumentConverter.py +152 -89
  17. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +4 -1
  18. vortexpy-4.3.0/vortex/TupleRestful.py +134 -0
  19. vortexpy-4.2.0/vortex/TupleRestfulTest.py → vortexpy-4.3.0/vortex/TupleRestful_test.py +69 -7
  20. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexAccessControlContextABC.py +1 -0
  21. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientTcp.py +4 -19
  22. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientWebsocketFactory.py +5 -29
  23. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexFactory.py +20 -56
  24. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexPayloadProtocol.py +6 -2
  25. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexPermission.py +1 -0
  26. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexPermissionError.py +1 -0
  27. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServer.py +0 -1
  28. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexSession.py +4 -2
  29. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +24 -24
  30. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexUtil.py +17 -2
  31. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexWritePushProducer.py +0 -1
  32. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/__init__.py +1 -1
  33. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLiveLockManager.py +1 -5
  34. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoader.py +28 -16
  35. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderDelegate.py +14 -4
  36. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBus.py +7 -8
  37. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessor.py +74 -41
  38. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorProxy.py +8 -10
  39. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleActionProcessorTestHandler.py +3 -3
  40. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableCache.py +3 -5
  41. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableHandler.py +28 -20
  42. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableProxyHandler.py +14 -5
  43. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObserverClient.py +3 -6
  44. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataProviderABC.py +3 -1
  45. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/rpc/RPC.py +70 -38
  46. vortexpy-4.3.0/vortex/rpc/RPC_test.py +114 -0
  47. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/OrmCrudHandler.py +2 -5
  48. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/storage/TupleStorageSqlite.py +3 -3
  49. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +3 -1
  50. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +3 -1
  51. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics.py +227 -27
  52. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/twisted_runtime_metrics_test.py +75 -5
  53. {vortexpy-4.2.0 → vortexpy-4.3.0/vortexpy.egg-info}/PKG-INFO +1 -1
  54. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/SOURCES.txt +7 -11
  55. vortexpy-4.2.0/LICENSE +0 -22
  56. vortexpy-4.2.0/vortex/DeferUtil_test.py +0 -86
  57. vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry.py +0 -150
  58. vortexpy-4.2.0/vortex/MtlsAuthorizedClientRegistry_test.py +0 -75
  59. vortexpy-4.2.0/vortex/MtlsServerSslOptions.py +0 -176
  60. vortexpy-4.2.0/vortex/VortexFactory_test.py +0 -114
  61. vortexpy-4.2.0/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -20
  62. vortexpy-4.2.0/vortex/handler/TupleActionPushOffline.py +0 -180
  63. vortexpy-4.2.0/vortex/handler/TupleActionPushOffline_test.py +0 -133
  64. vortexpy-4.2.0/vortex/storage/TupleActionStorageSqlite.py +0 -156
  65. {vortexpy-4.2.0 → vortexpy-4.3.0}/README.md +0 -0
  66. {vortexpy-4.2.0 → vortexpy-4.3.0}/setup.cfg +0 -0
  67. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/DataWrapTuple.py +0 -0
  68. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/Payload.py +0 -0
  69. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadFilterKeys.py +0 -0
  70. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadPriority.py +0 -0
  71. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadResponseTest.py +0 -0
  72. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PayloadTest.py +0 -0
  73. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/PerformTestActionTuple.py +0 -0
  74. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TestTuple.py +0 -0
  75. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleAction.py +0 -0
  76. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleDataForTest.py +0 -0
  77. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TuplePerfTest.py +0 -0
  78. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleSelector.py +0 -0
  79. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/TupleTest.py +0 -0
  80. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexABC.py +0 -0
  81. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexAuthenticatorABC.py +0 -0
  82. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientHttp.py +0 -0
  83. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexClientHttpTest.py +0 -0
  84. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexConnectionABC.py +0 -0
  85. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexEndpointPermission.py +0 -0
  86. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerConnection.py +0 -0
  87. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerHttpResource.py +0 -0
  88. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerTcp.py +0 -0
  89. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexServerWebsocket.py +0 -0
  90. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  91. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/VortexTest.py +0 -0
  92. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  93. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  94. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/data_loader/__init__.py +0 -0
  95. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  96. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  97. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/event_bus/__init__.py +0 -0
  98. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  99. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataActionClient.py +0 -0
  100. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  101. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  102. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/handler/__init__.py +0 -0
  103. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/mypy_plugin.py +0 -0
  104. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/py.typed +0 -0
  105. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
  106. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  107. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  108. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClient.py +0 -0
  109. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulHttpClientTest.py +0 -0
  110. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulResource.py +0 -0
  111. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/RestfulResourceTest.py +0 -0
  112. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/restful/__init__.py +0 -0
  113. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/rpc/RPCTest.py +0 -0
  114. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/rpc/__init__.py +0 -0
  115. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  116. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  117. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/sqla_orm/__init__.py +0 -0
  118. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/storage/__init__.py +0 -0
  119. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/__init__.py +0 -0
  120. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/constants.py +0 -0
  121. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  122. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  123. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/dependency_links.txt +0 -0
  124. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/entry_points.txt +0 -0
  125. {vortexpy-4.2.0 → vortexpy-4.3.0}/vortexpy.egg-info/requires.txt +0 -0
  126. {vortexpy-4.2.0 → vortexpy-4.3.0}/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.3.0
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.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
- 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
 
@@ -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