vortexpy 4.0.29__tar.gz → 4.1.2__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.0.29 → vortexpy-4.1.2}/PKG-INFO +1 -1
- {vortexpy-4.0.29 → vortexpy-4.1.2}/pyproject.toml +1 -1
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/Tuple.py +17 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TupleRestful.py +32 -1
- vortexpy-4.1.2/vortex/__init__.py +9 -0
- vortexpy-4.1.2/vortex/tuple_json_schema/__init__.py +18 -0
- vortexpy-4.1.2/vortex/tuple_json_schema/constants.py +35 -0
- vortexpy-4.1.2/vortex/tuple_json_schema/json_schema_node_tuple.py +99 -0
- vortexpy-4.1.2/vortex/tuple_json_schema/tuple_json_schema_builder.py +225 -0
- vortexpy-4.1.2/vortex/tuple_json_schema/tuple_json_schema_test.py +257 -0
- vortexpy-4.1.2/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +69 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortexpy.egg-info/SOURCES.txt +6 -0
- vortexpy-4.1.2/vortexpy.egg-info/dependency_links.txt +1 -0
- vortexpy-4.0.29/vortex/__init__.py +0 -9
- vortexpy-4.0.29/vortex/py.typed +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/README.md +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/setup.cfg +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/DeferUtil.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/Jsonable.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/Payload.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadEndpoint.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadEndpointTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadEnvelope.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadIO.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadResponse.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/SerialiseUtil.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TestTuple.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TupleAction.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TupleRestfulTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/TupleTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexAccessControlContextABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexClientTcp.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexClientWebsocketFactory.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexFactory.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexPayloadProtocol.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexPermission.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexPermissionError.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexServer.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexSession.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexUtil.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/VortexWritePushProducer.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/data_loader/TupleDataLoader.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/event_bus/TupleChangeEventBus.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleActionProcessor.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleActionProcessorProxy.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataObservableCache.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataObservableHandler.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataObserverClient.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/TupleDataProviderABC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/mypy_plugin.py +0 -0
- /vortexpy-4.0.29/vortexpy.egg-info/dependency_links.txt → /vortexpy-4.1.2/vortex/py.typed +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/RestfulHttpClientTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/RestfulResourceTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/rpc/RPC.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/storage/TupleStorageSqlite.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.0.29 → vortexpy-4.1.2}/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.
|
|
7
|
+
version = "4.1.2"
|
|
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."
|
|
@@ -595,6 +595,23 @@ class Tuple(Jsonable, _TupleToPlainJsonMixin, _TupleToSqlaJsonMixin):
|
|
|
595
595
|
def tupleFieldNames(cls) -> List[str]:
|
|
596
596
|
return cls.__fieldNames__
|
|
597
597
|
|
|
598
|
+
@classmethod
|
|
599
|
+
def tupleJsonSchema(cls) -> "JsonSchemaNodeTuple":
|
|
600
|
+
"""Tuple JSON Schema
|
|
601
|
+
|
|
602
|
+
Build a typed Draft 2020-12 subset schema tree from this tuple's
|
|
603
|
+
``TupleField`` metadata.
|
|
604
|
+
|
|
605
|
+
:return: Root ``JsonSchemaNodeTuple``. Call ``buildJsonSchemaDict()``
|
|
606
|
+
for Draft 2020-12 JSON Schema keys, or ``tupleToRestfulJsonDict()``
|
|
607
|
+
for Vortex restful JSON.
|
|
608
|
+
"""
|
|
609
|
+
from vortex.tuple_json_schema.tuple_json_schema_builder import (
|
|
610
|
+
TupleJsonSchemaBuilder,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
return TupleJsonSchemaBuilder.buildJsonSchema(cls)
|
|
614
|
+
|
|
598
615
|
@classmethod
|
|
599
616
|
def tupleName(cls): # DEPRECIATED
|
|
600
617
|
return cls.__tupleType__
|
|
@@ -19,7 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
from typing import TYPE_CHECKING, Annotated, Any, NamedTuple, Optional, Type
|
|
21
21
|
|
|
22
|
-
from typing import get_args
|
|
22
|
+
from typing import Union, get_args
|
|
23
23
|
from typing import get_origin
|
|
24
24
|
from typing import get_type_hints
|
|
25
25
|
|
|
@@ -256,6 +256,24 @@ class TupleRestfulConverter:
|
|
|
256
256
|
|
|
257
257
|
return True
|
|
258
258
|
|
|
259
|
+
@classmethod
|
|
260
|
+
def _nestedTupleTypeFromHint(cls, typeHint: Any) -> Any | None:
|
|
261
|
+
"""Return nested tuple class for a field hint, including ``X | None`` unions."""
|
|
262
|
+
if hasattr(typeHint, "__tupleType__"):
|
|
263
|
+
return typeHint
|
|
264
|
+
|
|
265
|
+
unionArgs = get_args(typeHint)
|
|
266
|
+
if not unionArgs:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
for unionArg in unionArgs:
|
|
270
|
+
if unionArg is type(None):
|
|
271
|
+
continue
|
|
272
|
+
if hasattr(unionArg, "__tupleType__"):
|
|
273
|
+
return unionArg
|
|
274
|
+
|
|
275
|
+
return None
|
|
276
|
+
|
|
259
277
|
@classmethod
|
|
260
278
|
def _checkType(
|
|
261
279
|
cls,
|
|
@@ -284,6 +302,9 @@ class TupleRestfulConverter:
|
|
|
284
302
|
if hasattr(typeHint, "__tupleType__"):
|
|
285
303
|
return None
|
|
286
304
|
|
|
305
|
+
if cls._nestedTupleTypeFromHint(typeHint) is not None:
|
|
306
|
+
return None
|
|
307
|
+
|
|
287
308
|
if type(value) in get_args(typeHint):
|
|
288
309
|
if validator:
|
|
289
310
|
try:
|
|
@@ -425,6 +446,16 @@ class TupleRestfulConverter:
|
|
|
425
446
|
cls._prefixValidationErrors(child.errors, field_path),
|
|
426
447
|
)
|
|
427
448
|
|
|
449
|
+
nestedTupleType = cls._nestedTupleTypeFromHint(typeHint)
|
|
450
|
+
if nestedTupleType is not None and nestedTupleType is not typeHint:
|
|
451
|
+
child = cls.restfulJsonDictToTupleWithValidationResult(
|
|
452
|
+
value, nestedTupleType
|
|
453
|
+
)
|
|
454
|
+
return _ConvertResult(
|
|
455
|
+
child.tuple_,
|
|
456
|
+
cls._prefixValidationErrors(child.errors, field_path),
|
|
457
|
+
)
|
|
458
|
+
|
|
428
459
|
if not get_args(typeHint):
|
|
429
460
|
return _ConvertResult(
|
|
430
461
|
_UNSET,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""Tuple JSON Schema — typed Vortex tuples for Draft 2020-12 subset schemas.
|
|
10
|
+
|
|
11
|
+
Import from submodules::
|
|
12
|
+
|
|
13
|
+
from vortex.tuple_json_schema.json_schema_node_tuple import JsonSchemaNodeTuple
|
|
14
|
+
from vortex.tuple_json_schema.tuple_json_schema_builder import (
|
|
15
|
+
TupleJsonSchemaBuilder,
|
|
16
|
+
)
|
|
17
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_STRING
|
|
18
|
+
"""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""JSON Schema string constants for Draft 2020-12 subset."""
|
|
10
|
+
|
|
11
|
+
JSON_SCHEMA_DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema"
|
|
12
|
+
|
|
13
|
+
JSON_SCHEMA_KEY_SCHEMA = "$schema"
|
|
14
|
+
JSON_SCHEMA_KEY_DEFS = "$defs"
|
|
15
|
+
JSON_SCHEMA_KEY_REF = "$ref"
|
|
16
|
+
JSON_SCHEMA_KEY_TYPE = "type"
|
|
17
|
+
JSON_SCHEMA_KEY_TITLE = "title"
|
|
18
|
+
JSON_SCHEMA_KEY_PROPERTIES = "properties"
|
|
19
|
+
JSON_SCHEMA_KEY_REQUIRED = "required"
|
|
20
|
+
JSON_SCHEMA_KEY_ITEMS = "items"
|
|
21
|
+
JSON_SCHEMA_KEY_FORMAT = "format"
|
|
22
|
+
JSON_SCHEMA_KEY_CONTENT_ENCODING = "contentEncoding"
|
|
23
|
+
JSON_SCHEMA_KEY_DESCRIPTION = "description"
|
|
24
|
+
|
|
25
|
+
JSON_SCHEMA_TYPE_OBJECT = "object"
|
|
26
|
+
JSON_SCHEMA_TYPE_STRING = "string"
|
|
27
|
+
JSON_SCHEMA_TYPE_INTEGER = "integer"
|
|
28
|
+
JSON_SCHEMA_TYPE_NUMBER = "number"
|
|
29
|
+
JSON_SCHEMA_TYPE_BOOLEAN = "boolean"
|
|
30
|
+
JSON_SCHEMA_TYPE_ARRAY = "array"
|
|
31
|
+
|
|
32
|
+
JSON_SCHEMA_FORMAT_DATE_TIME = "date-time"
|
|
33
|
+
JSON_SCHEMA_CONTENT_ENCODING_BINARY = "binary"
|
|
34
|
+
|
|
35
|
+
JSON_SCHEMA_DEFS_REF_PREFIX = f"#/{JSON_SCHEMA_KEY_DEFS}/"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""JSON Schema node as a Vortex tuple (Draft 2020-12 subset)."""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from vortex.Tuple import Tuple
|
|
16
|
+
from vortex.Tuple import TupleField
|
|
17
|
+
from vortex.Tuple import addTupleType
|
|
18
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_CONTENT_ENCODING
|
|
19
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_DEFS
|
|
20
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_DESCRIPTION
|
|
21
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_FORMAT
|
|
22
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_ITEMS
|
|
23
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_PROPERTIES
|
|
24
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_REF
|
|
25
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_REQUIRED
|
|
26
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_SCHEMA
|
|
27
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_TITLE
|
|
28
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_TYPE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@addTupleType
|
|
32
|
+
class JsonSchemaNodeTuple(Tuple):
|
|
33
|
+
"""JSON Schema Node Tuple
|
|
34
|
+
|
|
35
|
+
Typed Draft 2020-12 subset node. Application code uses this tuple;
|
|
36
|
+
call ``buildJsonSchemaDict()`` only at a boundary that needs raw JSON Schema
|
|
37
|
+
(``$schema`` / ``$ref`` / ``$defs`` keys). Prefer
|
|
38
|
+
``tupleToRestfulJsonDict()`` when serialising as a normal Vortex tuple.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__tupleType__ = "vortex.JsonSchemaNodeTuple"
|
|
42
|
+
|
|
43
|
+
schemaUri: str | None = TupleField(None, typingType=str | None)
|
|
44
|
+
schemaType: str | None = TupleField(None, typingType=str | None)
|
|
45
|
+
title: str | None = TupleField(None, typingType=str | None)
|
|
46
|
+
description: str | None = TupleField(None, typingType=str | None)
|
|
47
|
+
format: str | None = TupleField(None, typingType=str | None)
|
|
48
|
+
contentEncoding: str | None = TupleField(None, typingType=str | None)
|
|
49
|
+
ref: str | None = TupleField(None, typingType=str | None)
|
|
50
|
+
items: JsonSchemaNodeTuple | None = TupleField(None)
|
|
51
|
+
properties: dict[str, JsonSchemaNodeTuple] | None = TupleField(None)
|
|
52
|
+
required: list[str] | None = TupleField(None, typingType=list[str] | None)
|
|
53
|
+
defs: dict[str, JsonSchemaNodeTuple] | None = TupleField(None)
|
|
54
|
+
|
|
55
|
+
def buildJsonSchemaDict(self) -> dict[str, Any]:
|
|
56
|
+
"""Build JSON Schema Dict
|
|
57
|
+
|
|
58
|
+
Emit a Draft 2020-12 subset dict with JSON Schema keys
|
|
59
|
+
(``$schema``, ``$ref``, ``$defs``, ``type``, …).
|
|
60
|
+
|
|
61
|
+
:return: JSON-serialisable schema dict.
|
|
62
|
+
"""
|
|
63
|
+
out: dict[str, Any] = {}
|
|
64
|
+
if self.schemaUri is not None:
|
|
65
|
+
out[JSON_SCHEMA_KEY_SCHEMA] = self.schemaUri
|
|
66
|
+
if self.schemaType is not None:
|
|
67
|
+
out[JSON_SCHEMA_KEY_TYPE] = self.schemaType
|
|
68
|
+
if self.title is not None:
|
|
69
|
+
out[JSON_SCHEMA_KEY_TITLE] = self.title
|
|
70
|
+
if self.description is not None:
|
|
71
|
+
out[JSON_SCHEMA_KEY_DESCRIPTION] = self.description
|
|
72
|
+
if self.format is not None:
|
|
73
|
+
out[JSON_SCHEMA_KEY_FORMAT] = self.format
|
|
74
|
+
if self.contentEncoding is not None:
|
|
75
|
+
out[JSON_SCHEMA_KEY_CONTENT_ENCODING] = self.contentEncoding
|
|
76
|
+
if self.ref is not None:
|
|
77
|
+
out[JSON_SCHEMA_KEY_REF] = self.ref
|
|
78
|
+
if self.items is not None:
|
|
79
|
+
out[JSON_SCHEMA_KEY_ITEMS] = self.items.buildJsonSchemaDict()
|
|
80
|
+
if self.properties is not None:
|
|
81
|
+
out[JSON_SCHEMA_KEY_PROPERTIES] = {
|
|
82
|
+
name: node.buildJsonSchemaDict()
|
|
83
|
+
for name, node in self.properties.items()
|
|
84
|
+
}
|
|
85
|
+
if self.required is not None:
|
|
86
|
+
out[JSON_SCHEMA_KEY_REQUIRED] = list(self.required)
|
|
87
|
+
if self.defs is not None:
|
|
88
|
+
out[JSON_SCHEMA_KEY_DEFS] = {
|
|
89
|
+
name: node.buildJsonSchemaDict()
|
|
90
|
+
for name, node in self.defs.items()
|
|
91
|
+
}
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
JsonSchemaNodeTuple.items.typingType = JsonSchemaNodeTuple | None
|
|
96
|
+
JsonSchemaNodeTuple.properties.typingType = (
|
|
97
|
+
dict[str, JsonSchemaNodeTuple] | None
|
|
98
|
+
)
|
|
99
|
+
JsonSchemaNodeTuple.defs.typingType = dict[str, JsonSchemaNodeTuple] | None
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""Build a ``JsonSchemaNodeTuple`` tree from ``TupleField`` metadata."""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import types
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
from typing import Dict
|
|
17
|
+
from typing import List
|
|
18
|
+
from typing import Union
|
|
19
|
+
from typing import get_args
|
|
20
|
+
from typing import get_origin
|
|
21
|
+
|
|
22
|
+
from vortex.Tuple import Tuple
|
|
23
|
+
from vortex.Tuple import TupleField
|
|
24
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_CONTENT_ENCODING_BINARY
|
|
25
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_DEFS_REF_PREFIX
|
|
26
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_DRAFT_2020_12
|
|
27
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_FORMAT_DATE_TIME
|
|
28
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_ARRAY
|
|
29
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_BOOLEAN
|
|
30
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_INTEGER
|
|
31
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_NUMBER
|
|
32
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_OBJECT
|
|
33
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_STRING
|
|
34
|
+
from vortex.tuple_json_schema.json_schema_node_tuple import JsonSchemaNodeTuple
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TupleJsonSchemaBuilder:
|
|
38
|
+
"""Tuple JSON Schema Builder
|
|
39
|
+
|
|
40
|
+
Builds a ``JsonSchemaNodeTuple`` tree from a ``Tuple`` subclass using
|
|
41
|
+
``TupleField.typingType``, ``defaultValue``, and ``comment``.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def buildJsonSchema(cls, tupleCls: type[Tuple]) -> JsonSchemaNodeTuple:
|
|
46
|
+
"""Build JSON Schema Node
|
|
47
|
+
|
|
48
|
+
Produce a typed schema tree for ``tupleCls`` from its ``TupleField``
|
|
49
|
+
entries.
|
|
50
|
+
|
|
51
|
+
:param tupleCls: Registered ``Tuple`` subclass to describe.
|
|
52
|
+
:return: Root ``JsonSchemaNodeTuple`` (call ``buildJsonSchemaDict()``
|
|
53
|
+
for Draft 2020-12 JSON Schema keys).
|
|
54
|
+
"""
|
|
55
|
+
builder = cls()
|
|
56
|
+
return builder._buildRoot(tupleCls)
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._defs: dict[str, JsonSchemaNodeTuple] = {}
|
|
60
|
+
self._building: set[str] = set()
|
|
61
|
+
|
|
62
|
+
def _buildRoot(self, tupleCls: type[Tuple]) -> JsonSchemaNodeTuple:
|
|
63
|
+
# Build object schema for the root tuple type
|
|
64
|
+
objectSchema = self._buildObjectSchema(tupleCls)
|
|
65
|
+
rootTupleType = tupleCls.tupleType()
|
|
66
|
+
|
|
67
|
+
# Self-references emit $ref into defs; place the root object schema there
|
|
68
|
+
# (without draft URI or nested defs).
|
|
69
|
+
if rootTupleType in self._defs:
|
|
70
|
+
defsCopy = objectSchema.tupleClone(deep=True)
|
|
71
|
+
defsCopy.schemaUri = None
|
|
72
|
+
defsCopy.defs = None
|
|
73
|
+
self._defs[rootTupleType] = defsCopy
|
|
74
|
+
|
|
75
|
+
# Assemble root document with draft URI and optional defs
|
|
76
|
+
objectSchema.schemaUri = JSON_SCHEMA_DRAFT_2020_12
|
|
77
|
+
if self._defs:
|
|
78
|
+
objectSchema.defs = dict(self._defs)
|
|
79
|
+
|
|
80
|
+
return objectSchema
|
|
81
|
+
|
|
82
|
+
def _buildObjectSchema(self, tupleCls: type[Tuple]) -> JsonSchemaNodeTuple:
|
|
83
|
+
tupleTypeName = tupleCls.tupleType()
|
|
84
|
+
self._building.add(tupleTypeName)
|
|
85
|
+
|
|
86
|
+
# Map each TupleField to a property schema and required list
|
|
87
|
+
properties: dict[str, JsonSchemaNodeTuple] = {}
|
|
88
|
+
required: list[str] = []
|
|
89
|
+
for fieldName in tupleCls.tupleFieldNames():
|
|
90
|
+
field = getattr(tupleCls, fieldName, None)
|
|
91
|
+
if not isinstance(field, TupleField):
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
propSchema = self._schemaForTypingType(field.typingType)
|
|
95
|
+
if field.comment:
|
|
96
|
+
propSchema.description = field.comment
|
|
97
|
+
|
|
98
|
+
properties[fieldName] = propSchema
|
|
99
|
+
if not self._isOptionalField(field):
|
|
100
|
+
required.append(fieldName)
|
|
101
|
+
|
|
102
|
+
self._building.discard(tupleTypeName)
|
|
103
|
+
|
|
104
|
+
# Assemble object schema
|
|
105
|
+
objectSchema = JsonSchemaNodeTuple(
|
|
106
|
+
schemaType=JSON_SCHEMA_TYPE_OBJECT,
|
|
107
|
+
title=tupleTypeName,
|
|
108
|
+
properties=properties,
|
|
109
|
+
)
|
|
110
|
+
if required:
|
|
111
|
+
objectSchema.required = required
|
|
112
|
+
|
|
113
|
+
return objectSchema
|
|
114
|
+
|
|
115
|
+
def _isOptionalField(self, field: TupleField) -> bool:
|
|
116
|
+
if field.defaultValue is None:
|
|
117
|
+
return True
|
|
118
|
+
return self._typingIncludesNone(field.typingType)
|
|
119
|
+
|
|
120
|
+
def _typingIncludesNone(self, typingType: Any) -> bool:
|
|
121
|
+
if typingType is None:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
origin = get_origin(typingType)
|
|
125
|
+
if origin is Union or origin is types.UnionType:
|
|
126
|
+
return type(None) in get_args(typingType)
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def _unwrapOptional(self, typingType: Any) -> Any:
|
|
130
|
+
origin = get_origin(typingType)
|
|
131
|
+
if origin is not Union and origin is not types.UnionType:
|
|
132
|
+
return typingType
|
|
133
|
+
|
|
134
|
+
nonNoneArgs = [
|
|
135
|
+
arg for arg in get_args(typingType) if arg is not type(None)
|
|
136
|
+
]
|
|
137
|
+
if len(nonNoneArgs) == 1:
|
|
138
|
+
return nonNoneArgs[0]
|
|
139
|
+
|
|
140
|
+
if not nonNoneArgs:
|
|
141
|
+
return typingType
|
|
142
|
+
|
|
143
|
+
raise NotImplementedError(
|
|
144
|
+
f"Unsupported multi-member Union for JSON Schema: {typingType!r}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _schemaForTypingType(self, typingType: Any) -> JsonSchemaNodeTuple:
|
|
148
|
+
# Missing typingType maps to string (TupleField default)
|
|
149
|
+
if typingType is None:
|
|
150
|
+
return JsonSchemaNodeTuple(schemaType=JSON_SCHEMA_TYPE_STRING)
|
|
151
|
+
|
|
152
|
+
# Scalar and built-in types
|
|
153
|
+
typingType = self._unwrapOptional(typingType)
|
|
154
|
+
if typingType is str:
|
|
155
|
+
return JsonSchemaNodeTuple(schemaType=JSON_SCHEMA_TYPE_STRING)
|
|
156
|
+
|
|
157
|
+
if typingType is int:
|
|
158
|
+
return JsonSchemaNodeTuple(schemaType=JSON_SCHEMA_TYPE_INTEGER)
|
|
159
|
+
|
|
160
|
+
if typingType is float:
|
|
161
|
+
return JsonSchemaNodeTuple(schemaType=JSON_SCHEMA_TYPE_NUMBER)
|
|
162
|
+
|
|
163
|
+
if typingType is bool:
|
|
164
|
+
return JsonSchemaNodeTuple(schemaType=JSON_SCHEMA_TYPE_BOOLEAN)
|
|
165
|
+
|
|
166
|
+
if typingType is datetime:
|
|
167
|
+
return JsonSchemaNodeTuple(
|
|
168
|
+
schemaType=JSON_SCHEMA_TYPE_STRING,
|
|
169
|
+
format=JSON_SCHEMA_FORMAT_DATE_TIME,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if typingType in (bytes, bytearray):
|
|
173
|
+
return JsonSchemaNodeTuple(
|
|
174
|
+
schemaType=JSON_SCHEMA_TYPE_STRING,
|
|
175
|
+
contentEncoding=JSON_SCHEMA_CONTENT_ENCODING_BINARY,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Collections
|
|
179
|
+
origin = get_origin(typingType)
|
|
180
|
+
if origin in (list, List) or typingType is list:
|
|
181
|
+
args = get_args(typingType)
|
|
182
|
+
itemType = args[0] if args else None
|
|
183
|
+
return JsonSchemaNodeTuple(
|
|
184
|
+
schemaType=JSON_SCHEMA_TYPE_ARRAY,
|
|
185
|
+
items=self._schemaForTypingType(itemType),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if origin in (dict, Dict) or typingType is dict:
|
|
189
|
+
return JsonSchemaNodeTuple(schemaType=JSON_SCHEMA_TYPE_OBJECT)
|
|
190
|
+
|
|
191
|
+
# Nested Tuple subclasses
|
|
192
|
+
if self._isTupleSubclass(typingType):
|
|
193
|
+
return self._refForTuple(typingType)
|
|
194
|
+
|
|
195
|
+
raise NotImplementedError(
|
|
196
|
+
f"Unsupported TupleField typingType for JSON Schema: {typingType!r}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _isTupleSubclass(self, typingType: Any) -> bool:
|
|
200
|
+
return (
|
|
201
|
+
isinstance(typingType, type)
|
|
202
|
+
and issubclass(typingType, Tuple)
|
|
203
|
+
and typingType is not Tuple
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _refForTuple(self, tupleCls: type[Tuple]) -> JsonSchemaNodeTuple:
|
|
207
|
+
tupleTypeName = tupleCls.tupleType()
|
|
208
|
+
ref = JsonSchemaNodeTuple(
|
|
209
|
+
ref=f"{JSON_SCHEMA_DEFS_REF_PREFIX}{tupleTypeName}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Cycle: emit $ref only; outer build places the object in defs
|
|
213
|
+
if tupleTypeName in self._building:
|
|
214
|
+
if tupleTypeName not in self._defs:
|
|
215
|
+
self._defs[tupleTypeName] = JsonSchemaNodeTuple(
|
|
216
|
+
schemaType=JSON_SCHEMA_TYPE_OBJECT,
|
|
217
|
+
title=tupleTypeName,
|
|
218
|
+
properties={},
|
|
219
|
+
)
|
|
220
|
+
return ref
|
|
221
|
+
|
|
222
|
+
if tupleTypeName not in self._defs:
|
|
223
|
+
self._defs[tupleTypeName] = self._buildObjectSchema(tupleCls)
|
|
224
|
+
|
|
225
|
+
return ref
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
from twisted.trial import unittest
|
|
10
|
+
|
|
11
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_CONTENT_ENCODING_BINARY
|
|
12
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_DEFS_REF_PREFIX
|
|
13
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_DRAFT_2020_12
|
|
14
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_FORMAT_DATE_TIME
|
|
15
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_DEFS
|
|
16
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_DESCRIPTION
|
|
17
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_FORMAT
|
|
18
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_ITEMS
|
|
19
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_PROPERTIES
|
|
20
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_REF
|
|
21
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_REQUIRED
|
|
22
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_SCHEMA
|
|
23
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_TITLE
|
|
24
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_KEY_TYPE
|
|
25
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_ARRAY
|
|
26
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_BOOLEAN
|
|
27
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_INTEGER
|
|
28
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_NUMBER
|
|
29
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_OBJECT
|
|
30
|
+
from vortex.tuple_json_schema.constants import JSON_SCHEMA_TYPE_STRING
|
|
31
|
+
from vortex.tuple_json_schema.json_schema_node_tuple import JsonSchemaNodeTuple
|
|
32
|
+
from vortex.tuple_json_schema.tuple_json_schema_test_tuples import (
|
|
33
|
+
JsonSchemaBareListTuple,
|
|
34
|
+
)
|
|
35
|
+
from vortex.tuple_json_schema.tuple_json_schema_test_tuples import (
|
|
36
|
+
JsonSchemaNestedTuple,
|
|
37
|
+
)
|
|
38
|
+
from vortex.tuple_json_schema.tuple_json_schema_test_tuples import (
|
|
39
|
+
JsonSchemaParentTuple,
|
|
40
|
+
)
|
|
41
|
+
from vortex.tuple_json_schema.tuple_json_schema_test_tuples import (
|
|
42
|
+
JsonSchemaScalarTuple,
|
|
43
|
+
)
|
|
44
|
+
from vortex.tuple_json_schema.tuple_json_schema_test_tuples import (
|
|
45
|
+
JsonSchemaSelfRefTuple,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TupleJsonSchemaTest(unittest.TestCase):
|
|
50
|
+
def testScalarsDatetimeAndOptional(self):
|
|
51
|
+
schema = JsonSchemaScalarTuple.tupleJsonSchema()
|
|
52
|
+
self.assertIsInstance(schema, JsonSchemaNodeTuple)
|
|
53
|
+
|
|
54
|
+
self.assertEqual(schema.schemaUri, JSON_SCHEMA_DRAFT_2020_12)
|
|
55
|
+
self.assertEqual(schema.schemaType, JSON_SCHEMA_TYPE_OBJECT)
|
|
56
|
+
self.assertEqual(schema.title, JsonSchemaScalarTuple.tupleType())
|
|
57
|
+
assert schema.properties is not None, (
|
|
58
|
+
"JsonSchemaScalarTuple schema.properties must be set"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.assertEqual(
|
|
62
|
+
schema.properties["aString"].schemaType, JSON_SCHEMA_TYPE_STRING
|
|
63
|
+
)
|
|
64
|
+
self.assertEqual(
|
|
65
|
+
schema.properties["aInt"].schemaType, JSON_SCHEMA_TYPE_INTEGER
|
|
66
|
+
)
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
schema.properties["aFloat"].schemaType, JSON_SCHEMA_TYPE_NUMBER
|
|
69
|
+
)
|
|
70
|
+
self.assertEqual(
|
|
71
|
+
schema.properties["aBool"].schemaType, JSON_SCHEMA_TYPE_BOOLEAN
|
|
72
|
+
)
|
|
73
|
+
self.assertEqual(
|
|
74
|
+
schema.properties["aDate"].schemaType, JSON_SCHEMA_TYPE_STRING
|
|
75
|
+
)
|
|
76
|
+
self.assertEqual(
|
|
77
|
+
schema.properties["aDate"].format, JSON_SCHEMA_FORMAT_DATE_TIME
|
|
78
|
+
)
|
|
79
|
+
self.assertEqual(
|
|
80
|
+
schema.properties["aOptional"].schemaType, JSON_SCHEMA_TYPE_STRING
|
|
81
|
+
)
|
|
82
|
+
self.assertEqual(
|
|
83
|
+
schema.properties["aBytes"].schemaType, JSON_SCHEMA_TYPE_STRING
|
|
84
|
+
)
|
|
85
|
+
self.assertEqual(
|
|
86
|
+
schema.properties["aBytes"].contentEncoding,
|
|
87
|
+
JSON_SCHEMA_CONTENT_ENCODING_BINARY,
|
|
88
|
+
)
|
|
89
|
+
self.assertEqual(
|
|
90
|
+
schema.properties["aDict"].schemaType, JSON_SCHEMA_TYPE_OBJECT
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert schema.required is not None, (
|
|
94
|
+
"JsonSchemaScalarTuple schema.required must be set"
|
|
95
|
+
)
|
|
96
|
+
self.assertIn("aString", schema.required)
|
|
97
|
+
self.assertIn("aDate", schema.required)
|
|
98
|
+
self.assertNotIn("aOptional", schema.required)
|
|
99
|
+
|
|
100
|
+
def testMissingTypingTypeFallsBackToString(self):
|
|
101
|
+
schema = JsonSchemaScalarTuple.tupleJsonSchema()
|
|
102
|
+
assert schema.properties is not None, (
|
|
103
|
+
"JsonSchemaScalarTuple schema.properties must be set for "
|
|
104
|
+
"missing typingType test"
|
|
105
|
+
)
|
|
106
|
+
self.assertEqual(
|
|
107
|
+
schema.properties["aMissingTyping"].schemaType,
|
|
108
|
+
JSON_SCHEMA_TYPE_STRING,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def testCommentBecomesDescription(self):
|
|
112
|
+
schema = JsonSchemaScalarTuple.tupleJsonSchema()
|
|
113
|
+
assert schema.properties is not None, (
|
|
114
|
+
"JsonSchemaScalarTuple schema.properties must be set for "
|
|
115
|
+
"comment description test"
|
|
116
|
+
)
|
|
117
|
+
aComment = schema.properties["aComment"]
|
|
118
|
+
self.assertEqual(aComment.description, "Field with a description")
|
|
119
|
+
self.assertEqual(aComment.schemaType, JSON_SCHEMA_TYPE_STRING)
|
|
120
|
+
|
|
121
|
+
def testNestedTupleDefsAndRef(self):
|
|
122
|
+
schema = JsonSchemaParentTuple.tupleJsonSchema()
|
|
123
|
+
nestedType = JsonSchemaNestedTuple.tupleType()
|
|
124
|
+
assert schema.properties is not None, (
|
|
125
|
+
"JsonSchemaParentTuple schema.properties must be set"
|
|
126
|
+
)
|
|
127
|
+
assert schema.defs is not None, (
|
|
128
|
+
"JsonSchemaParentTuple schema.defs must be set for nested tuple"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self.assertEqual(
|
|
132
|
+
schema.properties["child"].ref,
|
|
133
|
+
f"{JSON_SCHEMA_DEFS_REF_PREFIX}{nestedType}",
|
|
134
|
+
)
|
|
135
|
+
self.assertIn(nestedType, schema.defs)
|
|
136
|
+
nestedSchema = schema.defs[nestedType]
|
|
137
|
+
self.assertEqual(nestedSchema.schemaType, JSON_SCHEMA_TYPE_OBJECT)
|
|
138
|
+
self.assertEqual(nestedSchema.title, nestedType)
|
|
139
|
+
assert nestedSchema.properties is not None, (
|
|
140
|
+
"nested JsonSchemaNestedTuple defs entry properties must be set"
|
|
141
|
+
)
|
|
142
|
+
self.assertEqual(
|
|
143
|
+
nestedSchema.properties["nestedInt"].schemaType,
|
|
144
|
+
JSON_SCHEMA_TYPE_INTEGER,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def testListOfNestedTuple(self):
|
|
148
|
+
schema = JsonSchemaParentTuple.tupleJsonSchema()
|
|
149
|
+
nestedType = JsonSchemaNestedTuple.tupleType()
|
|
150
|
+
assert schema.properties is not None, (
|
|
151
|
+
"JsonSchemaParentTuple schema.properties must be set for list test"
|
|
152
|
+
)
|
|
153
|
+
children = schema.properties["children"]
|
|
154
|
+
|
|
155
|
+
self.assertEqual(children.schemaType, JSON_SCHEMA_TYPE_ARRAY)
|
|
156
|
+
assert children.items is not None, (
|
|
157
|
+
"JsonSchemaParentTuple children.items must be set"
|
|
158
|
+
)
|
|
159
|
+
self.assertEqual(
|
|
160
|
+
children.items.ref, f"{JSON_SCHEMA_DEFS_REF_PREFIX}{nestedType}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def testBareListTypingTypeIsArray(self):
|
|
164
|
+
schema = JsonSchemaBareListTuple.tupleJsonSchema()
|
|
165
|
+
assert schema.properties is not None, (
|
|
166
|
+
"JsonSchemaBareListTuple schema.properties must be set"
|
|
167
|
+
)
|
|
168
|
+
items = schema.properties["items"]
|
|
169
|
+
|
|
170
|
+
self.assertEqual(items.schemaType, JSON_SCHEMA_TYPE_ARRAY)
|
|
171
|
+
assert items.items is not None, (
|
|
172
|
+
"JsonSchemaBareListTuple items.items must be set"
|
|
173
|
+
)
|
|
174
|
+
self.assertEqual(items.items.schemaType, JSON_SCHEMA_TYPE_STRING)
|
|
175
|
+
|
|
176
|
+
def testSelfReferenceDoesNotInfiniteLoop(self):
|
|
177
|
+
schema = JsonSchemaSelfRefTuple.tupleJsonSchema()
|
|
178
|
+
selfType = JsonSchemaSelfRefTuple.tupleType()
|
|
179
|
+
assert schema.properties is not None, (
|
|
180
|
+
"JsonSchemaSelfRefTuple schema.properties must be set"
|
|
181
|
+
)
|
|
182
|
+
assert schema.defs is not None, (
|
|
183
|
+
"JsonSchemaSelfRefTuple schema.defs must be set for self-ref"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
self.assertEqual(
|
|
187
|
+
schema.properties["parent"].ref,
|
|
188
|
+
f"{JSON_SCHEMA_DEFS_REF_PREFIX}{selfType}",
|
|
189
|
+
)
|
|
190
|
+
self.assertIn(selfType, schema.defs)
|
|
191
|
+
selfDefs = schema.defs[selfType]
|
|
192
|
+
self.assertEqual(selfDefs.schemaType, JSON_SCHEMA_TYPE_OBJECT)
|
|
193
|
+
assert selfDefs.properties is not None, (
|
|
194
|
+
"self-ref defs entry properties must be set"
|
|
195
|
+
)
|
|
196
|
+
self.assertEqual(
|
|
197
|
+
selfDefs.properties["parent"].ref,
|
|
198
|
+
f"{JSON_SCHEMA_DEFS_REF_PREFIX}{selfType}",
|
|
199
|
+
)
|
|
200
|
+
self.assertNotIn("parent", schema.required or [])
|
|
201
|
+
|
|
202
|
+
def testBuildJsonSchemaDictKeys(self):
|
|
203
|
+
schema = JsonSchemaScalarTuple.tupleJsonSchema()
|
|
204
|
+
schemaDict = schema.buildJsonSchemaDict()
|
|
205
|
+
|
|
206
|
+
self.assertEqual(
|
|
207
|
+
schemaDict[JSON_SCHEMA_KEY_SCHEMA], JSON_SCHEMA_DRAFT_2020_12
|
|
208
|
+
)
|
|
209
|
+
self.assertEqual(
|
|
210
|
+
schemaDict[JSON_SCHEMA_KEY_TYPE], JSON_SCHEMA_TYPE_OBJECT
|
|
211
|
+
)
|
|
212
|
+
self.assertEqual(
|
|
213
|
+
schemaDict[JSON_SCHEMA_KEY_TITLE],
|
|
214
|
+
JsonSchemaScalarTuple.tupleType(),
|
|
215
|
+
)
|
|
216
|
+
properties = schemaDict[JSON_SCHEMA_KEY_PROPERTIES]
|
|
217
|
+
self.assertEqual(
|
|
218
|
+
properties["aDate"],
|
|
219
|
+
{
|
|
220
|
+
JSON_SCHEMA_KEY_TYPE: JSON_SCHEMA_TYPE_STRING,
|
|
221
|
+
JSON_SCHEMA_KEY_FORMAT: JSON_SCHEMA_FORMAT_DATE_TIME,
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
self.assertEqual(
|
|
225
|
+
properties["aComment"][JSON_SCHEMA_KEY_DESCRIPTION],
|
|
226
|
+
"Field with a description",
|
|
227
|
+
)
|
|
228
|
+
self.assertIn("aString", schemaDict[JSON_SCHEMA_KEY_REQUIRED])
|
|
229
|
+
|
|
230
|
+
def testBuildJsonSchemaDictNestedRef(self):
|
|
231
|
+
schema = JsonSchemaParentTuple.tupleJsonSchema()
|
|
232
|
+
schemaDict = schema.buildJsonSchemaDict()
|
|
233
|
+
nestedType = JsonSchemaNestedTuple.tupleType()
|
|
234
|
+
|
|
235
|
+
self.assertEqual(
|
|
236
|
+
schemaDict[JSON_SCHEMA_KEY_PROPERTIES]["child"],
|
|
237
|
+
{
|
|
238
|
+
JSON_SCHEMA_KEY_REF: (
|
|
239
|
+
f"{JSON_SCHEMA_DEFS_REF_PREFIX}{nestedType}"
|
|
240
|
+
)
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
self.assertIn(nestedType, schemaDict[JSON_SCHEMA_KEY_DEFS])
|
|
244
|
+
self.assertEqual(
|
|
245
|
+
schemaDict[JSON_SCHEMA_KEY_PROPERTIES]["children"][
|
|
246
|
+
JSON_SCHEMA_KEY_ITEMS
|
|
247
|
+
],
|
|
248
|
+
{
|
|
249
|
+
JSON_SCHEMA_KEY_REF: (
|
|
250
|
+
f"{JSON_SCHEMA_DEFS_REF_PREFIX}{nestedType}"
|
|
251
|
+
)
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
self.assertEqual(
|
|
255
|
+
schemaDict[JSON_SCHEMA_KEY_DEFS][nestedType][JSON_SCHEMA_KEY_TYPE],
|
|
256
|
+
JSON_SCHEMA_TYPE_OBJECT,
|
|
257
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
"""Fixture tuples for ``tuple_json_schema_test``."""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from datetime import timezone
|
|
13
|
+
|
|
14
|
+
from vortex.Tuple import Tuple
|
|
15
|
+
from vortex.Tuple import TupleField
|
|
16
|
+
from vortex.Tuple import addTupleType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@addTupleType
|
|
20
|
+
class JsonSchemaNestedTuple(Tuple):
|
|
21
|
+
__tupleType__ = "flyingion.vortex.JsonSchemaNestedTuple"
|
|
22
|
+
nestedInt = TupleField(defaultValue=0, typingType=int)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@addTupleType
|
|
26
|
+
class JsonSchemaScalarTuple(Tuple):
|
|
27
|
+
__tupleType__ = "flyingion.vortex.JsonSchemaScalarTuple"
|
|
28
|
+
aString = TupleField(defaultValue="", typingType=str)
|
|
29
|
+
aInt = TupleField(defaultValue=0, typingType=int)
|
|
30
|
+
aFloat = TupleField(defaultValue=0.0, typingType=float)
|
|
31
|
+
aBool = TupleField(defaultValue=False, typingType=bool)
|
|
32
|
+
aDate = TupleField(
|
|
33
|
+
defaultValue=datetime(2020, 1, 1, tzinfo=timezone.utc),
|
|
34
|
+
typingType=datetime,
|
|
35
|
+
)
|
|
36
|
+
aOptional = TupleField(typingType=str | None)
|
|
37
|
+
aBytes = TupleField(defaultValue=b"", typingType=bytes)
|
|
38
|
+
aDict = TupleField(defaultValue={}, typingType=dict)
|
|
39
|
+
aComment = TupleField(
|
|
40
|
+
defaultValue="",
|
|
41
|
+
typingType=str,
|
|
42
|
+
comment="Field with a description",
|
|
43
|
+
)
|
|
44
|
+
aMissingTyping = TupleField(defaultValue="")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@addTupleType
|
|
48
|
+
class JsonSchemaParentTuple(Tuple):
|
|
49
|
+
__tupleType__ = "flyingion.vortex.JsonSchemaParentTuple"
|
|
50
|
+
child = TupleField(typingType=JsonSchemaNestedTuple)
|
|
51
|
+
children = TupleField(
|
|
52
|
+
defaultValue=[], typingType=list[JsonSchemaNestedTuple]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@addTupleType
|
|
57
|
+
class JsonSchemaBareListTuple(Tuple):
|
|
58
|
+
__tupleType__ = "flyingion.vortex.JsonSchemaBareListTuple"
|
|
59
|
+
items = TupleField(defaultValue=[], typingType=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@addTupleType
|
|
63
|
+
class JsonSchemaSelfRefTuple(Tuple):
|
|
64
|
+
__tupleType__ = "flyingion.vortex.JsonSchemaSelfRefTuple"
|
|
65
|
+
label = TupleField(defaultValue="", typingType=str)
|
|
66
|
+
parent = TupleField()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
JsonSchemaSelfRefTuple.parent.typingType = JsonSchemaSelfRefTuple | None
|
|
@@ -92,6 +92,12 @@ vortex/sqla_orm/VortexTupleJson.py
|
|
|
92
92
|
vortex/sqla_orm/__init__.py
|
|
93
93
|
vortex/storage/TupleStorageSqlite.py
|
|
94
94
|
vortex/storage/__init__.py
|
|
95
|
+
vortex/tuple_json_schema/__init__.py
|
|
96
|
+
vortex/tuple_json_schema/constants.py
|
|
97
|
+
vortex/tuple_json_schema/json_schema_node_tuple.py
|
|
98
|
+
vortex/tuple_json_schema/tuple_json_schema_builder.py
|
|
99
|
+
vortex/tuple_json_schema/tuple_json_schema_test.py
|
|
100
|
+
vortex/tuple_json_schema/tuple_json_schema_test_tuples.py
|
|
95
101
|
vortexpy.egg-info/PKG-INFO
|
|
96
102
|
vortexpy.egg-info/SOURCES.txt
|
|
97
103
|
vortexpy.egg-info/dependency_links.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
vortexpy-4.0.29/vortex/py.typed
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|