structurize 3.5.10__tar.gz → 3.5.12__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.10/structurize.egg-info → structurize-3.5.12}/PKG-INFO +1 -1
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/_version.py +3 -3
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotojsons.py +16 -4
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotojstruct.py +10 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotokusto.py +13 -3
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotopython.py +38 -3
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotots.py +21 -5
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrovalidator.py +27 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/jsonstoavro.py +16 -6
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/jstructtoavro.py +111 -30
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretokusto.py +14 -4
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretopython.py +64 -7
- {structurize-3.5.10 → structurize-3.5.12/structurize.egg-info}/PKG-INFO +1 -1
- {structurize-3.5.10 → structurize-3.5.12}/.gitignore +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/LICENSE +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/MANIFEST.in +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/README.md +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/__init__.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/__main__.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/asn1toavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotize.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotocpp.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotocsharp.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotocsv.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotodatapackage.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotodb.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotogo.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotographql.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotoiceberg.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotojava.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotojs.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotomd.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotools.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotoparquet.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotoproto.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotorust.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrototsml.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/avrotoxsd.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/cddltostructure.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/choice_inference.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/commands.json +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/common.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/constants.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/csvtoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/datapackagetoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/dependencies/typescript/node22/package.json +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/dependency_resolver.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/dependency_version.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/jsonstostructure.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/jsontoschema.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/kstructtoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/kustotoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/kustotojstruct.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/mcp_server.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/openapitostructure.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/parquettoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/proto2parser.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/proto3parser.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/prototoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/schema_inference.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/sqltoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretocddl.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretocpp.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretocsharp.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretocsv.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretodatapackage.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretodb.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretogo.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretographql.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretoiceberg.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretojava.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretojs.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretojsons.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretomd.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretoproto.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretorust.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretots.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretotsml.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/structuretoxsd.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/tmslvalidate.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/validate.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/xmltoschema.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/avrotize/xsdtoavro.py +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/build.ps1 +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/build.sh +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/pyproject.toml +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/setup.cfg +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/structurize.egg-info/SOURCES.txt +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/structurize.egg-info/dependency_links.txt +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/structurize.egg-info/entry_points.txt +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/structurize.egg-info/requires.txt +0 -0
- {structurize-3.5.10 → structurize-3.5.12}/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.5.
|
|
3
|
+
Version: 3.5.12
|
|
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.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (3, 5,
|
|
21
|
+
__version__ = version = '3.5.12'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 5, 12)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g0eb0a7b9d'
|
|
@@ -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([
|
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -20,6 +20,8 @@ class JsonStructureToAvro:
|
|
|
20
20
|
"""Initialize the converter."""
|
|
21
21
|
self.structure_doc: Optional[Dict[str, Any]] = None
|
|
22
22
|
self.converted_types: Dict[str, Dict[str, Any]] = {}
|
|
23
|
+
self._anon_object_counter: int = 0
|
|
24
|
+
self._generated_record_names: set = set()
|
|
23
25
|
|
|
24
26
|
def convert(self, structure_schema: Dict[str, Any]) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
|
|
25
27
|
"""
|
|
@@ -33,6 +35,8 @@ class JsonStructureToAvro:
|
|
|
33
35
|
"""
|
|
34
36
|
self.structure_doc = structure_schema
|
|
35
37
|
self.converted_types.clear()
|
|
38
|
+
self._anon_object_counter = 0
|
|
39
|
+
self._generated_record_names = set()
|
|
36
40
|
|
|
37
41
|
# Check if this is an inline type (type at root) or uses $root
|
|
38
42
|
root_ref = structure_schema.get('$root')
|
|
@@ -386,7 +390,7 @@ class JsonStructureToAvro:
|
|
|
386
390
|
for prop_name, prop_schema in properties.items():
|
|
387
391
|
field = {
|
|
388
392
|
'name': prop_name,
|
|
389
|
-
'type': self._convert_type_reference(prop_schema)
|
|
393
|
+
'type': self._convert_type_reference(prop_schema, name_hint=prop_name)
|
|
390
394
|
}
|
|
391
395
|
|
|
392
396
|
# Build documentation with annotations
|
|
@@ -678,13 +682,55 @@ class JsonStructureToAvro:
|
|
|
678
682
|
|
|
679
683
|
return avro_record
|
|
680
684
|
|
|
681
|
-
def
|
|
685
|
+
def _convert_union_member(self, member: Union[Dict[str, Any], str]) -> Union[str, Dict[str, Any], List]:
|
|
686
|
+
"""Convert a single member of a JSON Structure union type.
|
|
687
|
+
|
|
688
|
+
Members are usually primitive type names, but a union may also list the
|
|
689
|
+
loose container names ``"object"`` and ``"array"`` (open, untyped). A
|
|
690
|
+
bare ``"object"``/``"array"`` is not a valid Avro type, so map them to
|
|
691
|
+
their canonical open Avro forms (string-valued map / string-element
|
|
692
|
+
array), consistent with how inline open objects are handled (#336).
|
|
693
|
+
"""
|
|
694
|
+
if isinstance(member, str):
|
|
695
|
+
if member == 'object':
|
|
696
|
+
return {'type': 'map', 'values': 'string'}
|
|
697
|
+
if member == 'array':
|
|
698
|
+
return {'type': 'array', 'items': 'string'}
|
|
699
|
+
return self._map_primitive_type(member)
|
|
700
|
+
return self._convert_type_reference(member)
|
|
701
|
+
|
|
702
|
+
def _anonymous_object_name(self, name_hint: Optional[str]) -> str:
|
|
703
|
+
"""Generate a valid, unique Avro record name for an inline object.
|
|
704
|
+
|
|
705
|
+
Prefers a name derived from the enclosing property (``name_hint``) so
|
|
706
|
+
that output is readable and round-trips sensibly; falls back to a
|
|
707
|
+
monotonically increasing generated name. A uniqueness suffix is added
|
|
708
|
+
when the same hint occurs more than once (e.g. like-named properties at
|
|
709
|
+
different nesting levels) to avoid Avro name redefinition errors.
|
|
710
|
+
"""
|
|
711
|
+
if name_hint:
|
|
712
|
+
candidate = f"{name_hint[0].upper() + name_hint[1:]}Type"
|
|
713
|
+
else:
|
|
714
|
+
self._anon_object_counter += 1
|
|
715
|
+
candidate = f"AnonymousObject{self._anon_object_counter}"
|
|
716
|
+
unique = candidate
|
|
717
|
+
suffix = 2
|
|
718
|
+
while unique in self._generated_record_names:
|
|
719
|
+
unique = f"{candidate}_{suffix}"
|
|
720
|
+
suffix += 1
|
|
721
|
+
self._generated_record_names.add(unique)
|
|
722
|
+
return unique
|
|
723
|
+
|
|
724
|
+
def _convert_type_reference(self, schema: Union[Dict[str, Any], str], name_hint: Optional[str] = None) -> Union[str, Dict[str, Any], List]:
|
|
682
725
|
"""
|
|
683
726
|
Convert a type reference or inline type definition.
|
|
684
727
|
|
|
685
728
|
Args:
|
|
686
729
|
schema: Type schema or reference
|
|
687
|
-
|
|
730
|
+
name_hint: Optional name to use when an inline object must be
|
|
731
|
+
materialized as a named Avro record (e.g. the enclosing
|
|
732
|
+
property name). Falls back to a generated unique name.
|
|
733
|
+
|
|
688
734
|
Returns:
|
|
689
735
|
Avro type (string, dict, or list for union)
|
|
690
736
|
"""
|
|
@@ -712,7 +758,7 @@ class JsonStructureToAvro:
|
|
|
712
758
|
|
|
713
759
|
# Handle union types (type is an array like ["string", "null"])
|
|
714
760
|
if isinstance(type_value, list):
|
|
715
|
-
return [self.
|
|
761
|
+
return [self._convert_union_member(t) for t in type_value]
|
|
716
762
|
|
|
717
763
|
# Handle choice types
|
|
718
764
|
if type_value == 'choice':
|
|
@@ -744,7 +790,7 @@ class JsonStructureToAvro:
|
|
|
744
790
|
if type_value == 'array':
|
|
745
791
|
return {
|
|
746
792
|
'type': 'array',
|
|
747
|
-
'items': self._convert_type_reference(schema['items'])
|
|
793
|
+
'items': self._convert_type_reference(schema['items'], name_hint=name_hint)
|
|
748
794
|
}
|
|
749
795
|
|
|
750
796
|
if type_value == 'map':
|
|
@@ -753,6 +799,31 @@ class JsonStructureToAvro:
|
|
|
753
799
|
'values': self._convert_type_reference(schema['values'])
|
|
754
800
|
}
|
|
755
801
|
|
|
802
|
+
# Handle object types appearing inline (i.e. not as a named $root or
|
|
803
|
+
# definition). Without this, an inline {"type": "object"} fell through
|
|
804
|
+
# to _map_primitive_type and emitted a bare "object", which is not a
|
|
805
|
+
# valid Avro type. See issue #336.
|
|
806
|
+
if type_value == 'object':
|
|
807
|
+
properties = schema.get('properties')
|
|
808
|
+
if properties:
|
|
809
|
+
# Object with a defined shape -> nested Avro record.
|
|
810
|
+
obj_name = schema.get('name') or self._anonymous_object_name(name_hint)
|
|
811
|
+
return self._convert_object(schema, None, obj_name)
|
|
812
|
+
# Open / property-less object -> Avro map. Honor an
|
|
813
|
+
# additionalProperties value type when specified, otherwise default
|
|
814
|
+
# to string (an open object carries arbitrary string-keyed values).
|
|
815
|
+
additional = schema.get('additionalProperties')
|
|
816
|
+
if isinstance(additional, dict):
|
|
817
|
+
values_type: Union[str, Dict[str, Any], List] = self._convert_type_reference(additional)
|
|
818
|
+
elif isinstance(additional, str):
|
|
819
|
+
values_type = self._map_primitive_type(additional)
|
|
820
|
+
else:
|
|
821
|
+
values_type = 'string'
|
|
822
|
+
return {
|
|
823
|
+
'type': 'map',
|
|
824
|
+
'values': values_type
|
|
825
|
+
}
|
|
826
|
+
|
|
756
827
|
# Handle logical types
|
|
757
828
|
logical_type = schema.get('logicalType')
|
|
758
829
|
if logical_type:
|
|
@@ -767,8 +838,13 @@ class JsonStructureToAvro:
|
|
|
767
838
|
def _map_primitive_type(self, struct_type: str) -> Union[str, Dict[str, Any]]:
|
|
768
839
|
"""Map JSON Structure primitive type to Avro primitive type.
|
|
769
840
|
|
|
770
|
-
For temporal types, returns Avrotize Schema format with string base type
|
|
771
|
-
and logical
|
|
841
|
+
For temporal types, returns Avrotize Schema format with a string base type
|
|
842
|
+
and an ``rfc3339-*`` logical-type annotation carrying the RFC 3339 textual
|
|
843
|
+
value. The ``rfc3339-*`` names are NOT reserved Avro logical types, so strict
|
|
844
|
+
Avro parsers ignore the annotation and treat the value as a plain string,
|
|
845
|
+
which keeps the emitted schema valid *standard* Avro (see issue #335) while
|
|
846
|
+
Avrotize-aware tools still recognize the temporal semantics.
|
|
847
|
+
See specs/avrotize-schema.md (Logical Types).
|
|
772
848
|
"""
|
|
773
849
|
# Simple types without logical type annotation
|
|
774
850
|
simple_type_mapping = {
|
|
@@ -802,13 +878,16 @@ class JsonStructureToAvro:
|
|
|
802
878
|
'jsonpointer': 'string',
|
|
803
879
|
}
|
|
804
880
|
|
|
805
|
-
# Temporal types
|
|
881
|
+
# Temporal types use the rfc3339-* string-based logical-type family so the
|
|
882
|
+
# output is valid *standard* Avro (rfc3339-* are not reserved Avro logical
|
|
883
|
+
# types, so strict parsers fall back to the string base) while preserving the
|
|
884
|
+
# RFC 3339 textual representation. See issue #335 and specs/avrotize-schema.md.
|
|
806
885
|
temporal_type_mapping = {
|
|
807
|
-
'date': {'type': 'string', 'logicalType': 'date'}, # RFC 3339 full-date
|
|
808
|
-
'datetime': {'type': 'string', 'logicalType': 'timestamp-millis'}, # RFC 3339 date-time
|
|
809
|
-
'time': {'type': 'string', 'logicalType': 'time-millis'}, # RFC 3339 partial-time
|
|
810
|
-
'duration': {'type': 'string', 'logicalType': 'duration'}, # RFC 3339 duration
|
|
811
|
-
'timestamp': {'type': 'string', 'logicalType': 'timestamp-millis'}, # RFC 3339 date-time
|
|
886
|
+
'date': {'type': 'string', 'logicalType': 'rfc3339-date'}, # RFC 3339 full-date
|
|
887
|
+
'datetime': {'type': 'string', 'logicalType': 'rfc3339-timestamp-millis'}, # RFC 3339 date-time
|
|
888
|
+
'time': {'type': 'string', 'logicalType': 'rfc3339-time-millis'}, # RFC 3339 partial-time
|
|
889
|
+
'duration': {'type': 'string', 'logicalType': 'rfc3339-duration'}, # RFC 3339 duration
|
|
890
|
+
'timestamp': {'type': 'string', 'logicalType': 'rfc3339-timestamp-millis'}, # RFC 3339 date-time
|
|
812
891
|
}
|
|
813
892
|
|
|
814
893
|
# Special types with logical type annotation
|
|
@@ -831,29 +910,31 @@ class JsonStructureToAvro:
|
|
|
831
910
|
def _map_logical_type(self, logical_type: str, base_type: Optional[str]) -> Dict[str, Any]:
|
|
832
911
|
"""Map JSON Structure logical type to Avro/Avrotize logical type.
|
|
833
912
|
|
|
834
|
-
|
|
913
|
+
Temporal logical types map to the rfc3339-* string-based family so the output
|
|
914
|
+
is valid standard Avro (see issue #335 and specs/avrotize-schema.md).
|
|
835
915
|
"""
|
|
836
|
-
# Avrotize Schema: temporal types on string
|
|
916
|
+
# Avrotize Schema: temporal types on string carrying RFC 3339 text, using the
|
|
917
|
+
# rfc3339-* logical-type family (not reserved Avro names -> valid standard Avro).
|
|
837
918
|
logical_mapping = {
|
|
838
919
|
# Timestamps
|
|
839
|
-
'timestampMicros': {'type': 'string', 'logicalType': 'timestamp-micros'},
|
|
840
|
-
'timestampMillis': {'type': 'string', 'logicalType': 'timestamp-millis'},
|
|
841
|
-
'timestamp-micros': {'type': 'string', 'logicalType': 'timestamp-micros'},
|
|
842
|
-
'timestamp-millis': {'type': 'string', 'logicalType': 'timestamp-millis'},
|
|
920
|
+
'timestampMicros': {'type': 'string', 'logicalType': 'rfc3339-timestamp-micros'},
|
|
921
|
+
'timestampMillis': {'type': 'string', 'logicalType': 'rfc3339-timestamp-millis'},
|
|
922
|
+
'timestamp-micros': {'type': 'string', 'logicalType': 'rfc3339-timestamp-micros'},
|
|
923
|
+
'timestamp-millis': {'type': 'string', 'logicalType': 'rfc3339-timestamp-millis'},
|
|
843
924
|
# Local timestamps (no timezone)
|
|
844
|
-
'localTimestampMicros': {'type': 'string', 'logicalType': 'local-timestamp-micros'},
|
|
845
|
-
'localTimestampMillis': {'type': 'string', 'logicalType': 'local-timestamp-millis'},
|
|
846
|
-
'local-timestamp-micros': {'type': 'string', 'logicalType': 'local-timestamp-micros'},
|
|
847
|
-
'local-timestamp-millis': {'type': 'string', 'logicalType': 'local-timestamp-millis'},
|
|
925
|
+
'localTimestampMicros': {'type': 'string', 'logicalType': 'rfc3339-local-timestamp-micros'},
|
|
926
|
+
'localTimestampMillis': {'type': 'string', 'logicalType': 'rfc3339-local-timestamp-millis'},
|
|
927
|
+
'local-timestamp-micros': {'type': 'string', 'logicalType': 'rfc3339-local-timestamp-micros'},
|
|
928
|
+
'local-timestamp-millis': {'type': 'string', 'logicalType': 'rfc3339-local-timestamp-millis'},
|
|
848
929
|
# Date and time
|
|
849
|
-
'date': {'type': 'string', 'logicalType': 'date'},
|
|
850
|
-
'time-millis': {'type': 'string', 'logicalType': 'time-millis'},
|
|
851
|
-
'time-micros': {'type': 'string', 'logicalType': 'time-micros'},
|
|
852
|
-
'timeMillis': {'type': 'string', 'logicalType': 'time-millis'},
|
|
853
|
-
'timeMicros': {'type': 'string', 'logicalType': 'time-micros'},
|
|
930
|
+
'date': {'type': 'string', 'logicalType': 'rfc3339-date'},
|
|
931
|
+
'time-millis': {'type': 'string', 'logicalType': 'rfc3339-time-millis'},
|
|
932
|
+
'time-micros': {'type': 'string', 'logicalType': 'rfc3339-time-micros'},
|
|
933
|
+
'timeMillis': {'type': 'string', 'logicalType': 'rfc3339-time-millis'},
|
|
934
|
+
'timeMicros': {'type': 'string', 'logicalType': 'rfc3339-time-micros'},
|
|
854
935
|
# Duration
|
|
855
|
-
'duration': {'type': 'string', 'logicalType': 'duration'},
|
|
856
|
-
# UUID
|
|
936
|
+
'duration': {'type': 'string', 'logicalType': 'rfc3339-duration'},
|
|
937
|
+
# UUID (valid standard Avro on a string base)
|
|
857
938
|
'uuid': {'type': 'string', 'logicalType': 'uuid'},
|
|
858
939
|
# Decimal (Avrotize extension: on string)
|
|
859
940
|
'decimal': {'type': 'string', 'logicalType': 'decimal'},
|
|
@@ -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 StructureToKusto:
|
|
11
21
|
"""Converts a JSON Structure schema to a Kusto table schema."""
|
|
12
22
|
|
|
@@ -284,9 +294,9 @@ class StructureToKusto:
|
|
|
284
294
|
if notes:
|
|
285
295
|
doc_data = doc_data + " " + " ".join(notes)
|
|
286
296
|
|
|
287
|
-
doc_string =
|
|
297
|
+
doc_string = kusto_json_literal({
|
|
288
298
|
"description": doc_data
|
|
289
|
-
})
|
|
299
|
+
})
|
|
290
300
|
kusto.append(
|
|
291
301
|
f".alter table {table_ref} docstring {doc_string};")
|
|
292
302
|
kusto.append("")
|
|
@@ -304,7 +314,7 @@ class StructureToKusto:
|
|
|
304
314
|
else:
|
|
305
315
|
doc_data = f"Constant field with value: {json.dumps(const_value)}"
|
|
306
316
|
doc_content = {"description": doc_data}
|
|
307
|
-
doc =
|
|
317
|
+
doc = kusto_json_literal(doc_content)
|
|
308
318
|
# Add as comment - const fields are not stored in table
|
|
309
319
|
kusto.insert(len(kusto) - (2 if kusto and kusto[-1] == "" else 1),
|
|
310
320
|
f"-- Const field '{column_name}' with value: {json.dumps(const_value)}")
|
|
@@ -323,7 +333,7 @@ class StructureToKusto:
|
|
|
323
333
|
doc_content["schema"] = '{ "doc": "Schema too large to inline. Please refer to the JSON Structure schema for more details." }'
|
|
324
334
|
else:
|
|
325
335
|
doc_content["schema"] = prop_schema
|
|
326
|
-
doc =
|
|
336
|
+
doc = kusto_json_literal(doc_content)
|
|
327
337
|
doc_string_statement.append(f" [{column_name}]: {doc}")
|
|
328
338
|
if doc_string_statement and emit_cloudevents_columns:
|
|
329
339
|
doc_string_statement.extend([
|
|
@@ -497,17 +497,51 @@ class StructureToPython:
|
|
|
497
497
|
field_docstring = f"{field_name} ({field_type})"
|
|
498
498
|
return field_docstring
|
|
499
499
|
|
|
500
|
+
# Mapping of special characters to descriptive names for enum member
|
|
501
|
+
# identifiers. Applied *before* the catch-all regex so that symbols
|
|
502
|
+
# like "5+" and "5-" produce distinct identifiers instead of both
|
|
503
|
+
# collapsing to "5_".
|
|
504
|
+
_CHAR_NAME_MAP = {
|
|
505
|
+
'+': '_PLUS',
|
|
506
|
+
'-': '_MINUS',
|
|
507
|
+
'*': '_STAR',
|
|
508
|
+
'/': '_SLASH',
|
|
509
|
+
'&': '_AMP',
|
|
510
|
+
'|': '_PIPE',
|
|
511
|
+
'!': '_BANG',
|
|
512
|
+
'?': '_QMARK',
|
|
513
|
+
'#': '_HASH',
|
|
514
|
+
'%': '_PCT',
|
|
515
|
+
'@': '_AT',
|
|
516
|
+
'^': '_CARET',
|
|
517
|
+
'~': '_TILDE',
|
|
518
|
+
'<': '_LT',
|
|
519
|
+
'>': '_GT',
|
|
520
|
+
'=': '_EQ',
|
|
521
|
+
'.': '_DOT',
|
|
522
|
+
',': '_COMMA',
|
|
523
|
+
':': '_COLON',
|
|
524
|
+
';': '_SEMI',
|
|
525
|
+
'(': '_LPAREN',
|
|
526
|
+
')': '_RPAREN',
|
|
527
|
+
'[': '_LBRACK',
|
|
528
|
+
']': '_RBRACK',
|
|
529
|
+
'{': '_LBRACE',
|
|
530
|
+
'}': '_RBRACE',
|
|
531
|
+
}
|
|
532
|
+
|
|
500
533
|
def _python_enum_member_name(self, value) -> str:
|
|
501
534
|
"""Derives a valid Python identifier for an enum member from a JSON
|
|
502
535
|
Structure enum value.
|
|
503
536
|
|
|
504
537
|
Numeric values (including negative integers) are prefixed with
|
|
505
538
|
``VALUE_`` (with negatives spelled ``VALUE_NEG_n``) so the generated
|
|
506
|
-
class body is valid Python.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
539
|
+
class body is valid Python. String values are sanitized by
|
|
540
|
+
replacing known special characters with descriptive names (e.g.
|
|
541
|
+
``+`` → ``_PLUS``, ``-`` → ``_MINUS``) and then replacing any
|
|
542
|
+
remaining non-identifier characters with underscores. Values that
|
|
543
|
+
start with a digit are prefixed with ``VALUE_``. Reserved words
|
|
544
|
+
get a trailing underscore.
|
|
511
545
|
"""
|
|
512
546
|
if isinstance(value, bool):
|
|
513
547
|
return "TRUE_" if value else "FALSE_"
|
|
@@ -520,7 +554,18 @@ class StructureToPython:
|
|
|
520
554
|
token = str(value)
|
|
521
555
|
return f"VALUE_{token}"
|
|
522
556
|
text = str(value)
|
|
523
|
-
|
|
557
|
+
# Replace known special characters with descriptive names
|
|
558
|
+
parts: list[str] = []
|
|
559
|
+
for ch in text:
|
|
560
|
+
if ch in self._CHAR_NAME_MAP:
|
|
561
|
+
parts.append(self._CHAR_NAME_MAP[ch])
|
|
562
|
+
else:
|
|
563
|
+
parts.append(ch)
|
|
564
|
+
candidate = ''.join(parts)
|
|
565
|
+
# Replace any remaining invalid identifier characters
|
|
566
|
+
candidate = re.sub(r'[^0-9A-Za-z_]', '_', candidate)
|
|
567
|
+
# Collapse runs of underscores and strip leading/trailing underscores
|
|
568
|
+
candidate = re.sub(r'_+', '_', candidate).strip('_')
|
|
524
569
|
if not candidate or not (candidate[0].isalpha() or candidate[0] == '_'):
|
|
525
570
|
candidate = f"VALUE_{candidate}" if candidate else "VALUE_"
|
|
526
571
|
if is_python_reserved_word(candidate):
|
|
@@ -548,10 +593,22 @@ class StructureToPython:
|
|
|
548
593
|
# Build (member_name, repr) pairs. ``repr`` is rendered verbatim into the
|
|
549
594
|
# class body, so for numeric enums we emit the bare numeric literal and
|
|
550
595
|
# for string enums a quoted Python string literal.
|
|
596
|
+
# Names that collide with each other or with generated methods are
|
|
597
|
+
# disambiguated with numeric suffixes.
|
|
551
598
|
members: List[Tuple[str, str]] = []
|
|
552
599
|
symbols: List[str] = []
|
|
600
|
+
# Pre-seed with names that would clash with generated Enum methods or
|
|
601
|
+
# Python Enum internals.
|
|
602
|
+
used_names: set[str] = {'from_ordinal', 'to_ordinal', 'mro', '_ignore_',
|
|
603
|
+
'_generate_next_value_', '_missing_', '_order_'}
|
|
553
604
|
for value in raw_values:
|
|
554
|
-
|
|
605
|
+
base_name = self._python_enum_member_name(value)
|
|
606
|
+
member_name = base_name
|
|
607
|
+
suffix = 2
|
|
608
|
+
while member_name in used_names:
|
|
609
|
+
member_name = f"{base_name}_{suffix}"
|
|
610
|
+
suffix += 1
|
|
611
|
+
used_names.add(member_name)
|
|
555
612
|
if is_numeric:
|
|
556
613
|
value_repr = repr(value)
|
|
557
614
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structurize
|
|
3
|
-
Version: 3.5.
|
|
3
|
+
Version: 3.5.12
|
|
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
|
{structurize-3.5.10 → structurize-3.5.12}/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
|