structurize 3.9.1__tar.gz → 3.9.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.
- {structurize-3.9.1/structurize.egg-info → structurize-3.9.2}/PKG-INFO +1 -1
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/_version.py +3 -3
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretopython.py +503 -25
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretots.py +73 -13
- {structurize-3.9.1 → structurize-3.9.2/structurize.egg-info}/PKG-INFO +1 -1
- {structurize-3.9.1 → structurize-3.9.2}/.gitignore +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/LICENSE +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/MANIFEST.in +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/README.md +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/__init__.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/__main__.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/asn1toavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotize.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotoasn1.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotocapnproto.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotocpp.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotocsharp.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotocsv.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotocue.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotodatapackage.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotodb.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotoflatbuffers.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotogo.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotographql.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotoiceberg.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotojava.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotojs.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotojsons.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotojstruct.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotojtd.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotokusto.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotomd.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotools.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotoparquet.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotoproto.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotopython.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotoraml.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotorust.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotosmithy.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotosurreal.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotothrift.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotots.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrototsml.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrotoxsd.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/avrovalidator.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/capnprototoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/cddltostructure.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/choice_inference.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/commands.json +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/common.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/constants.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/csvtoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/cuetoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/cuetostructure.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/datapackagetoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/dependencies/typescript/node22/package.json +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/dependency_resolver.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/dependency_version.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/flatbufferstoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/flatbufferstojstruct.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jsonstoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jsonstostructure.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jsontoschema.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jstructtoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jstructtoflatbuffers.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jstructtoraml.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jstructtosmithy.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jtdtoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/jtdtostructure.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/kstructtoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/kustotoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/kustotojstruct.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/mcp_server.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/openapitostructure.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/parquettoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/proto2parser.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/proto3parser.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/prototoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/ramltoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/ramltojstruct.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/rust_xml.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/schema_inference.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/smithytoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/smithytojstruct.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/sqltoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretoasn1.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretocddl.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretocpp.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretocsharp.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretocsv.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretocue.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretodatapackage.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretodb.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretogo.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretographql.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretoiceberg.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretojava.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretojs.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretojsons.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretojtd.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretokusto.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretomd.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretoparquet.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretoproto.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretorust.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretotsml.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/structuretoxsd.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/surrealtoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/thriftstructure.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/thrifttoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/tmslvalidate.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/validate.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/xmltoschema.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/avrotize/xsdtoavro.py +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/build.ps1 +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/build.sh +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/pyproject.toml +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/setup.cfg +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/structurize.egg-info/SOURCES.txt +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/structurize.egg-info/dependency_links.txt +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/structurize.egg-info/entry_points.txt +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/structurize.egg-info/requires.txt +0 -0
- {structurize-3.9.1 → structurize-3.9.2}/structurize.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structurize
|
|
3
|
-
Version: 3.9.
|
|
3
|
+
Version: 3.9.2
|
|
4
4
|
Summary: Tools to convert from and to JSON Structure from various other schema languages.
|
|
5
5
|
Author-email: Clemens Vasters <clemensv@microsoft.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '3.9.
|
|
22
|
-
__version_tuple__ = version_tuple = (3, 9,
|
|
21
|
+
__version__ = version = '3.9.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 9, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g8d1c504ec'
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
-
import
|
|
8
|
+
import hashlib
|
|
9
|
+
from random import Random
|
|
9
10
|
from typing import Any, Dict, List, Set, Tuple, Union, Optional
|
|
10
11
|
|
|
11
12
|
from avrotize.common import pascal, process_template, json_wire_name, json_enum_wire_value
|
|
@@ -15,6 +16,432 @@ JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
|
|
|
15
16
|
|
|
16
17
|
INDENT = ' '
|
|
17
18
|
|
|
19
|
+
# Python types that need a dataclasses-json encoder/decoder pair.
|
|
20
|
+
# Ordered most-specific first: datetime.datetime is a subclass of datetime.date,
|
|
21
|
+
# so an isinstance chain must test datetime before date.
|
|
22
|
+
JSON_CODEC_SCALARS = (
|
|
23
|
+
'datetime.datetime',
|
|
24
|
+
'datetime.date',
|
|
25
|
+
'datetime.time',
|
|
26
|
+
'datetime.timedelta',
|
|
27
|
+
'uuid.UUID',
|
|
28
|
+
'bytes',
|
|
29
|
+
)
|
|
30
|
+
STRICT_JSON_CODEC_SCALARS = frozenset(
|
|
31
|
+
('datetime.timedelta', 'uuid.UUID', 'bytes'))
|
|
32
|
+
|
|
33
|
+
# Marshmallow field expression per custom JSON scalar. List, set and string-keyed
|
|
34
|
+
# map fields compose these scalar fields so schema() uses the same wire codec.
|
|
35
|
+
#
|
|
36
|
+
# These name generated subclasses rather than marshmallow's own fields, because
|
|
37
|
+
# dataclasses_json.mm.schema() uses a supplied mm_field verbatim: it neither
|
|
38
|
+
# assigns data_key nor applies the field's decoder. A plain fields.Date would
|
|
39
|
+
# therefore lose the JSON field name and parse with marshmallow's own parser,
|
|
40
|
+
# so schema() would disagree with the other four entry points.
|
|
41
|
+
_MM_FIELDS = {
|
|
42
|
+
'datetime.datetime': '_IsoDateTimeField',
|
|
43
|
+
'datetime.date': '_IsoDateField',
|
|
44
|
+
'datetime.time': '_IsoTimeField',
|
|
45
|
+
'datetime.timedelta': '_DurationField',
|
|
46
|
+
'uuid.UUID': '_UuidField',
|
|
47
|
+
'bytes': '_Base64Field',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_JSON_PARSERS = {
|
|
51
|
+
'datetime.datetime': '_parse_iso_datetime',
|
|
52
|
+
'datetime.date': '_parse_iso_date',
|
|
53
|
+
'datetime.time': '_parse_iso_time',
|
|
54
|
+
'datetime.timedelta': '_parse_duration',
|
|
55
|
+
'uuid.UUID': '_parse_uuid',
|
|
56
|
+
'bytes': '_parse_base64',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Each generated marshmallow field delegates to one of the JSON parsers, so
|
|
60
|
+
# emitting the field also requires emitting its parser.
|
|
61
|
+
_MM_FIELD_PARSERS = {
|
|
62
|
+
'_IsoDateField': '_parse_iso_date',
|
|
63
|
+
'_IsoDateTimeField': '_parse_iso_datetime',
|
|
64
|
+
'_IsoTimeField': '_parse_iso_time',
|
|
65
|
+
'_DurationField': '_parse_duration',
|
|
66
|
+
'_UuidField': '_parse_uuid',
|
|
67
|
+
'_Base64Field': '_parse_base64',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Emitted in this order so a module's helper block is stable.
|
|
71
|
+
JSON_PARSER_ORDER = (
|
|
72
|
+
'_parse_iso_date',
|
|
73
|
+
'_parse_iso_datetime',
|
|
74
|
+
'_parse_iso_time',
|
|
75
|
+
'_parse_duration',
|
|
76
|
+
'_parse_uuid',
|
|
77
|
+
'_parse_base64',
|
|
78
|
+
)
|
|
79
|
+
MM_FIELD_ORDER = (
|
|
80
|
+
'_IsoDateField',
|
|
81
|
+
'_IsoDateTimeField',
|
|
82
|
+
'_IsoTimeField',
|
|
83
|
+
'_DurationField',
|
|
84
|
+
'_UuidField',
|
|
85
|
+
'_Base64Field',
|
|
86
|
+
'_SetField',
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
_GENERIC_RE = re.compile(r'^typing\.(Optional|List|Set|Dict|Union|Tuple|FrozenSet)\[(.+)\]$')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def split_type_args(arg_text: str) -> List[str]:
|
|
93
|
+
""" Splits the comma-separated arguments of a typing generic at bracket depth zero. """
|
|
94
|
+
args: List[str] = []
|
|
95
|
+
depth = 0
|
|
96
|
+
current = ''
|
|
97
|
+
for char in arg_text:
|
|
98
|
+
if char == '[':
|
|
99
|
+
depth += 1
|
|
100
|
+
elif char == ']':
|
|
101
|
+
depth -= 1
|
|
102
|
+
if char == ',' and depth == 0:
|
|
103
|
+
args.append(current.strip())
|
|
104
|
+
current = ''
|
|
105
|
+
else:
|
|
106
|
+
current += char
|
|
107
|
+
if current.strip():
|
|
108
|
+
args.append(current.strip())
|
|
109
|
+
return args
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_generic_type(type_name: str) -> Optional[Tuple[str, List[str]]]:
|
|
113
|
+
""" Splits a typing generic into its origin and its arguments, or None if it is not generic. """
|
|
114
|
+
match = _GENERIC_RE.match(type_name)
|
|
115
|
+
if not match:
|
|
116
|
+
return None
|
|
117
|
+
origin, arg_text = match.groups()
|
|
118
|
+
return origin, split_type_args(arg_text)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def type_contains_json_codec(type_name: str) -> bool:
|
|
122
|
+
""" Reports whether a Python type annotation contains a custom JSON scalar at any nesting depth. """
|
|
123
|
+
if type_name in JSON_CODEC_SCALARS:
|
|
124
|
+
return True
|
|
125
|
+
generic = parse_generic_type(type_name)
|
|
126
|
+
if not generic:
|
|
127
|
+
return False
|
|
128
|
+
return any(type_contains_json_codec(arg) for arg in generic[1])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
NONE_TYPES = ('None', 'NoneType', 'type(None)')
|
|
132
|
+
|
|
133
|
+
# Branch priorities. Lower is tested first, so datetime must precede date: a
|
|
134
|
+
# datetime.datetime satisfies isinstance(v, datetime.date) as well.
|
|
135
|
+
_PRIORITY_DATETIME = 0
|
|
136
|
+
_PRIORITY_DATE_NARROWING = 1
|
|
137
|
+
_PRIORITY_DATE = 2
|
|
138
|
+
_PRIORITY_TIME = 3
|
|
139
|
+
_PRIORITY_DURATION = 4
|
|
140
|
+
_PRIORITY_UUID = 5
|
|
141
|
+
_PRIORITY_BINARY = 6
|
|
142
|
+
_PRIORITY_CONTAINER = 7
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def json_codec_scalars_in(type_name: str) -> Set[str]:
|
|
146
|
+
""" Collects the distinct custom JSON scalars a Python type annotation contains at any depth. """
|
|
147
|
+
if type_name in JSON_CODEC_SCALARS:
|
|
148
|
+
return {type_name}
|
|
149
|
+
generic = parse_generic_type(type_name)
|
|
150
|
+
if not generic:
|
|
151
|
+
return set()
|
|
152
|
+
found: Set[str] = set()
|
|
153
|
+
for arg in generic[1]:
|
|
154
|
+
found |= json_codec_scalars_in(arg)
|
|
155
|
+
return found
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def union_is_ambiguous(args: List[str]) -> bool:
|
|
159
|
+
""" Reports whether a union mixes custom scalar arms in a way no undiscriminated codec can encode.
|
|
160
|
+
|
|
161
|
+
A union of two custom scalars (``Union[date, datetime]``) cannot be encoded
|
|
162
|
+
without losing the distinction, and a custom scalar beside an unencoded
|
|
163
|
+
arm (``Union[date, str]``) cannot be decoded without coercing values that
|
|
164
|
+
legitimately belong to the other arm. In both cases no codec is emitted, so
|
|
165
|
+
the pre-existing loud failure is preserved rather than replaced by silent
|
|
166
|
+
data loss.
|
|
167
|
+
"""
|
|
168
|
+
arms = [arg for arg in args if arg not in NONE_TYPES]
|
|
169
|
+
scalars: Set[str] = set()
|
|
170
|
+
for arm in arms:
|
|
171
|
+
scalars |= json_codec_scalars_in(arm)
|
|
172
|
+
if len(scalars) > 1:
|
|
173
|
+
return True
|
|
174
|
+
return any(not type_contains_json_codec(arm) for arm in arms)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _codec_branches(type_name: str, var: str, depth: int, encode: bool,
|
|
178
|
+
parsers: Set[str]) -> List[Tuple[int, str, str]]:
|
|
179
|
+
""" Builds the (priority, condition, value) branches of a custom JSON codec expression.
|
|
180
|
+
|
|
181
|
+
Every parser referenced by the emitted branches is recorded in ``parsers``
|
|
182
|
+
so the caller knows exactly which helper definitions the generated module
|
|
183
|
+
needs. Values whose type the branches do not recognize are left untouched;
|
|
184
|
+
strings that should use a custom wire format but cannot be parsed raise from
|
|
185
|
+
the parser.
|
|
186
|
+
"""
|
|
187
|
+
if type_name == 'datetime.datetime':
|
|
188
|
+
if encode:
|
|
189
|
+
return [(_PRIORITY_DATETIME, f'isinstance({var}, datetime.datetime)', f'{var}.isoformat()')]
|
|
190
|
+
parsers.add('_parse_iso_datetime')
|
|
191
|
+
return [(_PRIORITY_DATETIME, f'isinstance({var}, str)', f'_parse_iso_datetime({var}, {{field_name}})')]
|
|
192
|
+
if type_name == 'datetime.date':
|
|
193
|
+
if encode:
|
|
194
|
+
# datetime.datetime is a subclass of datetime.date: narrow it to the
|
|
195
|
+
# date component so the emitted string is parseable by the decoder.
|
|
196
|
+
# This branch is deliberately ranked below the datetime branch above
|
|
197
|
+
# so it can never displace it when both appear in one expression.
|
|
198
|
+
return [(_PRIORITY_DATE_NARROWING, f'isinstance({var}, datetime.datetime)',
|
|
199
|
+
f'{var}.date().isoformat()'),
|
|
200
|
+
(_PRIORITY_DATE, f'isinstance({var}, datetime.date)', f'{var}.isoformat()')]
|
|
201
|
+
parsers.add('_parse_iso_date')
|
|
202
|
+
return [(_PRIORITY_DATE, f'isinstance({var}, str)', f'_parse_iso_date({var}, {{field_name}})')]
|
|
203
|
+
if type_name == 'datetime.time':
|
|
204
|
+
if encode:
|
|
205
|
+
return [(_PRIORITY_TIME, f'isinstance({var}, datetime.time)', f'{var}.isoformat()')]
|
|
206
|
+
parsers.add('_parse_iso_time')
|
|
207
|
+
return [(_PRIORITY_TIME, f'isinstance({var}, str)', f'_parse_iso_time({var}, {{field_name}})')]
|
|
208
|
+
if type_name == 'datetime.timedelta':
|
|
209
|
+
if encode:
|
|
210
|
+
return [(_PRIORITY_DURATION, f'isinstance({var}, datetime.timedelta)',
|
|
211
|
+
f'_format_duration({var})')]
|
|
212
|
+
parsers.add('_parse_duration')
|
|
213
|
+
return [(_PRIORITY_DURATION, 'True',
|
|
214
|
+
f'_parse_duration({var}, {{field_name}})')]
|
|
215
|
+
if type_name == 'uuid.UUID':
|
|
216
|
+
if encode:
|
|
217
|
+
return [(_PRIORITY_UUID, f'isinstance({var}, uuid.UUID)', f'str({var})')]
|
|
218
|
+
parsers.add('_parse_uuid')
|
|
219
|
+
return [(_PRIORITY_UUID, 'True',
|
|
220
|
+
f'_parse_uuid({var}, {{field_name}})')]
|
|
221
|
+
if type_name == 'bytes':
|
|
222
|
+
if encode:
|
|
223
|
+
return [(_PRIORITY_BINARY, f'isinstance({var}, bytes)',
|
|
224
|
+
f"base64.b64encode({var}).decode('ascii')")]
|
|
225
|
+
parsers.add('_parse_base64')
|
|
226
|
+
return [(_PRIORITY_BINARY, 'True',
|
|
227
|
+
f'_parse_base64({var}, {{field_name}})')]
|
|
228
|
+
|
|
229
|
+
generic = parse_generic_type(type_name)
|
|
230
|
+
if not generic:
|
|
231
|
+
return []
|
|
232
|
+
origin, args = generic
|
|
233
|
+
|
|
234
|
+
if origin in ('Optional', 'Union'):
|
|
235
|
+
if union_is_ambiguous(args):
|
|
236
|
+
return []
|
|
237
|
+
branches: List[Tuple[int, str, str]] = []
|
|
238
|
+
for arg in args:
|
|
239
|
+
branches.extend(_codec_branches(arg, var, depth, encode, parsers))
|
|
240
|
+
branches.sort(key=lambda branch: branch[0])
|
|
241
|
+
seen: Set[str] = set()
|
|
242
|
+
unique: List[Tuple[int, str, str]] = []
|
|
243
|
+
for branch in branches:
|
|
244
|
+
if branch[1] in seen:
|
|
245
|
+
continue
|
|
246
|
+
seen.add(branch[1])
|
|
247
|
+
unique.append(branch)
|
|
248
|
+
return unique
|
|
249
|
+
|
|
250
|
+
item = f'_item{depth}'
|
|
251
|
+
# Only homogeneous containers are handled. typing.Tuple is deliberately
|
|
252
|
+
# excluded because its arguments are positional, so a single element codec
|
|
253
|
+
# cannot describe it.
|
|
254
|
+
if origin in ('List', 'Set', 'FrozenSet'):
|
|
255
|
+
inner = args[0]
|
|
256
|
+
if not type_contains_json_codec(inner):
|
|
257
|
+
return []
|
|
258
|
+
inner_expr = build_codec_expression(inner, item, depth + 1, encode, parsers)
|
|
259
|
+
comprehension = f'[{inner_expr} for {item} in {var}]'
|
|
260
|
+
if not encode and origin == 'Set':
|
|
261
|
+
comprehension = f'set({comprehension})'
|
|
262
|
+
elif not encode and origin == 'FrozenSet':
|
|
263
|
+
comprehension = f'frozenset({comprehension})'
|
|
264
|
+
return [(_PRIORITY_CONTAINER, f'isinstance({var}, (list, tuple, set, frozenset))', comprehension)]
|
|
265
|
+
if origin == 'Dict':
|
|
266
|
+
value_type = args[-1]
|
|
267
|
+
if not type_contains_json_codec(value_type):
|
|
268
|
+
return []
|
|
269
|
+
key = f'_key{depth}'
|
|
270
|
+
inner_expr = build_codec_expression(value_type, item, depth + 1, encode, parsers)
|
|
271
|
+
return [(_PRIORITY_CONTAINER, f'isinstance({var}, dict)',
|
|
272
|
+
f'{{{key}: {inner_expr} for {key}, {item} in {var}.items()}}')]
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def build_codec_expression(type_name: str, var: str, depth: int, encode: bool,
|
|
277
|
+
parsers: Set[str]) -> str:
|
|
278
|
+
""" Builds a custom JSON encode/decode expression over ``var``, or ``var`` itself when nothing applies. """
|
|
279
|
+
branches = _codec_branches(type_name, var, depth, encode, parsers)
|
|
280
|
+
expression = var
|
|
281
|
+
if not encode and (
|
|
282
|
+
json_codec_scalars_in(type_name) & STRICT_JSON_CODEC_SCALARS):
|
|
283
|
+
expected_container = decoded_container_kind(type_name)
|
|
284
|
+
if expected_container:
|
|
285
|
+
expression = (
|
|
286
|
+
f"_invalid_container({var}, {{field_name}}, "
|
|
287
|
+
f"'{expected_container}')"
|
|
288
|
+
)
|
|
289
|
+
for _priority, condition, value in reversed(branches):
|
|
290
|
+
expression = f'{value} if {condition} else {expression}'
|
|
291
|
+
if not encode and type_allows_none(type_name):
|
|
292
|
+
expression = f'None if {var} is None else {expression}'
|
|
293
|
+
return expression
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def type_allows_none(type_name: str) -> bool:
|
|
297
|
+
""" Reports whether this exact annotation node declares nullability. """
|
|
298
|
+
generic = parse_generic_type(type_name)
|
|
299
|
+
if not generic:
|
|
300
|
+
return type_name in NONE_TYPES
|
|
301
|
+
origin, args = generic
|
|
302
|
+
return origin == 'Optional' or (
|
|
303
|
+
origin == 'Union' and any(arg in NONE_TYPES for arg in args))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def decoded_container_kind(type_name: str) -> Optional[str]:
|
|
307
|
+
""" Returns the JSON container kind required by a decoded custom-scalar field. """
|
|
308
|
+
generic = parse_generic_type(type_name)
|
|
309
|
+
if not generic:
|
|
310
|
+
return None
|
|
311
|
+
origin, args = generic
|
|
312
|
+
if origin == 'Optional':
|
|
313
|
+
return decoded_container_kind(args[0])
|
|
314
|
+
if origin in ('List', 'Set', 'FrozenSet'):
|
|
315
|
+
return 'array'
|
|
316
|
+
if origin == 'Dict':
|
|
317
|
+
return 'object'
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def build_mm_field(type_name: str, mm_classes: Set[str],
|
|
322
|
+
options: str = '') -> Optional[str]:
|
|
323
|
+
""" Builds the marshmallow field expression for a custom JSON type, or None when
|
|
324
|
+
marshmallow has no faithful equivalent (e.g. a union of several custom
|
|
325
|
+
scalar types).
|
|
326
|
+
|
|
327
|
+
``options`` carries the keyword arguments for the outermost field only, so
|
|
328
|
+
``data_key`` is not repeated on the element field of a list or map.
|
|
329
|
+
"""
|
|
330
|
+
if type_name in _MM_FIELDS:
|
|
331
|
+
mm_class = _MM_FIELDS[type_name]
|
|
332
|
+
mm_classes.add(mm_class)
|
|
333
|
+
return f'{mm_class}({options})'
|
|
334
|
+
generic = parse_generic_type(type_name)
|
|
335
|
+
if not generic:
|
|
336
|
+
return None
|
|
337
|
+
origin, args = generic
|
|
338
|
+
if origin == 'Optional':
|
|
339
|
+
optional_options = options
|
|
340
|
+
if 'allow_none=' not in optional_options:
|
|
341
|
+
optional_options += (
|
|
342
|
+
', ' if optional_options else '') + 'allow_none=True'
|
|
343
|
+
return build_mm_field(args[0], mm_classes, optional_options)
|
|
344
|
+
if origin == 'List':
|
|
345
|
+
inner = build_mm_field(args[0], mm_classes)
|
|
346
|
+
return f'fields.List({inner}{", " + options if options else ""})' if inner else None
|
|
347
|
+
if origin == 'Set':
|
|
348
|
+
inner = build_mm_field(args[0], mm_classes)
|
|
349
|
+
if not inner:
|
|
350
|
+
return None
|
|
351
|
+
mm_classes.add('_SetField')
|
|
352
|
+
return f'_SetField({inner}{", " + options if options else ""})'
|
|
353
|
+
if origin == 'Dict' and len(args) == 2 and args[0] == 'str':
|
|
354
|
+
inner = build_mm_field(args[1], mm_classes)
|
|
355
|
+
if not inner:
|
|
356
|
+
return None
|
|
357
|
+
return (f'fields.Dict(keys=fields.Str(), values={inner}'
|
|
358
|
+
f'{", " + options if options else ""})')
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def build_mm_options(type_name: str, json_name: str) -> str:
|
|
363
|
+
""" Builds the marshmallow keyword arguments that dataclasses_json would have
|
|
364
|
+
supplied itself.
|
|
365
|
+
|
|
366
|
+
``dataclasses_json.mm.schema()`` only assigns ``data_key``, ``required`` and
|
|
367
|
+
``allow_none`` on the branch it takes when no ``mm_field`` is configured, so
|
|
368
|
+
a field that supplies one has to carry them. ``data_key`` is the same value
|
|
369
|
+
passed to ``dataclasses_json.config(field_name=...)``, which is what
|
|
370
|
+
dataclasses_json would have computed from the configured letter case.
|
|
371
|
+
"""
|
|
372
|
+
# Generated fields are kw_only and carry no default, so dataclasses_json
|
|
373
|
+
# would mark every one of them required.
|
|
374
|
+
options = [f'data_key={json_name!r}', 'required=True']
|
|
375
|
+
generic = parse_generic_type(type_name)
|
|
376
|
+
if generic and generic[0] == 'Optional':
|
|
377
|
+
options.append('allow_none=True')
|
|
378
|
+
return ', '.join(options)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def build_container_rebuild(type_name: str) -> Optional[str]:
|
|
382
|
+
""" Builds an expression rebuilding a declared set container from a JSON list.
|
|
383
|
+
|
|
384
|
+
JSON has no set type, so a ``typing.Set`` field arrives as a list. The
|
|
385
|
+
dataclasses-json ``from_json`` path reconstructs the declared container, and
|
|
386
|
+
the generated ``from_serializer_dict`` has to do the same or the two paths
|
|
387
|
+
hand back different container types for the same payload.
|
|
388
|
+
"""
|
|
389
|
+
generic = parse_generic_type(type_name)
|
|
390
|
+
if not generic:
|
|
391
|
+
return None
|
|
392
|
+
origin, args = generic
|
|
393
|
+
if origin == 'Optional':
|
|
394
|
+
inner = build_container_rebuild(args[0])
|
|
395
|
+
return f'None if v is None else {inner}' if inner else None
|
|
396
|
+
if origin in ('Set', 'FrozenSet'):
|
|
397
|
+
return f'{"set" if origin == "Set" else "frozenset"}(v)'
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def build_json_codec(type_name: str, field_name: str) -> Optional[Dict[str, Optional[str]]]:
|
|
402
|
+
""" Builds the dataclasses-json encoder/decoder/mm_field triple for a custom JSON field type.
|
|
403
|
+
|
|
404
|
+
Returns None when the type contains no custom JSON scalar, or when it is a union
|
|
405
|
+
whose arms cannot be told apart on the wire. The encoder and decoder walk
|
|
406
|
+
nested ``Optional``/``Union``/``List``/``Set``/``Dict`` annotations, so
|
|
407
|
+
collections of custom scalars are serialized just like scalar values.
|
|
408
|
+
"""
|
|
409
|
+
if not type_contains_json_codec(type_name):
|
|
410
|
+
return None
|
|
411
|
+
encode_parsers: Set[str] = set()
|
|
412
|
+
decode_parsers: Set[str] = set()
|
|
413
|
+
encoder = build_codec_expression(type_name, 'v', 0, True, encode_parsers)
|
|
414
|
+
decoder = build_codec_expression(type_name, 'v', 0, False, decode_parsers)
|
|
415
|
+
if encoder == 'v' and decoder == 'v':
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
# The parsers name the field that rejected a value, so a malformed payload
|
|
419
|
+
# reports where it came from instead of surfacing a bare isoformat error.
|
|
420
|
+
decoder = decoder.replace('{field_name}', repr(field_name))
|
|
421
|
+
|
|
422
|
+
mm_classes: Set[str] = set()
|
|
423
|
+
mm_field = build_mm_field(type_name, mm_classes,
|
|
424
|
+
build_mm_options(type_name, field_name))
|
|
425
|
+
if not mm_field:
|
|
426
|
+
mm_classes.clear()
|
|
427
|
+
|
|
428
|
+
# A generated marshmallow field parses with the module's own ISO helper, so
|
|
429
|
+
# its parser has to be emitted even when no decoder lambda referenced it.
|
|
430
|
+
parsers = set(decode_parsers)
|
|
431
|
+
for mm_class in mm_classes:
|
|
432
|
+
parser = _MM_FIELD_PARSERS.get(mm_class)
|
|
433
|
+
if parser:
|
|
434
|
+
parsers.add(parser)
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
'encoder': f'lambda v: {encoder}',
|
|
438
|
+
'decoder': f'lambda v: {decoder}',
|
|
439
|
+
'mm_field': mm_field,
|
|
440
|
+
'mm_classes': sorted(mm_classes),
|
|
441
|
+
'parsers': sorted(parsers),
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
18
445
|
# Python standard library modules that should not be shadowed by package names
|
|
19
446
|
PYTHON_STDLIB_MODULES = {
|
|
20
447
|
'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore',
|
|
@@ -407,25 +834,52 @@ class StructureToPython:
|
|
|
407
834
|
doc = structure_schema.get('description', structure_schema.get('doc', class_name))
|
|
408
835
|
|
|
409
836
|
# Generate field docstrings
|
|
410
|
-
field_docstrings = [
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
'
|
|
424
|
-
'
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
837
|
+
field_docstrings = []
|
|
838
|
+
for field in fields:
|
|
839
|
+
codec = build_json_codec(
|
|
840
|
+
field['type'], field.get('json_name') or field['name'])
|
|
841
|
+
field_docstrings.append({
|
|
842
|
+
'name': self.safe_name(field['name']),
|
|
843
|
+
'original_name': field.get('json_name') or field['name'],
|
|
844
|
+
'type': field['type'],
|
|
845
|
+
'is_primitive': field['is_primitive'],
|
|
846
|
+
'is_enum': field['is_enum'],
|
|
847
|
+
'docstring': self.generate_field_docstring(field, schema_namespace),
|
|
848
|
+
'test_value': self.generate_test_value(field),
|
|
849
|
+
'source_type': field.get('source_type', 'string'),
|
|
850
|
+
'json_encoder': codec['encoder'] if codec else None,
|
|
851
|
+
'json_decoder': codec['decoder'] if codec else None,
|
|
852
|
+
'mm_field': codec['mm_field'] if codec else None,
|
|
853
|
+
'mm_classes': codec['mm_classes'] if codec else [],
|
|
854
|
+
'json_parsers': codec['parsers'] if codec else [],
|
|
855
|
+
'reject_json_null': (
|
|
856
|
+
bool(codec)
|
|
857
|
+
and bool(json_codec_scalars_in(field['type'])
|
|
858
|
+
& STRICT_JSON_CODEC_SCALARS)
|
|
859
|
+
and not type_allows_none(field['type'])
|
|
860
|
+
),
|
|
861
|
+
# A set arrives from JSON as a list. Temporal sets are rebuilt by
|
|
862
|
+
# their own decoder; every other set needs an explicit rebuild so
|
|
863
|
+
# from_serializer_dict agrees with the dataclasses-json path.
|
|
864
|
+
'container_rebuild': (None if self.dataclasses_json_annotation and codec
|
|
865
|
+
else build_container_rebuild(field['type'])),
|
|
866
|
+
'xml_name': field['xml_name'],
|
|
867
|
+
'xml_kind': field['xml_kind'],
|
|
868
|
+
'xml_namespace': field['xml_namespace'],
|
|
869
|
+
'xml_metadata': {
|
|
870
|
+
'type': 'Attribute' if field['xml_kind'] == 'attribute' else 'Element',
|
|
871
|
+
'name': field['xml_name'],
|
|
872
|
+
**({'namespace': field['xml_namespace']}
|
|
873
|
+
if field['xml_kind'] != 'attribute' and field['xml_namespace'] else {}),
|
|
874
|
+
},
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
# JSON parsing helpers required by the emitted decoders, in a stable order.
|
|
878
|
+
json_parsers = [parser for parser in JSON_PARSER_ORDER
|
|
879
|
+
if any(parser in field['json_parsers'] for field in field_docstrings)]
|
|
880
|
+
# Marshmallow field subclasses required by the emitted mm_fields.
|
|
881
|
+
mm_classes = [mm_class for mm_class in MM_FIELD_ORDER
|
|
882
|
+
if any(mm_class in field['mm_classes'] for field in field_docstrings)]
|
|
429
883
|
|
|
430
884
|
# If avro_annotation is enabled, convert JSON Structure schema to Avro schema
|
|
431
885
|
# This is embedded in the generated class for runtime Avro serialization
|
|
@@ -443,7 +897,18 @@ class StructureToPython:
|
|
|
443
897
|
class_name=class_name,
|
|
444
898
|
docstring=doc,
|
|
445
899
|
fields=field_docstrings,
|
|
446
|
-
|
|
900
|
+
json_parsers=json_parsers,
|
|
901
|
+
uses_iso_parser=any(parser.startswith('_parse_iso_') for parser in json_parsers),
|
|
902
|
+
uses_container_validator=any(
|
|
903
|
+
field['json_decoder']
|
|
904
|
+
and '_invalid_container' in field['json_decoder']
|
|
905
|
+
for field in field_docstrings),
|
|
906
|
+
needs_base64='_parse_base64' in json_parsers,
|
|
907
|
+
needs_decimal=('decimal.Decimal' in import_types
|
|
908
|
+
or (self.dataclasses_json_annotation
|
|
909
|
+
and '_parse_duration' in json_parsers)),
|
|
910
|
+
mm_classes=mm_classes,
|
|
911
|
+
import_types=sorted(import_types),
|
|
447
912
|
base_package=self.base_package,
|
|
448
913
|
dataclasses_json_annotation=self.dataclasses_json_annotation,
|
|
449
914
|
avro_annotation=self.avro_annotation,
|
|
@@ -765,7 +1230,7 @@ class StructureToPython:
|
|
|
765
1230
|
class_name=class_name,
|
|
766
1231
|
docstring=doc,
|
|
767
1232
|
values_type=values_type,
|
|
768
|
-
import_types=import_types,
|
|
1233
|
+
import_types=sorted(import_types),
|
|
769
1234
|
base_package=self.base_package
|
|
770
1235
|
)
|
|
771
1236
|
|
|
@@ -776,8 +1241,17 @@ class StructureToPython:
|
|
|
776
1241
|
return python_qualified_name
|
|
777
1242
|
|
|
778
1243
|
def generate_test_value(self, field: Dict) -> Any:
|
|
779
|
-
"""Generates a test value for a given field
|
|
1244
|
+
"""Generates a test value for a given field.
|
|
1245
|
+
|
|
1246
|
+
The value is drawn from a generator seeded with a stable hash of the field
|
|
1247
|
+
name and type, so repeated generation of the same schema produces
|
|
1248
|
+
byte-identical output regardless of interpreter hash randomization.
|
|
1249
|
+
"""
|
|
780
1250
|
field_type = field['type']
|
|
1251
|
+
seed = int.from_bytes(
|
|
1252
|
+
hashlib.sha256(f"{field.get('name', '')}:{field_type}".encode('utf-8')).digest()[:8],
|
|
1253
|
+
'big')
|
|
1254
|
+
random = Random(seed)
|
|
781
1255
|
|
|
782
1256
|
def generate_value(field_type: str):
|
|
783
1257
|
test_values = {
|
|
@@ -818,9 +1292,13 @@ class StructureToPython:
|
|
|
818
1292
|
field_type = resolve(field_type)
|
|
819
1293
|
|
|
820
1294
|
if field_type.startswith('typing.List[') or field_type.startswith('typing.Set['):
|
|
1295
|
+
is_set = field_type.startswith('typing.Set[')
|
|
821
1296
|
field_type = resolve(field_type)
|
|
822
1297
|
array_range = random.randint(1, 5)
|
|
823
|
-
|
|
1298
|
+
items = f"[{', '.join([generate_value(field_type) for _ in range(array_range)])}]"
|
|
1299
|
+
# A typing.Set field must be constructed as a set, or the value
|
|
1300
|
+
# would not match the container the deserializers rebuild.
|
|
1301
|
+
return f"set({items})" if is_set else items
|
|
824
1302
|
elif field_type.startswith('typing.Dict['):
|
|
825
1303
|
field_type = resolve(field_type)
|
|
826
1304
|
dict_range = random.randint(1, 5)
|
|
@@ -864,7 +1342,7 @@ class StructureToPython:
|
|
|
864
1342
|
class_name=class_name,
|
|
865
1343
|
test_class_name=test_class_name,
|
|
866
1344
|
fields=fields,
|
|
867
|
-
import_types=import_types,
|
|
1345
|
+
import_types=sorted(import_types),
|
|
868
1346
|
avro_annotation=self.avro_annotation,
|
|
869
1347
|
dataclasses_json_annotation=self.dataclasses_json_annotation
|
|
870
1348
|
)
|
|
@@ -562,32 +562,92 @@ class StructureToTypeScript:
|
|
|
562
562
|
|
|
563
563
|
def generate_tuple(self, structure_schema: Dict, parent_namespace: str,
|
|
564
564
|
write_file: bool = True, explicit_name: str = '') -> str:
|
|
565
|
-
"""
|
|
565
|
+
"""Generates a named TypeScript class that serializes as a JSON array."""
|
|
566
566
|
tuple_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'Tuple'))
|
|
567
567
|
namespace = self.concat_namespace(self.base_package, structure_schema.get('namespace', parent_namespace)).lower()
|
|
568
568
|
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
569
|
-
typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(
|
|
569
|
+
typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(schema_namespace, tuple_name)
|
|
570
570
|
|
|
571
571
|
if typescript_qualified_name in self.generated_types:
|
|
572
572
|
return typescript_qualified_name
|
|
573
573
|
|
|
574
574
|
import_types: Set[str] = set()
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
575
|
+
properties = structure_schema.get('properties', {})
|
|
576
|
+
tuple_order = structure_schema.get('tuple', [])
|
|
577
|
+
elements = []
|
|
578
|
+
for prop_name in tuple_order:
|
|
579
|
+
prop_schema = properties.get(prop_name, {'type': 'any'})
|
|
578
580
|
item_type = self.convert_structure_type_to_typescript(
|
|
579
|
-
tuple_name,
|
|
580
|
-
|
|
581
|
+
tuple_name, prop_name, prop_schema, schema_namespace, import_types)
|
|
582
|
+
item_type_no_null = self.strip_nullable(item_type)
|
|
583
|
+
elements.append({
|
|
584
|
+
'name': self.safe_name(prop_name),
|
|
585
|
+
'type': item_type_no_null,
|
|
586
|
+
'test_value': self.generate_test_value({
|
|
587
|
+
'type_no_null': item_type_no_null,
|
|
588
|
+
'is_enum': any(
|
|
589
|
+
import_type.endswith('.' + item_type_no_null)
|
|
590
|
+
and self.generated_types.get(import_type) == 'enum'
|
|
591
|
+
for import_type in import_types)
|
|
592
|
+
}),
|
|
593
|
+
'docstring': prop_schema.get('description', '') if isinstance(prop_schema, dict) else ''
|
|
594
|
+
})
|
|
581
595
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
596
|
+
imports = []
|
|
597
|
+
for import_type in import_types:
|
|
598
|
+
if import_type == typescript_qualified_name:
|
|
599
|
+
continue
|
|
600
|
+
import_type_parts = import_type.split('.')
|
|
601
|
+
import_type_name = pascal(import_type_parts[-1])
|
|
602
|
+
import_path = '/'.join(import_type_parts)
|
|
603
|
+
current_path = '/'.join(namespace.split('.'))
|
|
604
|
+
relative_import_path = os.path.relpath(import_path, current_path).replace(os.sep, '/')
|
|
605
|
+
if not relative_import_path.startswith('.'):
|
|
606
|
+
relative_import_path = f'./{relative_import_path}'
|
|
607
|
+
imports.append(f"import {{ {import_type_name} }} from '{relative_import_path}.js';")
|
|
608
|
+
|
|
609
|
+
tuple_type = f"[{', '.join(element['type'] for element in elements)}]"
|
|
610
|
+
constructor_parameters = ',\n '.join(
|
|
611
|
+
f"public {element['name']}: {element['type']}" for element in elements)
|
|
612
|
+
array_values = ', '.join(f"this.{element['name']}" for element in elements)
|
|
613
|
+
parsed_values = ',\n '.join(
|
|
614
|
+
f"value[{index}] as {element['type']}" for index, element in enumerate(elements))
|
|
615
|
+
test_values = ',\n '.join(element['test_value'] for element in elements)
|
|
616
|
+
docstring = structure_schema.get('description', f'A {tuple_name} tuple.')
|
|
617
|
+
tuple_definition = '\n'.join(imports)
|
|
618
|
+
if imports:
|
|
619
|
+
tuple_definition += '\n'
|
|
620
|
+
tuple_definition += f"""/** {docstring} */
|
|
621
|
+
export class {tuple_name} {{
|
|
622
|
+
constructor(
|
|
623
|
+
{constructor_parameters}
|
|
624
|
+
) {{}}
|
|
625
|
+
|
|
626
|
+
public toJSON(): {tuple_type} {{
|
|
627
|
+
return [{array_values}];
|
|
628
|
+
}}
|
|
629
|
+
|
|
630
|
+
public static fromJSON(json: string): {tuple_name} {{
|
|
631
|
+
const value: unknown = JSON.parse(json);
|
|
632
|
+
if (!Array.isArray(value) || value.length !== {len(elements)}) {{
|
|
633
|
+
throw new Error('Expected a {tuple_name} JSON array with {len(elements)} elements');
|
|
634
|
+
}}
|
|
635
|
+
return new {tuple_name}(
|
|
636
|
+
{parsed_values}
|
|
637
|
+
);
|
|
638
|
+
}}
|
|
639
|
+
|
|
640
|
+
public static createInstance(): {tuple_name} {{
|
|
641
|
+
return new {tuple_name}(
|
|
642
|
+
{test_values}
|
|
643
|
+
);
|
|
644
|
+
}}
|
|
645
|
+
}}
|
|
646
|
+
"""
|
|
587
647
|
|
|
588
648
|
if write_file:
|
|
589
649
|
self.write_to_file(namespace, tuple_name, tuple_definition)
|
|
590
|
-
self.generated_types[typescript_qualified_name] = '
|
|
650
|
+
self.generated_types[typescript_qualified_name] = 'class'
|
|
591
651
|
return typescript_qualified_name
|
|
592
652
|
|
|
593
653
|
def generate_test_value(self, field: Dict) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structurize
|
|
3
|
-
Version: 3.9.
|
|
3
|
+
Version: 3.9.2
|
|
4
4
|
Summary: Tools to convert from and to JSON Structure from various other schema languages.
|
|
5
5
|
Author-email: Clemens Vasters <clemensv@microsoft.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
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
|
{structurize-3.9.1 → structurize-3.9.2}/avrotize/dependencies/typescript/node22/package.json
RENAMED
|
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
|