structurize 3.5.11__tar.gz → 3.6.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.
- {structurize-3.5.11/structurize.egg-info → structurize-3.6.0}/PKG-INFO +1 -1
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/_version.py +3 -3
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojsons.py +16 -4
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojstruct.py +10 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotokusto.py +32 -26
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotopython.py +38 -3
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotots.py +21 -5
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrovalidator.py +27 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/common.py +46 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependencies/typescript/node22/package.json +1 -1
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/jsonstoavro.py +16 -6
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/jstructtoavro.py +111 -30
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocpp.py +14 -7
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocsharp.py +13 -11
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretogo.py +6 -5
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretojava.py +6 -5
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretokusto.py +33 -27
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretopython.py +7 -6
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretorust.py +4 -5
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretots.py +4 -3
- {structurize-3.5.11 → structurize-3.6.0/structurize.egg-info}/PKG-INFO +1 -1
- {structurize-3.5.11 → structurize-3.6.0}/.gitignore +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/LICENSE +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/MANIFEST.in +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/README.md +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/__init__.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/__main__.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/asn1toavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotize.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotocpp.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotocsharp.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotocsv.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotodatapackage.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotodb.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotogo.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotographql.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoiceberg.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojava.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojs.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotomd.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotools.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoparquet.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoproto.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotorust.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrototsml.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoxsd.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/cddltostructure.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/choice_inference.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/commands.json +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/constants.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/csvtoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/datapackagetoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependency_resolver.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependency_version.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/jsonstostructure.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/jsontoschema.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/kstructtoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/kustotoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/kustotojstruct.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/mcp_server.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/openapitostructure.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/parquettoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/proto2parser.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/proto3parser.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/prototoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/schema_inference.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/sqltoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocddl.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocsv.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretodatapackage.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretodb.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretographql.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretoiceberg.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretojs.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretojsons.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretomd.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretoproto.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretotsml.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretoxsd.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/tmslvalidate.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/validate.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/xmltoschema.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/avrotize/xsdtoavro.py +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/build.ps1 +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/build.sh +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/pyproject.toml +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/setup.cfg +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/SOURCES.txt +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/dependency_links.txt +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/entry_points.txt +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/requires.txt +0 -0
- {structurize-3.5.11 → structurize-3.6.0}/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.
|
|
3
|
+
Version: 3.6.0
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (3,
|
|
21
|
+
__version__ = version = '3.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 6, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g1329b8c45'
|
|
@@ -98,22 +98,34 @@ class AvroToJsonSchemaConverter:
|
|
|
98
98
|
json_type['type'] = 'integer'
|
|
99
99
|
json_type['format'] = 'int64'
|
|
100
100
|
return json_type
|
|
101
|
-
# Avrotize schema extensions: string-based logical types
|
|
102
|
-
|
|
101
|
+
# Avrotize schema extensions: string-based logical types.
|
|
102
|
+
# The rfc3339-* family is the current emission; the bare reserved
|
|
103
|
+
# names on a string base are still recognized for backward compat.
|
|
104
|
+
elif base_type == 'string' and logical_type in ['date', 'rfc3339-date']:
|
|
103
105
|
# Avrotize extension: string with date logicalType
|
|
104
106
|
json_type['type'] = 'string'
|
|
105
107
|
json_type['format'] = 'date'
|
|
106
108
|
return json_type
|
|
107
|
-
elif base_type == 'string' and logical_type in [
|
|
109
|
+
elif base_type == 'string' and logical_type in [
|
|
110
|
+
'timestamp-millis', 'timestamp-micros', 'datetime',
|
|
111
|
+
'rfc3339-timestamp-millis', 'rfc3339-timestamp-micros',
|
|
112
|
+
'rfc3339-local-timestamp-millis', 'rfc3339-local-timestamp-micros']:
|
|
108
113
|
# Avrotize extension: string with datetime logicalType
|
|
109
114
|
json_type['type'] = 'string'
|
|
110
115
|
json_type['format'] = 'date-time'
|
|
111
116
|
return json_type
|
|
112
|
-
elif base_type == 'string' and logical_type in [
|
|
117
|
+
elif base_type == 'string' and logical_type in [
|
|
118
|
+
'time-millis', 'time-micros', 'time',
|
|
119
|
+
'rfc3339-time-millis', 'rfc3339-time-micros']:
|
|
113
120
|
# Avrotize extension: string with time logicalType
|
|
114
121
|
json_type['type'] = 'string'
|
|
115
122
|
json_type['format'] = 'time'
|
|
116
123
|
return json_type
|
|
124
|
+
elif base_type == 'string' and logical_type in ['duration', 'rfc3339-duration']:
|
|
125
|
+
# Avrotize extension: string with duration logicalType (RFC 3339)
|
|
126
|
+
json_type['type'] = 'string'
|
|
127
|
+
json_type['format'] = 'duration'
|
|
128
|
+
return json_type
|
|
117
129
|
elif logical_type == 'decimal':
|
|
118
130
|
json_type['type'] = 'number'
|
|
119
131
|
return json_type
|
|
@@ -305,6 +305,16 @@ class AvroToJsonStructure:
|
|
|
305
305
|
"timestamp-millis": {"type": "int64", "logicalType": "timestampMillis"},
|
|
306
306
|
"date": {"type": "int32", "logicalType": "date"},
|
|
307
307
|
"uuid": {"type": "string", "format": "uuid"},
|
|
308
|
+
# Avrotize Schema rfc3339-* string temporal family -> JSON Structure native
|
|
309
|
+
# temporal types (round-trips with jstructtoavro). See issue #335.
|
|
310
|
+
"rfc3339-date": {"type": "date"},
|
|
311
|
+
"rfc3339-time-millis": {"type": "time"},
|
|
312
|
+
"rfc3339-time-micros": {"type": "time"},
|
|
313
|
+
"rfc3339-timestamp-millis": {"type": "datetime"},
|
|
314
|
+
"rfc3339-timestamp-micros": {"type": "datetime"},
|
|
315
|
+
"rfc3339-local-timestamp-millis": {"type": "datetime"},
|
|
316
|
+
"rfc3339-local-timestamp-micros": {"type": "datetime"},
|
|
317
|
+
"rfc3339-duration": {"type": "duration"},
|
|
308
318
|
}
|
|
309
319
|
return mapping.get(logical_type, {"type": "string"})
|
|
310
320
|
|
|
@@ -7,6 +7,16 @@ from avrotize.common import build_flat_type_dict, inline_avro_references, strip_
|
|
|
7
7
|
from azure.kusto.data import KustoClient, KustoConnectionStringBuilder, ClientRequestProperties
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def kusto_string_literal(value: str) -> str:
|
|
11
|
+
"""Return a KQL verbatim string literal for a raw Python string."""
|
|
12
|
+
return '@"' + value.replace('"', '""') + '"'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def kusto_json_literal(value: Any) -> str:
|
|
16
|
+
"""Return a KQL string literal containing one JSON encoding of value."""
|
|
17
|
+
return kusto_string_literal(json.dumps(value))
|
|
18
|
+
|
|
19
|
+
|
|
10
20
|
class AvroToKusto:
|
|
11
21
|
"""Converts an Avro schema to a Kusto table schema."""
|
|
12
22
|
|
|
@@ -60,9 +70,9 @@ class AvroToKusto:
|
|
|
60
70
|
if "doc" in recordschema:
|
|
61
71
|
doc_data = recordschema["doc"]
|
|
62
72
|
doc_data = (doc_data[:997] + "...") if len(doc_data) > 1000 else doc_data
|
|
63
|
-
doc_string =
|
|
73
|
+
doc_string = kusto_json_literal({
|
|
64
74
|
"description": doc_data
|
|
65
|
-
})
|
|
75
|
+
})
|
|
66
76
|
kusto.append(
|
|
67
77
|
f".alter table {table_ref} docstring {doc_string};")
|
|
68
78
|
kusto.append("")
|
|
@@ -89,7 +99,7 @@ class AvroToKusto:
|
|
|
89
99
|
doc_content["schema"] = inline_schema
|
|
90
100
|
else:
|
|
91
101
|
doc_content["schema"] = inline_schema
|
|
92
|
-
doc =
|
|
102
|
+
doc = kusto_json_literal(doc_content)
|
|
93
103
|
doc_string_statement.append(f" [{column_name}]: {doc}")
|
|
94
104
|
if doc_string_statement and emit_cloudevents_columns:
|
|
95
105
|
doc_string_statement.extend([
|
|
@@ -109,15 +119,13 @@ class AvroToKusto:
|
|
|
109
119
|
# .create-or-alter table dfl_data_events ingestion json mapping
|
|
110
120
|
kusto.append(
|
|
111
121
|
f".create-or-alter table {table_ref} ingestion json mapping \"{mapping_base}_json_flat\"")
|
|
112
|
-
|
|
122
|
+
mapping_entries = []
|
|
113
123
|
if emit_cloudevents_columns:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
kusto.append(
|
|
120
|
-
" {\"column\": \"___subject\", \"path\": \"$.subject\"},")
|
|
124
|
+
mapping_entries.append(" {\"column\": \"___type\", \"path\": \"$.type\"}")
|
|
125
|
+
mapping_entries.append(" {\"column\": \"___source\", \"path\": \"$.source\"}")
|
|
126
|
+
mapping_entries.append(" {\"column\": \"___id\", \"path\": \"$.id\"}")
|
|
127
|
+
mapping_entries.append(" {\"column\": \"___time\", \"path\": \"$.time\"}")
|
|
128
|
+
mapping_entries.append(" {\"column\": \"___subject\", \"path\": \"$.subject\"}")
|
|
121
129
|
for field in fields:
|
|
122
130
|
json_name = column_name = field["name"]
|
|
123
131
|
if 'altnames' in field:
|
|
@@ -125,21 +133,19 @@ class AvroToKusto:
|
|
|
125
133
|
column_name = field['altnames']['kql']
|
|
126
134
|
if 'json' in field['altnames']:
|
|
127
135
|
json_name = field['altnames']['json']
|
|
128
|
-
|
|
129
|
-
f" {{\"column\": \"{column_name}\", \"path\": \"$.{json_name}\"}}
|
|
130
|
-
kusto.append("]\n```\n\n")
|
|
136
|
+
mapping_entries.append(
|
|
137
|
+
f" {{\"column\": \"{column_name}\", \"path\": \"$.{json_name}\"}}")
|
|
138
|
+
kusto.append("```\n[\n" + ",\n".join(mapping_entries) + "\n]\n```\n\n")
|
|
131
139
|
|
|
132
140
|
if emit_cloudevents_columns:
|
|
133
141
|
kusto.append(
|
|
134
142
|
f".create-or-alter table {table_ref} ingestion json mapping \"{mapping_base}_json_ce_structured\"")
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
kusto.append(
|
|
142
|
-
" {\"column\": \"___subject\", \"path\": \"$.subject\"},")
|
|
143
|
+
ce_entries = []
|
|
144
|
+
ce_entries.append(" {\"column\": \"___type\", \"path\": \"$.type\"}")
|
|
145
|
+
ce_entries.append(" {\"column\": \"___source\", \"path\": \"$.source\"}")
|
|
146
|
+
ce_entries.append(" {\"column\": \"___id\", \"path\": \"$.id\"}")
|
|
147
|
+
ce_entries.append(" {\"column\": \"___time\", \"path\": \"$.time\"}")
|
|
148
|
+
ce_entries.append(" {\"column\": \"___subject\", \"path\": \"$.subject\"}")
|
|
143
149
|
for field in fields:
|
|
144
150
|
json_name = column_name = field["name"]
|
|
145
151
|
if 'altnames' in field:
|
|
@@ -147,9 +153,9 @@ class AvroToKusto:
|
|
|
147
153
|
column_name = field['altnames']['kql']
|
|
148
154
|
if 'json' in field['altnames']:
|
|
149
155
|
json_name = field['altnames']['json']
|
|
150
|
-
|
|
151
|
-
f" {{\"column\": \"{column_name}\", \"path\": \"$.data.{json_name}\"}}
|
|
152
|
-
kusto.append("]\n```\n\n")
|
|
156
|
+
ce_entries.append(
|
|
157
|
+
f" {{\"column\": \"{column_name}\", \"path\": \"$.data.{json_name}\"}}")
|
|
158
|
+
kusto.append("```\n[\n" + ",\n".join(ce_entries) + "\n]\n```\n\n")
|
|
153
159
|
|
|
154
160
|
if emit_cloudevents_columns:
|
|
155
161
|
kusto.append(
|
|
@@ -186,7 +192,7 @@ class AvroToKusto:
|
|
|
186
192
|
kusto.append(
|
|
187
193
|
f" \"Query\": \"{query}\",")
|
|
188
194
|
kusto.append(" \"IsTransactional\": false,")
|
|
189
|
-
kusto.append(" \"PropagateIngestionProperties\": true
|
|
195
|
+
kusto.append(" \"PropagateIngestionProperties\": true")
|
|
190
196
|
kusto.append("}]")
|
|
191
197
|
kusto.append("```\n")
|
|
192
198
|
|
|
@@ -90,6 +90,38 @@ class AvroToPython:
|
|
|
90
90
|
return name + "_"
|
|
91
91
|
return name
|
|
92
92
|
|
|
93
|
+
def safe_enum_symbols(self, symbols: List[str]) -> List[Dict[str, str]]:
|
|
94
|
+
"""Converts Avro enum symbols to collision-safe Python enum members."""
|
|
95
|
+
enum_symbols = []
|
|
96
|
+
used_names = set()
|
|
97
|
+
|
|
98
|
+
for symbol in symbols:
|
|
99
|
+
if symbol.isidentifier() and not is_python_reserved_word(symbol):
|
|
100
|
+
member_name = symbol
|
|
101
|
+
else:
|
|
102
|
+
member_name = re.sub(r'[^0-9A-Za-z_]', '_', symbol).strip('_').upper()
|
|
103
|
+
if not member_name:
|
|
104
|
+
member_name = "VALUE"
|
|
105
|
+
if member_name[0].isdigit():
|
|
106
|
+
member_name = f"VALUE_{member_name}"
|
|
107
|
+
if is_python_reserved_word(symbol) or is_python_reserved_word(member_name.lower()) or is_python_reserved_word(member_name):
|
|
108
|
+
member_name = f"{member_name}_"
|
|
109
|
+
|
|
110
|
+
base_member_name = member_name
|
|
111
|
+
suffix = 2
|
|
112
|
+
while member_name in used_names or not member_name.isidentifier() or is_python_reserved_word(member_name):
|
|
113
|
+
member_name = f"{base_member_name}_{suffix}"
|
|
114
|
+
suffix += 1
|
|
115
|
+
|
|
116
|
+
used_names.add(member_name)
|
|
117
|
+
enum_symbols.append({
|
|
118
|
+
'name': member_name,
|
|
119
|
+
'value': symbol,
|
|
120
|
+
'value_literal': repr(symbol)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return enum_symbols
|
|
124
|
+
|
|
93
125
|
def pascal_type_name(self, ref: str) -> str:
|
|
94
126
|
"""Converts a reference to a type name"""
|
|
95
127
|
return '_'.join([pascal(part) for part in ref.split('.')[-1].split('_')])
|
|
@@ -350,9 +382,12 @@ class AvroToPython:
|
|
|
350
382
|
if python_qualified_name in self.generated_types:
|
|
351
383
|
return python_qualified_name
|
|
352
384
|
|
|
353
|
-
symbols = [
|
|
354
|
-
|
|
355
|
-
ordinals =
|
|
385
|
+
symbols = self.safe_enum_symbols(avro_schema.get('symbols', []))
|
|
386
|
+
symbol_names_by_value = {symbol['value']: symbol['name'] for symbol in symbols}
|
|
387
|
+
ordinals = {
|
|
388
|
+
symbol_names_by_value[symbol]: ordinal
|
|
389
|
+
for symbol, ordinal in avro_schema.get('ordinals', {}).items()
|
|
390
|
+
}
|
|
356
391
|
|
|
357
392
|
enum_definition = process_template(
|
|
358
393
|
"avrotopython/enum_core.jinja",
|
|
@@ -16,7 +16,6 @@ def is_typescript_reserved_word(word: str) -> bool:
|
|
|
16
16
|
'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return',
|
|
17
17
|
'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
|
|
18
18
|
'while', 'with', 'yield', 'enum', 'string', 'number', 'boolean', 'symbol',
|
|
19
|
-
'type', 'namespace', 'module', 'declare', 'abstract', 'readonly',
|
|
20
19
|
]
|
|
21
20
|
return word in reserved_words
|
|
22
21
|
|
|
@@ -32,6 +31,7 @@ class AvroToTypeScript:
|
|
|
32
31
|
self.src_dir = os.path.join(self.output_dir, "src")
|
|
33
32
|
self.generated_types: Dict[str, str] = {}
|
|
34
33
|
self.generated_type_fields: Dict[str, List[Dict]] = {} # Store fields for test generation
|
|
34
|
+
self.generated_type_imports: Dict[str, List[str]] = {} # Store import type FQNs for test generation
|
|
35
35
|
self.main_schema = None
|
|
36
36
|
self.type_dict = None
|
|
37
37
|
self.INDENT = ' ' * 4
|
|
@@ -207,6 +207,10 @@ class AvroToTypeScript:
|
|
|
207
207
|
self.write_to_file(namespace, class_name, class_definition)
|
|
208
208
|
self.generated_types[ts_qualified_name] = 'class'
|
|
209
209
|
self.generated_type_fields[ts_qualified_name] = fields # Store fields for test generation
|
|
210
|
+
# Store the fully-qualified import names so generated test files can resolve
|
|
211
|
+
# sub-namespaced types (e.g. enums placed in a "<Record>_types" namespace)
|
|
212
|
+
# to their real source path instead of a flattened, non-existent one.
|
|
213
|
+
self.generated_type_imports[ts_qualified_name] = sorted(import_types)
|
|
210
214
|
return ts_qualified_name
|
|
211
215
|
|
|
212
216
|
def generate_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bool = True) -> str:
|
|
@@ -670,17 +674,29 @@ class AvroToTypeScript:
|
|
|
670
674
|
|
|
671
675
|
fields = self.generated_type_fields.get(qualified_name, [])
|
|
672
676
|
|
|
673
|
-
# Build imports for nested types
|
|
677
|
+
# Build imports for nested types. Reuse the fully-qualified import names
|
|
678
|
+
# captured during class generation so that types living in sub-namespaces
|
|
679
|
+
# (e.g. enums placed in a "<Record>_types" namespace) resolve to their real
|
|
680
|
+
# source path instead of a flattened, non-existent one (issue #338).
|
|
674
681
|
imports_with_paths: Dict[str, str] = {}
|
|
682
|
+
import_fqns = self.generated_type_imports.get(qualified_name, [])
|
|
683
|
+
path_by_leaf: Dict[str, str] = {}
|
|
684
|
+
for fqn in import_fqns:
|
|
685
|
+
fqn_parts = fqn.split('.')
|
|
686
|
+
leaf = pascal(fqn_parts[-1])
|
|
687
|
+
path_by_leaf[leaf] = '../src/' + '/'.join(fqn_parts) + '.js'
|
|
675
688
|
for field in fields:
|
|
676
689
|
field_type = field.get('type_no_null', '')
|
|
677
690
|
if not self.is_typescript_primitive(field_type.replace('[]', '')):
|
|
678
691
|
# It's a reference type, need to import it
|
|
679
692
|
type_name = field_type.replace('[]', '').replace('?', '')
|
|
680
693
|
if type_name and type_name not in ['null', 'any', 'Date']:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
694
|
+
if type_name in path_by_leaf:
|
|
695
|
+
imports_with_paths[type_name] = path_by_leaf[type_name]
|
|
696
|
+
else:
|
|
697
|
+
# Fallback: best-effort path relative to the class namespace
|
|
698
|
+
src_path = '/'.join(parts)
|
|
699
|
+
imports_with_paths[type_name] = f'../src/{src_path.rsplit("/", 1)[0]}/{type_name}.js' if '/' in src_path else f'../src/{parts[0]}/{type_name}.js'
|
|
684
700
|
|
|
685
701
|
# Calculate relative path from test directory to class file
|
|
686
702
|
class_path_parts = namespace.split('.') if namespace else []
|
|
@@ -414,6 +414,33 @@ class AvroValidator:
|
|
|
414
414
|
"""
|
|
415
415
|
base_type = schema.get('type')
|
|
416
416
|
|
|
417
|
+
# Avrotize Schema rfc3339-* string temporal family (issue #335). These are
|
|
418
|
+
# valid standard Avro because rfc3339-* is not a reserved Avro logical type;
|
|
419
|
+
# the value is an RFC 3339 textual string on a string base.
|
|
420
|
+
if isinstance(logical_type, str) and logical_type.startswith('rfc3339-'):
|
|
421
|
+
if base_type != 'string':
|
|
422
|
+
raise AvroValidationError(
|
|
423
|
+
f"{logical_type} logical type requires string base type", path)
|
|
424
|
+
if not isinstance(instance, str):
|
|
425
|
+
raise AvroValidationError(
|
|
426
|
+
f"Expected string for {logical_type}, got {type(instance).__name__}", path)
|
|
427
|
+
core = logical_type[len('rfc3339-'):]
|
|
428
|
+
if core == 'date':
|
|
429
|
+
pattern = self.DATE_PATTERN
|
|
430
|
+
elif core in ('time-millis', 'time-micros'):
|
|
431
|
+
pattern = self.TIME_PATTERN
|
|
432
|
+
elif core in ('timestamp-millis', 'timestamp-micros',
|
|
433
|
+
'local-timestamp-millis', 'local-timestamp-micros'):
|
|
434
|
+
pattern = self.DATETIME_PATTERN
|
|
435
|
+
elif core == 'duration':
|
|
436
|
+
pattern = self.DURATION_PATTERN
|
|
437
|
+
else:
|
|
438
|
+
pattern = None
|
|
439
|
+
if pattern is not None and not pattern.match(instance):
|
|
440
|
+
raise AvroValidationError(
|
|
441
|
+
f"Invalid {logical_type} format: {instance}", path)
|
|
442
|
+
return
|
|
443
|
+
|
|
417
444
|
if logical_type == 'decimal':
|
|
418
445
|
self._validate_decimal(instance, schema, path)
|
|
419
446
|
|
|
@@ -830,6 +830,52 @@ def altname(schema_obj: dict, purpose: str):
|
|
|
830
830
|
return schema_obj["name"]
|
|
831
831
|
|
|
832
832
|
|
|
833
|
+
def json_wire_name(prop_name: str, prop_schema: Any) -> str:
|
|
834
|
+
"""
|
|
835
|
+
Resolve the JSON wire key for a property, honoring JSON Structure ``altnames.json``.
|
|
836
|
+
|
|
837
|
+
JSON Structure requires property keys to be identifiers. A non-identifier wire key
|
|
838
|
+
(e.g. ``dr-type``) is modeled as an identifier property name (``dr_type``) carrying
|
|
839
|
+
``altnames: {"json": "dr-type"}``. The ``json`` purpose is the canonical wire name.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
prop_name (str): The property name (identifier) used as the schema key.
|
|
843
|
+
prop_schema (Any): The property schema; may carry an ``altnames`` map.
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
str: ``altnames.json`` when present, otherwise ``prop_name``.
|
|
847
|
+
"""
|
|
848
|
+
if isinstance(prop_schema, dict):
|
|
849
|
+
altnames = prop_schema.get("altnames")
|
|
850
|
+
if isinstance(altnames, dict) and "json" in altnames:
|
|
851
|
+
return altnames["json"]
|
|
852
|
+
return prop_name
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def json_enum_wire_value(value: Any, enum_schema: Any) -> str:
|
|
856
|
+
"""
|
|
857
|
+
Resolve the JSON wire string for an enum value, honoring JSON Structure ``altenums.json``.
|
|
858
|
+
|
|
859
|
+
``altenums.json`` is a map keyed by the original enum value; the language member name
|
|
860
|
+
stays derived from the original value, while the wire value uses the mapping when present.
|
|
861
|
+
Unmapped values serialize verbatim (identity fallback).
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
value: The original enum value (schema key).
|
|
865
|
+
enum_schema (Any): The enum schema; may carry an ``altenums`` map.
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
str: ``altenums.json[value]`` when present, otherwise ``str(value)``.
|
|
869
|
+
"""
|
|
870
|
+
if isinstance(enum_schema, dict):
|
|
871
|
+
altenums = enum_schema.get("altenums")
|
|
872
|
+
if isinstance(altenums, dict):
|
|
873
|
+
j = altenums.get("json")
|
|
874
|
+
if isinstance(j, dict) and str(value) in j:
|
|
875
|
+
return j[str(value)]
|
|
876
|
+
return str(value)
|
|
877
|
+
|
|
878
|
+
|
|
833
879
|
def process_template(file_path: str, **kvargs) -> str:
|
|
834
880
|
"""
|
|
835
881
|
Process a file as a Jinja2 template with the given object as input.
|
|
@@ -591,14 +591,16 @@ class JsonToAvroConverter:
|
|
|
591
591
|
|
|
592
592
|
# if you've got { 'type': 'string', 'format': ['date-time', 'duration'] }, I'm sorry
|
|
593
593
|
if format and isinstance(format, str):
|
|
594
|
-
if format
|
|
594
|
+
if format == 'date-time':
|
|
595
|
+
avro_primitive = {'type': 'long', 'logicalType': 'timestamp-millis'}
|
|
596
|
+
elif format == 'date':
|
|
595
597
|
avro_primitive = {'type': 'int', 'logicalType': 'date'}
|
|
596
|
-
elif format
|
|
598
|
+
elif format == 'time':
|
|
597
599
|
avro_primitive = {'type': 'int', 'logicalType': 'time-millis'}
|
|
598
|
-
elif format
|
|
600
|
+
elif format == 'duration':
|
|
599
601
|
avro_primitive = {'type': 'fixed',
|
|
600
602
|
'size': 12, 'logicalType': 'duration'}
|
|
601
|
-
elif format
|
|
603
|
+
elif format == 'uuid':
|
|
602
604
|
avro_primitive = {'type': 'string', 'logicalType': 'uuid'}
|
|
603
605
|
|
|
604
606
|
return avro_primitive
|
|
@@ -1357,7 +1359,14 @@ class JsonToAvroConverter:
|
|
|
1357
1359
|
if isinstance(avro_type, dict) and 'type' in avro_type and (isinstance(avro_type, list) or not avro_type['type'] in ['array', 'map', 'record', 'enum', 'fixed']):
|
|
1358
1360
|
if 'dependencies' in avro_type:
|
|
1359
1361
|
dependencies.extend(avro_type['dependencies'])
|
|
1360
|
-
|
|
1362
|
+
# Preserve logical-type annotations (e.g. timestamp-millis, date,
|
|
1363
|
+
# time-millis, uuid). Collapsing these to the bare base type would
|
|
1364
|
+
# silently discard the logical meaning (and, for date-time, the
|
|
1365
|
+
# required 64-bit width). See issue #337.
|
|
1366
|
+
if 'logicalType' in avro_type:
|
|
1367
|
+
avro_type = {k: v for k, v in avro_type.items() if k != 'dependencies'}
|
|
1368
|
+
else:
|
|
1369
|
+
avro_type = avro_type['type']
|
|
1361
1370
|
return avro_type
|
|
1362
1371
|
|
|
1363
1372
|
def register_type(self, avro_schema, avro_type) -> bool:
|
|
@@ -1733,8 +1742,9 @@ class JsonToAvroConverter:
|
|
|
1733
1742
|
# None type is a problem
|
|
1734
1743
|
raise ValueError(
|
|
1735
1744
|
f"avro_field_type is None for field {field_name}")
|
|
1736
|
-
if isinstance(avro_field_type, dict) and 'type' in avro_field_type and not self.is_avro_complex_type(avro_field_type):
|
|
1745
|
+
if isinstance(avro_field_type, dict) and 'type' in avro_field_type and 'logicalType' not in avro_field_type and not self.is_avro_complex_type(avro_field_type):
|
|
1737
1746
|
# if the field type is a basic type, inline it
|
|
1747
|
+
# (logical types are kept intact, see issue #337)
|
|
1738
1748
|
avro_field_type = avro_field_type['type']
|
|
1739
1749
|
field_type_list.append(avro_field_type)
|
|
1740
1750
|
field_ref_type_list.append(avro_field_ref_type)
|