vortexpy 6.2.3__tar.gz → 6.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vortexpy-6.3.0/LICENSE +22 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/PKG-INFO +1 -1
- {vortexpy-6.2.3 → vortexpy-6.3.0}/pyproject.toml +1 -1
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/Jsonable.py +58 -15
- vortexpy-6.3.0/vortex/Jsonable_tuple_dict_coercion_test.py +144 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadResponse.py +60 -17
- vortexpy-6.3.0/vortex/PayloadResponse_test.py +161 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/SerialiseUtil.py +29 -8
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleBson_test.py +23 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleDocumentConverter.py +13 -6
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/__init__.py +1 -1
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataOfflineObserverService.py +21 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataOfflineObserverService_test.py +49 -2
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/tuple_test_fixtures.py +6 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortexpy.egg-info/SOURCES.txt +2 -0
- vortexpy-6.2.3/vortex/PayloadResponse_test.py +0 -42
- {vortexpy-6.2.3 → vortexpy-6.3.0}/README.md +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/TwistedDeferredTimeout.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/run_http_upgraded_websocket_ping_client.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/run_http_upgraded_websocket_ping_server.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/run_websocket_rpc_test_client.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/run_websocket_rpc_test_server.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/setup_example_logging.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/example/store_and_load_json.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/setup.cfg +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/DeferUtil.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/DeferUtil_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/MtlsServerSslOptions.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/Payload.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadEndpoint.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadEndpointTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadEnvelope.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadIO.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadPriority.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PayloadTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/SerialiseUtil_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TestTuple.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/Tuple.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleAction.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleBson.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleRestful.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleRestful_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleSelector.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/TupleTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexAccessControlContextABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexClientTcp.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexClientWebsocketFactory.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexFactory.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexFactory_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexPayloadProtocol.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexPermission.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexPermissionError.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServer.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServerConnection_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServerTcp_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServerWebsocket_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexServer_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexSession.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexSession_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexUserDetailsABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexUtil.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexWritePushProducer.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/VortexWritePushProducer_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/data_loader/TupleDataLiveLockManager_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/data_loader/TupleDataLoader.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/event_bus/TupleChangeEventBus.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleActionProcessor.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleActionProcessorProxy.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleActionProcessor_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleActionPushOffline.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleActionPushOffline_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataActionClient_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObservableCache.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObservableCache_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObservableHandler.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObservableHandler_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataObserverClient.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/TupleDataProviderABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/handler/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/mypy_plugin.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/payload_many_tuples_benchmark_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/py.typed +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/restful_http_client_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/restful/restful_resource_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/rpc/RPC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/rpc/RPC_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/rpc/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleActionStorageSqlite.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleOfflineStorageNameService.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleOfflineStorageSecureService.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleOfflineStorageSecureService_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleOfflineStorageService.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleStorageServiceABC.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleStorageSqlite.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleStorageSqliteService.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/TupleStorageSqliteService_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/storage/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/twisted_runtime_metrics.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortex/twisted_runtime_metrics_test.py +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-6.2.3 → vortexpy-6.3.0}/vortexpy.egg-info/top_level.txt +0 -0
vortexpy-6.3.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortexpy"
|
|
7
|
-
version = "6.
|
|
7
|
+
version = "6.3.0"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
authors = [{name = "Flying Ion Pty Ltd", email = "contact@flyingion.com"}]
|
|
10
10
|
description = "Flying Ion's observable, routable, data serialisation and transport code."
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
import logging
|
|
10
10
|
import traceback
|
|
11
11
|
import typing
|
|
12
|
+
from _collections import defaultdict
|
|
12
13
|
from copy import copy, deepcopy
|
|
13
14
|
from json import JSONEncoder
|
|
14
|
-
from typing import List
|
|
15
|
-
from _collections import defaultdict
|
|
16
15
|
|
|
17
16
|
logger = logging.getLogger(__name__)
|
|
18
17
|
|
|
@@ -27,15 +26,15 @@ JSONEncoder.item_separator = ","
|
|
|
27
26
|
JSONEncoder.key_separator = ":"
|
|
28
27
|
|
|
29
28
|
|
|
30
|
-
class Jsonable
|
|
29
|
+
class Jsonable:
|
|
31
30
|
"""Xmlable
|
|
32
31
|
Inherit from this class if you want to serialise to XML.
|
|
33
32
|
"""
|
|
34
33
|
|
|
35
|
-
__fieldNames__:
|
|
34
|
+
__fieldNames__: list[str] = None
|
|
36
35
|
__rapuiSerialiseType__ = T_GENERIC_CLASS
|
|
37
36
|
|
|
38
|
-
__rawJonableFields__:
|
|
37
|
+
__rawJonableFields__: list[str] = None
|
|
39
38
|
|
|
40
39
|
JSON_CLASS_TYPE = "_ct"
|
|
41
40
|
JSON_CLASS = "_c"
|
|
@@ -52,9 +51,7 @@ class Jsonable(object):
|
|
|
52
51
|
cls.__memoryLoggingEnabled = True
|
|
53
52
|
|
|
54
53
|
@classmethod
|
|
55
|
-
def memoryLoggingDump(
|
|
56
|
-
cls, top=10, over=100
|
|
57
|
-
) -> List[typing.Tuple[str, int]]:
|
|
54
|
+
def memoryLoggingDump(cls, top=10, over=100) -> list[tuple[str, int]]:
|
|
58
55
|
if not Jsonable.__memoryLoggingRefs:
|
|
59
56
|
return []
|
|
60
57
|
|
|
@@ -67,10 +64,10 @@ class Jsonable(object):
|
|
|
67
64
|
return list(filter(lambda x: x[1] >= over, data))[:top]
|
|
68
65
|
|
|
69
66
|
def __memLoggingKey(self):
|
|
70
|
-
if
|
|
71
|
-
key = "Tuple: " +
|
|
67
|
+
if self.__rapuiSerialiseType__ == T_RAPUI_TUPLE:
|
|
68
|
+
key = "Tuple: " + self.__tupleType__
|
|
72
69
|
|
|
73
|
-
elif
|
|
70
|
+
elif self.__rapuiSerialiseType__ == T_RAPUI_PAYLOAD:
|
|
74
71
|
assert hasattr(self, "filt"), "Payload is missing filt"
|
|
75
72
|
if "key" in self.filt:
|
|
76
73
|
key = "Payload: key=" + self.filt["key"]
|
|
@@ -78,7 +75,7 @@ class Jsonable(object):
|
|
|
78
75
|
key = "Payload: " + str(self.filt)
|
|
79
76
|
|
|
80
77
|
else:
|
|
81
|
-
key = "Jsonable: " +
|
|
78
|
+
key = "Jsonable: " + self.__rapuiSerialiseType__
|
|
82
79
|
|
|
83
80
|
return key
|
|
84
81
|
|
|
@@ -100,9 +97,7 @@ class Jsonable(object):
|
|
|
100
97
|
from .Tuple import TupleField
|
|
101
98
|
|
|
102
99
|
if isinstance(value, TupleField):
|
|
103
|
-
value = (
|
|
104
|
-
None if value.defaultValue == None else copy(value.defaultValue)
|
|
105
|
-
)
|
|
100
|
+
value = None if value.defaultValue == None else copy(value.defaultValue)
|
|
106
101
|
|
|
107
102
|
try:
|
|
108
103
|
valueType = V_NULL if value is None else toRapuiType(value)
|
|
@@ -316,6 +311,54 @@ class Jsonable(object):
|
|
|
316
311
|
):
|
|
317
312
|
value = enumClass(value)
|
|
318
313
|
|
|
314
|
+
# Reconstruct Tuple subclasses for fields annotated with a Tuple
|
|
315
|
+
# type. Serialised tuples may arrive as plain dicts (no _ct/_c
|
|
316
|
+
# markers) when the sender has no Tuple class for the value; the
|
|
317
|
+
# receiving class's annotation drives the coercion.
|
|
318
|
+
value = self._coerceValueToTypeHint(value, hints.get(name))
|
|
319
|
+
|
|
319
320
|
setattr(self, name, value)
|
|
320
321
|
|
|
321
322
|
return self
|
|
323
|
+
|
|
324
|
+
def _coerceValueToTypeHint(
|
|
325
|
+
self, value: typing.Any, typeHint: typing.Any
|
|
326
|
+
) -> typing.Any:
|
|
327
|
+
"""Coerce a plain JSON value into the type declared by *typeHint*.
|
|
328
|
+
|
|
329
|
+
Currently handles ``vortex.Tuple`` subclasses (and lists of them);
|
|
330
|
+
other hints pass through unchanged. Plain dicts are validated:
|
|
331
|
+
unknown keys raise ``KeyError``. Values that are neither a dict nor
|
|
332
|
+
``None`` for a Tuple-typed field raise ``ValueError``.
|
|
333
|
+
"""
|
|
334
|
+
from .Tuple import Tuple
|
|
335
|
+
|
|
336
|
+
tupleClass = utilTupleClassFromTypeHint(typeHint)
|
|
337
|
+
if tupleClass is not None:
|
|
338
|
+
if isinstance(value, tupleClass):
|
|
339
|
+
return value
|
|
340
|
+
if isinstance(value, dict):
|
|
341
|
+
extraKeys = set(value) - set(tupleClass.__fieldNames__)
|
|
342
|
+
if extraKeys:
|
|
343
|
+
raise KeyError(
|
|
344
|
+
f"Unexpected keys for {tupleClass.__name__}:"
|
|
345
|
+
f" {sorted(extraKeys)}"
|
|
346
|
+
)
|
|
347
|
+
return Tuple.restfulJsonDictToTupleWithValidation(value, tupleClass)
|
|
348
|
+
if value is None:
|
|
349
|
+
return None
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"Expected dict for {tupleClass.__name__}, got {type(value).__name__}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
origin = typing.get_origin(typeHint)
|
|
355
|
+
if origin is list and isinstance(value, list):
|
|
356
|
+
args = typing.get_args(typeHint)
|
|
357
|
+
if args:
|
|
358
|
+
elementClass = utilTupleClassFromTypeHint(args[0])
|
|
359
|
+
if elementClass is not None:
|
|
360
|
+
return [
|
|
361
|
+
self._coerceValueToTypeHint(item, args[0]) for item in value
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
return value
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# ruff: noqa: C408, N999
|
|
2
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
3
|
+
#
|
|
4
|
+
# This software is open source; the MIT license applies.
|
|
5
|
+
# See LICENSE for the full license text.
|
|
6
|
+
|
|
7
|
+
"""Tests for Jsonable.fromJsonDict coercion of plain dicts into Tuple fields."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from vortex.SerialiseUtil import ISO8601
|
|
17
|
+
from vortex.Tuple import Tuple, TupleField, addTupleType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _ColorE(StrEnum):
|
|
21
|
+
RED = "red"
|
|
22
|
+
BLUE = "blue"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@addTupleType
|
|
26
|
+
class _InnerTuple(Tuple):
|
|
27
|
+
__tupleType__ = "flyingion.vortex.JsonableCoercionInnerTuple"
|
|
28
|
+
fieldMap: dict[str, str] = TupleField()
|
|
29
|
+
color: _ColorE = TupleField()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@addTupleType
|
|
33
|
+
class _OuterTuple(Tuple):
|
|
34
|
+
__tupleType__ = "flyingion.vortex.JsonableCoercionOuterTuple"
|
|
35
|
+
inner: _InnerTuple = TupleField()
|
|
36
|
+
inners: list[_InnerTuple] = TupleField()
|
|
37
|
+
optInner: _InnerTuple | None = TupleField()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@addTupleType
|
|
41
|
+
class _DatetimeInnerTuple(Tuple):
|
|
42
|
+
__tupleType__ = "flyingion.vortex.JsonableCoercionDatetimeInnerTuple"
|
|
43
|
+
when: datetime = TupleField()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@addTupleType
|
|
47
|
+
class _DatetimeOuterTuple(Tuple):
|
|
48
|
+
__tupleType__ = "flyingion.vortex.JsonableCoercionDatetimeOuterTuple"
|
|
49
|
+
inner: _DatetimeInnerTuple = TupleField()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_from_json_dict_coerces_nested_tuple_field() -> None:
|
|
53
|
+
data = dict(
|
|
54
|
+
inner=dict(fieldMap=dict(payload="body"), color="blue"),
|
|
55
|
+
inners=[dict(fieldMap=dict(x="y"), color="red")],
|
|
56
|
+
optInner=None,
|
|
57
|
+
)
|
|
58
|
+
result = _OuterTuple().fromJsonDict(data)
|
|
59
|
+
|
|
60
|
+
assert isinstance(result.inner, _InnerTuple), (
|
|
61
|
+
"plain dict inner must coerce to declared Tuple type"
|
|
62
|
+
)
|
|
63
|
+
assert result.inner.fieldMap == dict(payload="body"), (
|
|
64
|
+
"fieldMap must coerce nested dict values"
|
|
65
|
+
)
|
|
66
|
+
assert result.inner.color == _ColorE.BLUE, (
|
|
67
|
+
"enum field must coerce from string value"
|
|
68
|
+
)
|
|
69
|
+
assert isinstance(result.inners, list), "inners must be a list"
|
|
70
|
+
assert len(result.inners) == 1, "inners must contain one item"
|
|
71
|
+
assert isinstance(result.inners[0], _InnerTuple), (
|
|
72
|
+
"list item must coerce to declared Tuple type"
|
|
73
|
+
)
|
|
74
|
+
assert result.inners[0].color == _ColorE.RED, (
|
|
75
|
+
"list item enum must coerce from string value"
|
|
76
|
+
)
|
|
77
|
+
assert result.optInner is None, "optInner None must be preserved"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_from_json_dict_coerces_optional_nested_tuple_field() -> None:
|
|
81
|
+
data = dict(
|
|
82
|
+
inner=dict(fieldMap=dict(), color="red"),
|
|
83
|
+
inners=[],
|
|
84
|
+
optInner=dict(fieldMap=dict(k="v"), color="blue"),
|
|
85
|
+
)
|
|
86
|
+
result = _OuterTuple().fromJsonDict(data)
|
|
87
|
+
|
|
88
|
+
assert isinstance(result.optInner, _InnerTuple), (
|
|
89
|
+
"optional inner dict must coerce to declared Tuple type"
|
|
90
|
+
)
|
|
91
|
+
assert result.optInner.color == _ColorE.BLUE, (
|
|
92
|
+
"optional inner enum must coerce from string value"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_from_json_dict_coerces_nested_tuple_with_iso_datetime() -> None:
|
|
97
|
+
when = datetime(2024, 6, 15, 10, 30, 45, 123456, tzinfo=UTC)
|
|
98
|
+
data = dict(
|
|
99
|
+
inner=dict(when=when.strftime(ISO8601)),
|
|
100
|
+
)
|
|
101
|
+
result = _DatetimeOuterTuple().fromJsonDict(data)
|
|
102
|
+
|
|
103
|
+
assert isinstance(result.inner.when, datetime), (
|
|
104
|
+
"nested tuple datetime field must decode to datetime"
|
|
105
|
+
)
|
|
106
|
+
assert result.inner.when == when, "nested tuple datetime field must preserve value"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_from_json_dict_enum_inside_nested_tuple_reconstructs() -> None:
|
|
110
|
+
data = dict(inner=dict(fieldMap=dict(), color="blue"), inners=[], optInner=None)
|
|
111
|
+
result = _OuterTuple().fromJsonDict(data)
|
|
112
|
+
|
|
113
|
+
assert result.inner.color == _ColorE.BLUE, (
|
|
114
|
+
"nested enum field must reconstruct from string"
|
|
115
|
+
)
|
|
116
|
+
assert isinstance(result.inner.color, _ColorE), (
|
|
117
|
+
"nested enum field must be the enum type"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_from_json_dict_missing_required_key_raises_validation_error() -> None:
|
|
122
|
+
data = dict(
|
|
123
|
+
inner=dict(fieldMap=dict(payload="body")), # missing color
|
|
124
|
+
inners=[],
|
|
125
|
+
optInner=None,
|
|
126
|
+
)
|
|
127
|
+
with pytest.raises((KeyError, TypeError)):
|
|
128
|
+
_OuterTuple().fromJsonDict(data)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_from_json_dict_extra_key_raises_validation_error() -> None:
|
|
132
|
+
data = dict(
|
|
133
|
+
inner=dict(fieldMap=dict(), color="red", extra=1),
|
|
134
|
+
inners=[],
|
|
135
|
+
optInner=None,
|
|
136
|
+
)
|
|
137
|
+
with pytest.raises((KeyError, TypeError)):
|
|
138
|
+
_OuterTuple().fromJsonDict(data)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_from_json_dict_non_dict_for_tuple_typed_field_raises() -> None:
|
|
142
|
+
data = dict(inner="not-a-dict", inners=[], optInner=None)
|
|
143
|
+
with pytest.raises(ValueError):
|
|
144
|
+
_OuterTuple().fromJsonDict(data)
|
|
@@ -16,6 +16,8 @@ from uuid import uuid4
|
|
|
16
16
|
import pytz
|
|
17
17
|
from twisted.internet import reactor
|
|
18
18
|
from twisted.internet.defer import Deferred
|
|
19
|
+
from twisted.internet.defer import TimeoutError
|
|
20
|
+
from twisted.internet.interfaces import IDelayedCall
|
|
19
21
|
from twisted.python.failure import Failure
|
|
20
22
|
|
|
21
23
|
from vortex.PayloadEndpoint import PayloadEndpoint
|
|
@@ -68,6 +70,7 @@ class PayloadResponse(Deferred):
|
|
|
68
70
|
timeout: Optional[float] = None,
|
|
69
71
|
resultCheck=True,
|
|
70
72
|
logTimeoutException=True,
|
|
73
|
+
retries: Optional[int] = None,
|
|
71
74
|
) -> None:
|
|
72
75
|
"""Constructor
|
|
73
76
|
|
|
@@ -83,9 +86,16 @@ class PayloadResponse(Deferred):
|
|
|
83
86
|
it errback will be called.
|
|
84
87
|
:param logTimeoutException: When True, payload timeouts log at ERROR.
|
|
85
88
|
When False, timeouts log at DEBUG with neutral wording.
|
|
89
|
+
:param retries: The number of times to resend the payload envelope
|
|
90
|
+
when the response times out; None sends once with no
|
|
91
|
+
retries. Requires destVortexName or destVortexUuid.
|
|
86
92
|
"""
|
|
87
93
|
Deferred.__init__(self)
|
|
88
94
|
|
|
95
|
+
assert retries is None or (destVortexName or destVortexUuid), (
|
|
96
|
+
"retries requires destVortexName or destVortexUuid to resend"
|
|
97
|
+
)
|
|
98
|
+
|
|
89
99
|
if not timeout:
|
|
90
100
|
timeout = self.TIMEOUT
|
|
91
101
|
|
|
@@ -99,6 +109,11 @@ class PayloadResponse(Deferred):
|
|
|
99
109
|
payloadEnvelope.filt[self.MESSAGE_ID_KEY] = self._messageId
|
|
100
110
|
self._filt = copy(payloadEnvelope.filt)
|
|
101
111
|
self._destVortexName = destVortexName
|
|
112
|
+
self._destVortexUuid = destVortexUuid
|
|
113
|
+
self._payloadEnvelope = payloadEnvelope
|
|
114
|
+
self._timeout = timeout
|
|
115
|
+
self._retries: Optional[int] = retries
|
|
116
|
+
self._timeoutDelayedCall: Optional[IDelayedCall] = None
|
|
102
117
|
|
|
103
118
|
self._status = self.PROCESSING
|
|
104
119
|
self._date = datetime.now(pytz.utc)
|
|
@@ -107,23 +122,49 @@ class PayloadResponse(Deferred):
|
|
|
107
122
|
self._filt, self._process, requiredPermissions=frozenset()
|
|
108
123
|
)
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
d: Deferred = payloadEnvelope.toVortexMsgDefer()
|
|
112
|
-
d.addCallback(
|
|
113
|
-
VortexFactory.sendVortexMsg,
|
|
114
|
-
destVortexName=destVortexName,
|
|
115
|
-
destVortexUuid=destVortexUuid,
|
|
116
|
-
)
|
|
117
|
-
d.addErrback(self.errback)
|
|
125
|
+
self._sendVortexMsg()
|
|
118
126
|
|
|
119
127
|
try:
|
|
120
128
|
raise Exception()
|
|
121
129
|
except:
|
|
122
130
|
self._stack = sys.exc_info()[2]
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
if retries is None:
|
|
133
|
+
# noinspection PyTypeChecker
|
|
134
|
+
self.addTimeout(timeout, reactor)
|
|
135
|
+
self.addErrback(self._timedOut)
|
|
136
|
+
else:
|
|
137
|
+
self._armTimeout()
|
|
138
|
+
|
|
139
|
+
def _sendVortexMsg(self) -> None:
|
|
140
|
+
if not (self._destVortexName or self._destVortexUuid):
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
d: Deferred = self._payloadEnvelope.toVortexMsgDefer()
|
|
144
|
+
d.addCallback(
|
|
145
|
+
VortexFactory.sendVortexMsg,
|
|
146
|
+
destVortexName=self._destVortexName,
|
|
147
|
+
destVortexUuid=self._destVortexUuid,
|
|
148
|
+
)
|
|
149
|
+
d.addErrback(self.errback)
|
|
150
|
+
|
|
151
|
+
def _armTimeout(self) -> None:
|
|
152
|
+
self._timeoutDelayedCall = reactor.callLater( # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue]
|
|
153
|
+
self._timeout, self._onTimeout
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _onTimeout(self) -> None:
|
|
157
|
+
self._timeoutDelayedCall = None
|
|
158
|
+
|
|
159
|
+
if self._retries:
|
|
160
|
+
self._retries -= 1
|
|
161
|
+
self._sendVortexMsg()
|
|
162
|
+
self._armTimeout()
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
failure = Failure(TimeoutError("timed out"))
|
|
166
|
+
self._timedOut(failure)
|
|
167
|
+
self.errback(failure)
|
|
127
168
|
|
|
128
169
|
@classmethod
|
|
129
170
|
def isResponsePayloadEnvelope(cls, payloadEnvelope: PayloadEnvelope):
|
|
@@ -162,6 +203,12 @@ class PayloadResponse(Deferred):
|
|
|
162
203
|
self._endpoint.shutdown()
|
|
163
204
|
self._endpoint = None
|
|
164
205
|
|
|
206
|
+
if self._timeoutDelayedCall is not None:
|
|
207
|
+
# zope Interface methods omit self, which static analysis cannot bind
|
|
208
|
+
if self._timeoutDelayedCall.active(): # pyright: ignore[reportAttributeAccessIssue]
|
|
209
|
+
self._timeoutDelayedCall.cancel() # pyright: ignore[reportAttributeAccessIssue]
|
|
210
|
+
self._timeoutDelayedCall = None
|
|
211
|
+
|
|
165
212
|
if self._destVortexName and vortexName != self._destVortexName:
|
|
166
213
|
logger.debug(
|
|
167
214
|
"Received response from a vortex other than the dest vortex, "
|
|
@@ -175,14 +222,10 @@ class PayloadResponse(Deferred):
|
|
|
175
222
|
logger.error("Received response after timeout for %s" % self._filt)
|
|
176
223
|
return
|
|
177
224
|
|
|
178
|
-
if self._resultCheck and
|
|
225
|
+
if self._resultCheck and payloadEnvelope.result not in (None, True):
|
|
179
226
|
self._status = self.FAILED
|
|
180
227
|
self.errback(
|
|
181
|
-
Failure(
|
|
182
|
-
Exception(payloadEnvelope.result).with_traceback(
|
|
183
|
-
self._stack
|
|
184
|
-
)
|
|
185
|
-
)
|
|
228
|
+
Failure(Exception(payloadEnvelope.result).with_traceback(self._stack))
|
|
186
229
|
)
|
|
187
230
|
|
|
188
231
|
else:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from twisted.internet.defer import TimeoutError
|
|
5
|
+
from twisted.internet.defer import succeed
|
|
6
|
+
from twisted.python.failure import Failure
|
|
7
|
+
|
|
8
|
+
from vortex.PayloadEnvelope import PayloadEnvelope
|
|
9
|
+
from vortex.PayloadResponse import PayloadResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _FakeDelayedCall:
|
|
13
|
+
def __init__(self, callback) -> None:
|
|
14
|
+
self.callback = callback
|
|
15
|
+
self.cancelled = False
|
|
16
|
+
|
|
17
|
+
def active(self) -> bool:
|
|
18
|
+
return not self.cancelled
|
|
19
|
+
|
|
20
|
+
def cancel(self) -> None:
|
|
21
|
+
self.cancelled = True
|
|
22
|
+
|
|
23
|
+
def fire(self) -> None:
|
|
24
|
+
self.callback()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _FakeReactor:
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self.delayedCalls: list[_FakeDelayedCall] = []
|
|
30
|
+
|
|
31
|
+
def callLater(self, _seconds: float, callback) -> _FakeDelayedCall:
|
|
32
|
+
call = _FakeDelayedCall(callback)
|
|
33
|
+
self.delayedCalls.append(call)
|
|
34
|
+
return call
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PayloadResponseLoggingTest(unittest.TestCase):
|
|
38
|
+
@patch("vortex.PayloadResponse.VortexFactory.sendVortexMsg")
|
|
39
|
+
@patch("vortex.PayloadResponse.logger")
|
|
40
|
+
def testTimeoutLogsDebugWhenLogTimeoutExceptionFalse(
|
|
41
|
+
self, mockLogger: MagicMock, _mockSend: MagicMock
|
|
42
|
+
) -> None:
|
|
43
|
+
pr = PayloadResponse(
|
|
44
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
45
|
+
timeout=30.0,
|
|
46
|
+
logTimeoutException=False,
|
|
47
|
+
)
|
|
48
|
+
pr._timedOut(Failure(TimeoutError("timed out")))
|
|
49
|
+
mockLogger.error.assert_not_called()
|
|
50
|
+
mockLogger.debug.assert_called_once()
|
|
51
|
+
|
|
52
|
+
@patch("vortex.PayloadResponse.VortexFactory.sendVortexMsg")
|
|
53
|
+
@patch("vortex.PayloadResponse.logger")
|
|
54
|
+
def testTimeoutLogsErrorWhenLogTimeoutExceptionTrue(
|
|
55
|
+
self, mockLogger: MagicMock, _mockSend: MagicMock
|
|
56
|
+
) -> None:
|
|
57
|
+
pr = PayloadResponse(
|
|
58
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
59
|
+
timeout=30.0,
|
|
60
|
+
logTimeoutException=True,
|
|
61
|
+
)
|
|
62
|
+
pr._timedOut(Failure(TimeoutError("timed out")))
|
|
63
|
+
mockLogger.debug.assert_not_called()
|
|
64
|
+
mockLogger.error.assert_called_once()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PayloadResponseRetryTest(unittest.TestCase):
|
|
68
|
+
def setUp(self) -> None:
|
|
69
|
+
self.fakeReactor = _FakeReactor()
|
|
70
|
+
patch("vortex.PayloadResponse.reactor", self.fakeReactor).start()
|
|
71
|
+
self.mockSend: MagicMock = patch(
|
|
72
|
+
"vortex.PayloadResponse.VortexFactory.sendVortexMsg"
|
|
73
|
+
).start()
|
|
74
|
+
patch(
|
|
75
|
+
"vortex.PayloadEnvelope.PayloadEnvelope.toVortexMsgDefer",
|
|
76
|
+
return_value=succeed(b"{}"),
|
|
77
|
+
).start()
|
|
78
|
+
self.addCleanup(patch.stopall)
|
|
79
|
+
|
|
80
|
+
def testTimeoutResendsPayloadAndSucceedsOnLateResponse(self) -> None:
|
|
81
|
+
pr = PayloadResponse(
|
|
82
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
83
|
+
destVortexName="destVortex",
|
|
84
|
+
timeout=30.0,
|
|
85
|
+
retries=1,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.assertEqual(self.mockSend.call_count, 1)
|
|
89
|
+
self.assertEqual(len(self.fakeReactor.delayedCalls), 1)
|
|
90
|
+
|
|
91
|
+
# First attempt times out, the payload is resent and re-armed
|
|
92
|
+
self.fakeReactor.delayedCalls[0].fire()
|
|
93
|
+
|
|
94
|
+
self.assertEqual(self.mockSend.call_count, 2)
|
|
95
|
+
self.assertEqual(len(self.fakeReactor.delayedCalls), 2)
|
|
96
|
+
self.assertEqual(pr.status, PayloadResponse.PROCESSING)
|
|
97
|
+
|
|
98
|
+
# The resent payload receives a response
|
|
99
|
+
results = []
|
|
100
|
+
pr.addCallback(results.append)
|
|
101
|
+
response = PayloadEnvelope(filt={"key": "example"})
|
|
102
|
+
pr._process(response, "vortexUuid", "destVortex")
|
|
103
|
+
|
|
104
|
+
self.assertEqual(pr.status, PayloadResponse.SUCCESS)
|
|
105
|
+
self.assertEqual(results, [response])
|
|
106
|
+
self.assertTrue(self.fakeReactor.delayedCalls[1].cancelled)
|
|
107
|
+
|
|
108
|
+
def testExhaustedRetriesErrbackWithTimedOut(self) -> None:
|
|
109
|
+
pr = PayloadResponse(
|
|
110
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
111
|
+
destVortexName="destVortex",
|
|
112
|
+
timeout=30.0,
|
|
113
|
+
retries=1,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
failures = []
|
|
117
|
+
pr.addErrback(failures.append)
|
|
118
|
+
|
|
119
|
+
self.fakeReactor.delayedCalls[0].fire()
|
|
120
|
+
self.assertEqual(self.mockSend.call_count, 2)
|
|
121
|
+
self.assertEqual(pr.status, PayloadResponse.PROCESSING)
|
|
122
|
+
|
|
123
|
+
self.fakeReactor.delayedCalls[1].fire()
|
|
124
|
+
|
|
125
|
+
self.assertEqual(self.mockSend.call_count, 2)
|
|
126
|
+
self.assertEqual(pr.status, PayloadResponse.TIMED_OUT)
|
|
127
|
+
self.assertEqual(len(failures), 1)
|
|
128
|
+
self.assertIsInstance(failures[0].value, TimeoutError)
|
|
129
|
+
|
|
130
|
+
def testRetriesRequiresDestinationVortex(self) -> None:
|
|
131
|
+
with self.assertRaises(AssertionError):
|
|
132
|
+
PayloadResponse(
|
|
133
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
134
|
+
timeout=30.0,
|
|
135
|
+
retries=1,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def testDefaultTimeoutPathDoesNotResend(self) -> None:
|
|
139
|
+
pr = PayloadResponse(
|
|
140
|
+
PayloadEnvelope(filt={"key": "example"}),
|
|
141
|
+
destVortexName="destVortex",
|
|
142
|
+
timeout=30.0,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
failures = []
|
|
146
|
+
pr.addErrback(failures.append)
|
|
147
|
+
|
|
148
|
+
self.assertEqual(self.mockSend.call_count, 1)
|
|
149
|
+
self.assertEqual(len(self.fakeReactor.delayedCalls), 1)
|
|
150
|
+
|
|
151
|
+
# addTimeout cancels the deferred, which errbacks with TimeoutError
|
|
152
|
+
self.fakeReactor.delayedCalls[0].fire()
|
|
153
|
+
|
|
154
|
+
self.assertEqual(self.mockSend.call_count, 1)
|
|
155
|
+
self.assertEqual(pr.status, PayloadResponse.TIMED_OUT)
|
|
156
|
+
self.assertEqual(len(failures), 1)
|
|
157
|
+
self.assertIsInstance(failures[0].value, TimeoutError)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
unittest.main()
|
|
@@ -12,9 +12,9 @@ import functools
|
|
|
12
12
|
import logging
|
|
13
13
|
import types
|
|
14
14
|
import typing
|
|
15
|
-
from base64 import
|
|
15
|
+
from base64 import b64decode, b64encode
|
|
16
16
|
from collections import defaultdict
|
|
17
|
-
from datetime import UTC,
|
|
17
|
+
from datetime import UTC, date, datetime
|
|
18
18
|
|
|
19
19
|
import pytz
|
|
20
20
|
from sqlalchemy.orm.collections import InstrumentedList
|
|
@@ -114,15 +114,15 @@ def convertFromWkbElement(value):
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
def serialiseWkb(wkb: WKBElement) -> str:
|
|
117
|
-
from geoalchemy2.shape import to_shape
|
|
118
117
|
import shapely
|
|
118
|
+
from geoalchemy2.shape import to_shape
|
|
119
119
|
|
|
120
120
|
return b64encode(shapely.wkb.dumps(to_shape(wkb))).decode()
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def deserialiseWkb(wkbB64Data: str) -> WKBElement:
|
|
124
|
-
from geoalchemy2.shape import from_shape
|
|
125
124
|
import shapely
|
|
125
|
+
from geoalchemy2.shape import from_shape
|
|
126
126
|
|
|
127
127
|
return from_shape(shapely.wkb.loads(b64decode(wkbB64Data)))
|
|
128
128
|
|
|
@@ -253,9 +253,7 @@ def fromStr(val: str, typeName: str):
|
|
|
253
253
|
if typeName == T_DATETIME:
|
|
254
254
|
# TODO: Check with ISO8601_JS_REGEXP
|
|
255
255
|
if len(val) == len("2017-06-06T09:40:37.097Z"):
|
|
256
|
-
return datetime.strptime(val, ISO8601_JS_FORMAT).replace(
|
|
257
|
-
tzinfo=pytz.UTC
|
|
258
|
-
)
|
|
256
|
+
return datetime.strptime(val, ISO8601_JS_FORMAT).replace(tzinfo=pytz.UTC)
|
|
259
257
|
|
|
260
258
|
if len(val) <= len("2017-06-06 09:40:37.097068"):
|
|
261
259
|
raise ValueError(
|
|
@@ -286,7 +284,7 @@ def fromRapuiType(typeName: str):
|
|
|
286
284
|
return TYPE_MAP_RAPUI_TO_PY[typeName]
|
|
287
285
|
|
|
288
286
|
|
|
289
|
-
@functools.
|
|
287
|
+
@functools.cache
|
|
290
288
|
def utilTypeHintsForClass(cls: type) -> dict[str, typing.Any]:
|
|
291
289
|
"""Resolved type hints for a class, cached per class.
|
|
292
290
|
|
|
@@ -328,6 +326,29 @@ def utilEnumClassFromTypeHint(typeHint: typing.Any) -> type[enum.Enum] | None:
|
|
|
328
326
|
return None
|
|
329
327
|
|
|
330
328
|
|
|
329
|
+
def utilTupleClassFromTypeHint(typeHint: typing.Any) -> type | None:
|
|
330
|
+
"""The ``vortex.Tuple`` subclass for a field type hint, or None.
|
|
331
|
+
|
|
332
|
+
Handles bare ``SomeTuple``, ``SomeTuple | None`` and
|
|
333
|
+
``Optional[SomeTuple]``; every other hint returns None.
|
|
334
|
+
"""
|
|
335
|
+
from vortex.Tuple import Tuple
|
|
336
|
+
|
|
337
|
+
if typeHint is None:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
if isinstance(typeHint, type) and issubclass(typeHint, Tuple):
|
|
341
|
+
return typeHint
|
|
342
|
+
|
|
343
|
+
origin = typing.get_origin(typeHint)
|
|
344
|
+
if origin in (typing.Union, types.UnionType):
|
|
345
|
+
for arg in typing.get_args(typeHint):
|
|
346
|
+
if isinstance(arg, type) and issubclass(arg, Tuple):
|
|
347
|
+
return arg
|
|
348
|
+
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
|
|
331
352
|
def toRapuiType(value):
|
|
332
353
|
if value is None:
|
|
333
354
|
return V_NULL
|