vortexpy 4.1.2__tar.gz → 4.1.5__tar.gz

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