vortexpy 4.3.3__tar.gz → 4.3.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 (130) hide show
  1. {vortexpy-4.3.3 → vortexpy-4.3.5}/PKG-INFO +1 -1
  2. {vortexpy-4.3.3 → vortexpy-4.3.5}/pyproject.toml +1 -1
  3. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleBson_test.py +24 -0
  4. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleDocumentConverter.py +121 -0
  5. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleRestful_test.py +35 -0
  6. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/__init__.py +1 -1
  7. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessor.py +4 -1
  8. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataAccessGranterABC.py +4 -4
  9. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableHandler.py +57 -49
  10. vortexpy-4.3.5/vortex/handler/TupleDataObservableHandler_test.py +264 -0
  11. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableProxyHandler.py +4 -0
  12. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_test_fixtures.py +8 -0
  13. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/PKG-INFO +1 -1
  14. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/SOURCES.txt +1 -0
  15. {vortexpy-4.3.3 → vortexpy-4.3.5}/README.md +0 -0
  16. {vortexpy-4.3.3 → vortexpy-4.3.5}/setup.cfg +0 -0
  17. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/DataWrapTuple.py +0 -0
  18. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/DeferUtil.py +0 -0
  19. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/DeferUtil_test.py +0 -0
  20. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/Jsonable.py +0 -0
  21. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  22. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  23. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/MtlsServerSslOptions.py +0 -0
  24. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/Payload.py +0 -0
  25. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadEndpoint.py +0 -0
  26. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadEndpointTest.py +0 -0
  27. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadEnvelope.py +0 -0
  28. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadFilterKeys.py +0 -0
  29. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadIO.py +0 -0
  30. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadPriority.py +0 -0
  31. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadResponse.py +0 -0
  32. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadResponseTest.py +0 -0
  33. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadResponse_test.py +0 -0
  34. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadTest.py +0 -0
  35. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PerformTestActionTuple.py +0 -0
  36. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/SerialiseUtil.py +0 -0
  37. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/SerialiseUtil_test.py +0 -0
  38. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TestTuple.py +0 -0
  39. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/Tuple.py +0 -0
  40. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleAction.py +0 -0
  41. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleBson.py +0 -0
  42. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleDataForTest.py +0 -0
  43. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TuplePerfTest.py +0 -0
  44. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  45. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleRestful.py +0 -0
  46. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleSelector.py +0 -0
  47. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleTest.py +0 -0
  48. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexABC.py +0 -0
  49. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexAccessControlContextABC.py +0 -0
  50. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexAuthenticatorABC.py +0 -0
  51. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientHttp.py +0 -0
  52. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientHttpTest.py +0 -0
  53. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientTcp.py +0 -0
  54. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientWebsocketFactory.py +0 -0
  55. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexConnectionABC.py +0 -0
  56. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexEndpointPermission.py +0 -0
  57. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexFactory.py +0 -0
  58. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexFactory_test.py +0 -0
  59. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexPayloadProtocol.py +0 -0
  60. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexPermission.py +0 -0
  61. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexPermissionError.py +0 -0
  62. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServer.py +0 -0
  63. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerConnection.py +0 -0
  64. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerConnection_test.py +0 -0
  65. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerHttpResource.py +0 -0
  66. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerTcp.py +0 -0
  67. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerWebsocket.py +0 -0
  68. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexSession.py +0 -0
  69. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexSession_test.py +0 -0
  70. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  71. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  72. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexTest.py +0 -0
  73. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexUtil.py +0 -0
  74. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexWritePushProducer.py +0 -0
  75. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  76. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoader.py +0 -0
  77. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  78. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  79. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  80. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/__init__.py +0 -0
  81. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  82. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  83. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  84. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/__init__.py +0 -0
  85. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  86. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  87. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  88. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionPushOffline.py +0 -0
  89. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  90. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataActionClient.py +0 -0
  91. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableCache.py +0 -0
  92. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  93. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObserverClient.py +0 -0
  94. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataProviderABC.py +0 -0
  95. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  96. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/__init__.py +0 -0
  97. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/mypy_plugin.py +0 -0
  98. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  99. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/py.typed +0 -0
  100. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/GzippedDataHttpClient.py +0 -0
  101. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  102. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  103. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/RestfulHttpClient.py +0 -0
  104. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/RestfulResource.py +0 -0
  105. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/__init__.py +0 -0
  106. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/restful_http_client_test.py +0 -0
  107. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/restful_resource_test.py +0 -0
  108. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/RPC.py +0 -0
  109. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/RPCTest.py +0 -0
  110. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/RPC_test.py +0 -0
  111. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/__init__.py +0 -0
  112. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  113. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  114. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  115. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/__init__.py +0 -0
  116. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  117. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/storage/TupleStorageSqlite.py +0 -0
  118. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/storage/__init__.py +0 -0
  119. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/__init__.py +0 -0
  120. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/constants.py +0 -0
  121. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  122. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  123. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  124. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  125. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/twisted_runtime_metrics.py +0 -0
  126. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/twisted_runtime_metrics_test.py +0 -0
  127. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/dependency_links.txt +0 -0
  128. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/entry_points.txt +0 -0
  129. {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/requires.txt +0 -0
  130. {vortexpy-4.3.3 → vortexpy-4.3.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.3.3
3
+ Version: 4.3.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.3.3"
7
+ version = "4.3.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."
@@ -2,6 +2,7 @@
2
2
  # See LICENSE for the full license text.
3
3
 
4
4
  from datetime import UTC, datetime
5
+ from unittest.mock import patch
5
6
 
6
7
  from ddt import data, ddt
7
8
  import unittest
@@ -12,6 +13,7 @@ from vortex.tuple_test_fixtures import (
12
13
  BsonEnumTestTuple,
13
14
  BsonObjectTypeE,
14
15
  BsonOptionalDatetimeTestTuple,
16
+ BsonOptionalFloatTestTuple,
15
17
  BsonPriorityE,
16
18
  BsonTestTuple,
17
19
  makeBsonTestTuple,
@@ -49,6 +51,28 @@ class TupleBsonTest(unittest.TestCase):
49
51
  deserialisedTuple = self._testSerialisation(origTuple)
50
52
  self.assertEqual(TupleHash(origTuple), TupleHash(deserialisedTuple))
51
53
 
54
+ def testBsonIntegerParsesAsOptionalFloatField(self) -> None:
55
+ deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
56
+ dict(requiredFloat=151, optionalFloat=0),
57
+ BsonOptionalFloatTestTuple,
58
+ )
59
+ self.assertEqual(deserialisedTuple.requiredFloat, 151.0)
60
+ self.assertEqual(deserialisedTuple.optionalFloat, 0.0)
61
+
62
+ def testBsonFractionalDoubleParsesAsIntFieldWithWarning(self) -> None:
63
+ bsonDict = validBsonDict()
64
+ bsonDict["aInt"] = 151.7
65
+ with patch(
66
+ "vortex.TupleDocumentConverter.logger.warning"
67
+ ) as mockWarning:
68
+ deserialisedTuple = Tuple.bsonJsonDictToTupleWithValidation(
69
+ bsonDict, BsonTestTuple
70
+ )
71
+ self.assertEqual(deserialisedTuple.aInt, 151)
72
+ mockWarning.assert_called_once()
73
+ warningMessage = mockWarning.call_args[0][0]
74
+ self.assertIn("lost precision", warningMessage)
75
+
52
76
  def testDatetimeRoundTrip(self) -> None:
53
77
  origTuple = BsonOptionalDatetimeTestTuple()
54
78
  origTuple.requiredDate = datetime(2020, 1, 2, 3, 4, 5, 678000, tzinfo=UTC)
@@ -15,6 +15,7 @@ Subclasses supply datetime and plain-dict coercion at the document boundary.
15
15
  from __future__ import annotations
16
16
 
17
17
  import enum
18
+ import logging
18
19
  from typing import TYPE_CHECKING, Annotated, Any, NamedTuple, Optional, Type
19
20
 
20
21
  from typing import Union, get_args
@@ -29,6 +30,8 @@ from .Tuple import TupleFieldValidatorABC
29
30
  if TYPE_CHECKING:
30
31
  from .Tuple import Tuple
31
32
 
33
+ logger = logging.getLogger(__name__)
34
+
32
35
  _UNSET = object()
33
36
  _ROOT_PATH = "$"
34
37
 
@@ -287,6 +290,90 @@ class TupleDocumentConverterBase:
287
290
 
288
291
  return value, None
289
292
 
293
+ @classmethod
294
+ def _scalarTypeFromHint(cls, typeHint: Any, scalarType: type) -> bool:
295
+ """Return whether typeHint is ``scalarType`` or ``scalarType | None`` only."""
296
+ if typeHint is scalarType:
297
+ return True
298
+
299
+ unionArgs = get_args(typeHint)
300
+ if not unionArgs:
301
+ return False
302
+
303
+ nonNoneArgs = [
304
+ unionArg for unionArg in unionArgs if unionArg is not type(None)
305
+ ]
306
+ return len(nonNoneArgs) == 1 and nonNoneArgs[0] is scalarType
307
+
308
+ @classmethod
309
+ def _floatTypeFromHint(cls, typeHint: Any) -> bool:
310
+ """Return whether typeHint accepts float (including ``float | None``)."""
311
+ return cls._scalarTypeFromHint(typeHint, float)
312
+
313
+ @classmethod
314
+ def _tryParseFloatFieldValue(
315
+ cls,
316
+ field_path: str,
317
+ fieldName: str,
318
+ TupleClass: Type[Tuple],
319
+ typeHint: Any,
320
+ value: Any,
321
+ ) -> tuple[Any, TupleValidationError | None]:
322
+ """Parse JSON integer tokens as ``float`` when the field type is ``float``."""
323
+ if value is None:
324
+ return value, None
325
+
326
+ if not cls._floatTypeFromHint(typeHint):
327
+ return value, None
328
+
329
+ if isinstance(value, float):
330
+ return value, None
331
+
332
+ if isinstance(value, int) and not isinstance(value, bool):
333
+ return float(value), None
334
+
335
+ return value, None
336
+
337
+ @classmethod
338
+ def _intTypeFromHint(cls, typeHint: Any) -> bool:
339
+ """Return whether typeHint accepts int (including ``int | None`` only)."""
340
+ return cls._scalarTypeFromHint(typeHint, int)
341
+
342
+ @classmethod
343
+ def _tryParseIntFieldValue(
344
+ cls,
345
+ field_path: str,
346
+ fieldName: str,
347
+ TupleClass: Type[Tuple],
348
+ typeHint: Any,
349
+ value: Any,
350
+ ) -> tuple[Any, TupleValidationError | None]:
351
+ """Parse JSON float tokens as ``int`` when the field type is ``int``."""
352
+ if value is None:
353
+ return value, None
354
+
355
+ if not cls._intTypeFromHint(typeHint):
356
+ return value, None
357
+
358
+ if isinstance(value, int) and not isinstance(value, bool):
359
+ return value, None
360
+
361
+ if isinstance(value, float):
362
+ parsedInt = int(value)
363
+ if not value.is_integer():
364
+ logger.warning(
365
+ "At %s: parsing %s field %s lost precision converting "
366
+ "float %r to int %s",
367
+ field_path,
368
+ TupleClass.__name__,
369
+ fieldName,
370
+ value,
371
+ parsedInt,
372
+ )
373
+ return parsedInt, None
374
+
375
+ return value, None
376
+
290
377
  @classmethod
291
378
  def _nestedTupleTypeFromHint(cls, typeHint: Any) -> Any | None:
292
379
  """Return nested tuple class for a field hint, including ``X | None`` unions."""
@@ -413,6 +500,26 @@ class TupleDocumentConverterBase:
413
500
  if enumError:
414
501
  errors.append(enumError)
415
502
  continue
503
+ item, floatError = cls._tryParseFloatFieldValue(
504
+ item_path,
505
+ fieldName,
506
+ TupleClass,
507
+ listItemTypeHint,
508
+ item,
509
+ )
510
+ if floatError:
511
+ errors.append(floatError)
512
+ continue
513
+ item, intError = cls._tryParseIntFieldValue(
514
+ item_path,
515
+ fieldName,
516
+ TupleClass,
517
+ listItemTypeHint,
518
+ item,
519
+ )
520
+ if intError:
521
+ errors.append(intError)
522
+ continue
416
523
  itemError = cls._checkType(
417
524
  TupleClass,
418
525
  item_path,
@@ -578,6 +685,20 @@ class TupleDocumentConverterBase:
578
685
  errors.append(enumError)
579
686
  return _ConvertResult(_UNSET, errors)
580
687
 
688
+ value, floatError = cls._tryParseFloatFieldValue(
689
+ field_path, fieldName, TupleClass, typeHint, value
690
+ )
691
+ if floatError:
692
+ errors.append(floatError)
693
+ return _ConvertResult(_UNSET, errors)
694
+
695
+ value, intError = cls._tryParseIntFieldValue(
696
+ field_path, fieldName, TupleClass, typeHint, value
697
+ )
698
+ if intError:
699
+ errors.append(intError)
700
+ return _ConvertResult(_UNSET, errors)
701
+
581
702
  typeError = cls._checkType(
582
703
  TupleClass, field_path, fieldName, typeHint, value, validator
583
704
  )
@@ -3,6 +3,7 @@
3
3
 
4
4
  import json
5
5
  from datetime import UTC, datetime
6
+ from unittest.mock import patch
6
7
 
7
8
  from ddt import data, ddt
8
9
  import unittest
@@ -102,6 +103,40 @@ class TupleRestfulTest(unittest.TestCase):
102
103
  self.assertNotIn("aBoolFalse", jsonDict)
103
104
  self.assertIn("aBoolTrue", jsonDict)
104
105
 
106
+ def testRestfulIntegerJsonParsesAsFloatField(self) -> None:
107
+ jsonDict = _validRestfulJsonDict()
108
+ jsonDict["aFloat"] = 151
109
+ deserialisedTuple = Tuple.restfulJsonDictToTupleWithValidation(
110
+ jsonDict, RestfulTestTuple
111
+ )
112
+ self.assertEqual(deserialisedTuple.aFloat, 151.0)
113
+
114
+ def testRestfulWholeFloatJsonParsesAsIntField(self) -> None:
115
+ jsonDict = _validRestfulJsonDict()
116
+ jsonDict["aInt"] = 151.0
117
+ with patch(
118
+ "vortex.TupleDocumentConverter.logger.warning"
119
+ ) as mockWarning:
120
+ deserialisedTuple = Tuple.restfulJsonDictToTupleWithValidation(
121
+ jsonDict, RestfulTestTuple
122
+ )
123
+ self.assertEqual(deserialisedTuple.aInt, 151)
124
+ mockWarning.assert_not_called()
125
+
126
+ def testRestfulFractionalFloatJsonParsesAsIntFieldWithWarning(self) -> None:
127
+ jsonDict = _validRestfulJsonDict()
128
+ jsonDict["aInt"] = 151.7
129
+ with patch(
130
+ "vortex.TupleDocumentConverter.logger.warning"
131
+ ) as mockWarning:
132
+ deserialisedTuple = Tuple.restfulJsonDictToTupleWithValidation(
133
+ jsonDict, RestfulTestTuple
134
+ )
135
+ self.assertEqual(deserialisedTuple.aInt, 151)
136
+ mockWarning.assert_called_once()
137
+ warningMessage = mockWarning.call_args[0][0]
138
+ self.assertIn("lost precision", warningMessage)
139
+
105
140
  def testRestfulConverterClassMethodsMatchTupleDelegates(self) -> None:
106
141
  origTuple = makeRestfulTestTuple()
107
142
  viaTuple = origTuple.tupleToRestfulJsonDict()
@@ -6,4 +6,4 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
- __version__ = '4.3.3'
9
+ __version__ = '4.3.5'
@@ -188,7 +188,10 @@ class TupleActionProcessor:
188
188
  return tupleName in self._tupleProcessorsByTupleName
189
189
 
190
190
  def shutdown(self):
191
- self._endpoint.shutdown()
191
+ endpoint = getattr(self, "_endpoint", None)
192
+ if endpoint is not None:
193
+ endpoint.shutdown()
194
+ self._endpoint = None
192
195
 
193
196
  def _denyDelegate(
194
197
  self, log_fn, log_msg, error_msg, filt, tupleName, sendResponse
@@ -17,16 +17,16 @@ from vortex.VortexAccessControlContextABC import VortexAccessControlContextABC
17
17
 
18
18
 
19
19
  class TupleDataAccessGranterABC(ABC):
20
- """Abstract base class for per-observer Level 2 push access control.
20
+ """Abstract base class for per-observer Level 2 tuple data access control.
21
21
 
22
22
  Register a concrete implementation via
23
23
  :meth:`~vortex.handler.TupleDataObservableHandler.TupleDataObservableHandler.addTupleProvider`
24
24
  (``accessGranter=`` parameter) or via
25
25
  :meth:`~vortex.handler.TupleDataObservableProxyHandler.TupleDataObservableProxyHandler.addTupleAccessGranter`.
26
26
 
27
- Called for each subscribed observer before a server-push update is sent.
28
- Return ``True`` (or a Deferred that fires with ``True``) to allow delivery,
29
- ``False`` to silently skip that observer.
27
+ Called before a client subscribes (initial response) and for each subscribed
28
+ observer before a server-push update is sent. Return ``True`` (or a Deferred
29
+ that fires with ``True``) to allow delivery, ``False`` to deny that observer.
30
30
  """
31
31
 
32
32
  @abstractmethod
@@ -28,14 +28,12 @@ from vortex.PayloadEndpoint import PayloadEndpoint
28
28
  from vortex.PayloadEnvelope import PayloadEnvelope
29
29
  from vortex.TupleSelector import TupleSelector
30
30
  from vortex.VortexABC import SendVortexMsgResponseCallable
31
+ from vortex.VortexAccessControlContextABC import NO_AUTH_ACCESS_CONTROL_CONTEXT
31
32
  from vortex.VortexFactory import VortexFactory
32
33
  from twisted.internet.defer import maybeDeferred as _maybeDeferred
33
34
 
34
35
  from vortex.handler.TupleDataProviderABC import TupleDataProviderABC
35
36
  from vortex.handler.TupleDataAccessGranterABC import TupleDataAccessGranterABC
36
- from vortex.VortexEndpointPermission import (
37
- EndpointPermissionsType as PermissionsType,
38
- )
39
37
  from vortex.VortexSession import checkNoHttpSessionInSignature
40
38
  from vortex.VortexUtil import limitConcurrency, _dedupProcessorCallKeys
41
39
 
@@ -65,9 +63,7 @@ class TupleSelectorUpdateMapperABC(metaclass=ABCMeta):
65
63
  """
66
64
 
67
65
 
68
- _ObserverDetails = namedtuple(
69
- "_ObserverDetails", ["vortexUuid", "observerName"]
70
- )
66
+ _ObserverDetails = namedtuple("_ObserverDetails", ["vortexUuid", "observerName"])
71
67
 
72
68
 
73
69
  class _ObserverData:
@@ -114,9 +110,7 @@ class TupleDataObservableHandler:
114
110
  self._observerDataByTupleSelector = {}
115
111
 
116
112
  self._tupleProvidersByTupleName = {}
117
- self._accessGrantersByTupleName: Dict[
118
- str, TupleDataAccessGranterABC
119
- ] = {}
113
+ self._accessGrantersByTupleName: Dict[str, TupleDataAccessGranterABC] = {}
120
114
  self._tupleSelectorUpdateMappers = []
121
115
 
122
116
  def addTupleProvider(
@@ -126,7 +120,7 @@ class TupleDataObservableHandler:
126
120
  accessGranter: Optional[TupleDataAccessGranterABC] = None,
127
121
  ):
128
122
  """Add Tuple Provider"""
129
- assert not tupleName in self._tupleProvidersByTupleName, (
123
+ assert tupleName not in self._tupleProvidersByTupleName, (
130
124
  "Observable:%s, Tuple name %s is already registered"
131
125
  % (
132
126
  self._observableName,
@@ -150,16 +144,13 @@ class TupleDataObservableHandler:
150
144
  self._accessGrantersByTupleName[tupleName] = accessGranter
151
145
 
152
146
  providerDesc = (
153
- f"{type(provider).__module__}."
154
- f"{type(provider).__qualname__}.makeVortexMsg"
147
+ f"{type(provider).__module__}.{type(provider).__qualname__}.makeVortexMsg"
155
148
  )
156
149
  checkNoHttpSessionInSignature(provider.makeVortexMsg, providerDesc)
157
150
 
158
151
  self._tupleProvidersByTupleName[tupleName] = provider
159
152
 
160
- def addTupleSelectorUpdateMapper(
161
- self, mapper: TupleSelectorUpdateMapperABC
162
- ):
153
+ def addTupleSelectorUpdateMapper(self, mapper: TupleSelectorUpdateMapperABC):
163
154
  """Add Tuple Selector Update Mapper
164
155
 
165
156
  This mapper will be called every time a tuple selector is notified of an update.
@@ -180,9 +171,7 @@ class TupleDataObservableHandler:
180
171
  for jsonStr in self._observerDataByTupleSelector
181
172
  ]
182
173
 
183
- def tupleSubscriberCount(
184
- self, tupleNames: Optional[frozenset[str]] = None
185
- ) -> int:
174
+ def tupleSubscriberCount(self, tupleNames: Optional[frozenset[str]] = None) -> int:
186
175
  subscriberVortexUuids: set[str] = set()
187
176
  for observerData in self._observerDataByTupleSelector.values():
188
177
  if (
@@ -255,9 +244,7 @@ class TupleDataObservableHandler:
255
244
  if d:
256
245
  d.addErrback(vortexLogFailure, logger, consumeError=True)
257
246
 
258
- @limitConcurrency(
259
- logger, 25, deduplicateBasedOnKwArgsKey=_dedupProcessorCallKeys
260
- )
247
+ @limitConcurrency(logger, 25, deduplicateBasedOnKwArgsKey=_dedupProcessorCallKeys)
261
248
  def _processLimitedDecorated(
262
249
  self,
263
250
  payloadEnvelope: PayloadEnvelope,
@@ -286,9 +273,7 @@ class TupleDataObservableHandler:
286
273
  _ = vortexName
287
274
 
288
275
  if not VortexFactory.isVortexUuidOnline(vortexUuid):
289
- logger.debug(
290
- "Vortex %s is no longer online, skipping reply", vortexUuid
291
- )
276
+ logger.debug("Vortex %s is no longer online, skipping reply", vortexUuid)
292
277
  return
293
278
 
294
279
  # logger.debug(
@@ -319,12 +304,8 @@ class TupleDataObservableHandler:
319
304
  tupleSelector.name,
320
305
  )
321
306
  return
322
- endpointPermissions = yield _maybeDeferred(
323
- auth.getEndpointPermissions, vs
324
- )
325
- if not tupleProvider.requiredPermissions.issubset(
326
- endpointPermissions
327
- ):
307
+ endpointPermissions = yield _maybeDeferred(auth.getEndpointPermissions, vs)
308
+ if not tupleProvider.requiredPermissions.issubset(endpointPermissions):
328
309
  logger.warning(
329
310
  "Observable:%s permission denied for %s",
330
311
  self._observableName,
@@ -360,18 +341,50 @@ class TupleDataObservableHandler:
360
341
 
361
342
  return
362
343
 
344
+ granter = self._accessGrantersByTupleName.get(tupleSelector.name)
345
+ if granter:
346
+ filt = dict(tupleSelector=tupleSelector)
347
+ filt.update(self._filt)
348
+ auth = VortexFactory.getAuthenticator()
349
+ ctx = NO_AUTH_ACCESS_CONTROL_CONTEXT
350
+ if auth:
351
+ if not vs:
352
+ logger.warning(
353
+ "Observable:%s access denied for %s:"
354
+ " no VortexSession for access control context.",
355
+ self._observableName,
356
+ tupleSelector.name,
357
+ )
358
+ return
359
+ ctx = yield _maybeDeferred(
360
+ auth.getAccessControlContext,
361
+ vs,
362
+ vs.vortexName,
363
+ vortexUuid,
364
+ )
365
+ if ctx is None:
366
+ logger.warning(
367
+ "Observable:%s access denied for %s:"
368
+ " access control context unavailable.",
369
+ self._observableName,
370
+ tupleSelector.name,
371
+ )
372
+ return
373
+ granted = yield granter.isAccessGranted(filt, tupleSelector, ctx)
374
+ if not granted:
375
+ logger.warning(
376
+ "Observable:%s access denied for %s",
377
+ self._observableName,
378
+ tupleSelector.name,
379
+ )
380
+ return
381
+
363
382
  # Add support for just getting data, no subscription.
364
- if self._subscriptionsEnabled and payloadEnvelope.filt.get(
365
- "subscribe", True
366
- ):
383
+ if self._subscriptionsEnabled and payloadEnvelope.filt.get("subscribe", True):
367
384
  if tsStr not in self._observerDataByTupleSelector:
368
- self._observerDataByTupleSelector[tsStr] = _ObserverData(
369
- tupleSelector
370
- )
385
+ self._observerDataByTupleSelector[tsStr] = _ObserverData(tupleSelector)
371
386
 
372
- self._observerDataByTupleSelector[tsStr].observers.add(
373
- observerDetails
374
- )
387
+ self._observerDataByTupleSelector[tsStr].observers.add(observerDetails)
375
388
  self._observerDataByTupleSelector[tsStr].vortexSessionByVortexUuid[
376
389
  vortexUuid
377
390
  ] = vs
@@ -395,8 +408,7 @@ class TupleDataObservableHandler:
395
408
  ) -> List[TupleSelector]:
396
409
  # Get all tuple selectors
397
410
  allTupleSelectors = [
398
- data.tupleSelector
399
- for data in self._observerDataByTupleSelector.values()
411
+ data.tupleSelector for data in self._observerDataByTupleSelector.values()
400
412
  ]
401
413
 
402
414
  # Create a dict so we end up with only unique ones
@@ -404,9 +416,7 @@ class TupleDataObservableHandler:
404
416
 
405
417
  # Run through the mappers
406
418
  for mapper in self._tupleSelectorUpdateMappers:
407
- mappedSelectors = mapper.mapTupleSelector(
408
- tupleSelector, allTupleSelectors
409
- )
419
+ mappedSelectors = mapper.mapTupleSelector(tupleSelector, allTupleSelectors)
410
420
  if mappedSelectors:
411
421
  tupleSelectorByStr.update(
412
422
  {ts.toJsonStr(): ts for ts in mappedSelectors}
@@ -498,9 +508,7 @@ class TupleDataObservableHandler:
498
508
  # Send the vortex messages, checking access per observer if a granter is set
499
509
  for destVortexUuid in destVortexUuids:
500
510
  if granter:
501
- sessionVs = observerData.vortexSessionByVortexUuid.get(
502
- destVortexUuid
503
- )
511
+ sessionVs = observerData.vortexSessionByVortexUuid.get(destVortexUuid)
504
512
  ctx = None
505
513
  if auth and sessionVs:
506
514
  ctx = yield _maybeDeferred(
@@ -509,8 +517,8 @@ class TupleDataObservableHandler:
509
517
  sessionVs.vortexName,
510
518
  destVortexUuid,
511
519
  )
512
- granted = yield _maybeDeferred(
513
- granter.isAccessGranted, filt, tupleSelector, ctx
520
+ granted = yield granter.isAccessGranted(
521
+ filt, tupleSelector, ctx
514
522
  )
515
523
  if not granted:
516
524
  continue