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.
- {vortexpy-4.3.3 → vortexpy-4.3.5}/PKG-INFO +1 -1
- {vortexpy-4.3.3 → vortexpy-4.3.5}/pyproject.toml +1 -1
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleBson_test.py +24 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleDocumentConverter.py +121 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleRestful_test.py +35 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/__init__.py +1 -1
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessor.py +4 -1
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataAccessGranterABC.py +4 -4
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableHandler.py +57 -49
- vortexpy-4.3.5/vortex/handler/TupleDataObservableHandler_test.py +264 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableProxyHandler.py +4 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_test_fixtures.py +8 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/SOURCES.txt +1 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/README.md +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/setup.cfg +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/DeferUtil.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/DeferUtil_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/Jsonable.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/MtlsServerSslOptions.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/Payload.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadEndpoint.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadEndpointTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadEnvelope.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadIO.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadResponse.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadResponse_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/SerialiseUtil.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/SerialiseUtil_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TestTuple.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/Tuple.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleAction.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleBson.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleRestful.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/TupleTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexAccessControlContextABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientTcp.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexClientWebsocketFactory.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexFactory.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexFactory_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexPayloadProtocol.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexPermission.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexPermissionError.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServer.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerConnection_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexSession.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexSession_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexUtil.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/VortexWritePushProducer.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoader.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/TupleChangeEventBus.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessorProxy.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionPushOffline.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleActionPushOffline_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableCache.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataObserverClient.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/TupleDataProviderABC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/mypy_plugin.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/payload_many_tuples_benchmark_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/py.typed +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/restful_http_client_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/restful/restful_resource_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/RPC.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/RPC_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/storage/TupleActionStorageSqlite.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/storage/TupleStorageSqlite.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/twisted_runtime_metrics.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortex/twisted_runtime_metrics_test.py +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.3.3 → vortexpy-4.3.5}/vortexpy.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortexpy"
|
|
7
|
-
version = "4.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()
|
|
@@ -188,7 +188,10 @@ class TupleActionProcessor:
|
|
|
188
188
|
return tupleName in self._tupleProcessorsByTupleName
|
|
189
189
|
|
|
190
190
|
def shutdown(self):
|
|
191
|
-
self
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
``False`` to
|
|
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
|
|
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
|
-
|
|
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
|
|
513
|
-
|
|
520
|
+
granted = yield granter.isAccessGranted(
|
|
521
|
+
filt, tupleSelector, ctx
|
|
514
522
|
)
|
|
515
523
|
if not granted:
|
|
516
524
|
continue
|