structurize 2.16.2__py3-none-any.whl → 2.16.5__py3-none-any.whl

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.
Files changed (53) hide show
  1. avrotize/__init__.py +63 -63
  2. avrotize/__main__.py +5 -5
  3. avrotize/_version.py +34 -34
  4. avrotize/asn1toavro.py +160 -160
  5. avrotize/avrotize.py +152 -152
  6. avrotize/avrotocpp.py +483 -483
  7. avrotize/avrotocsharp.py +992 -992
  8. avrotize/avrotocsv.py +121 -121
  9. avrotize/avrotodatapackage.py +173 -173
  10. avrotize/avrotodb.py +1383 -1383
  11. avrotize/avrotogo.py +476 -476
  12. avrotize/avrotographql.py +197 -197
  13. avrotize/avrotoiceberg.py +210 -210
  14. avrotize/avrotojava.py +1023 -1023
  15. avrotize/avrotojs.py +250 -250
  16. avrotize/avrotojsons.py +481 -481
  17. avrotize/avrotojstruct.py +345 -345
  18. avrotize/avrotokusto.py +363 -363
  19. avrotize/avrotomd.py +137 -137
  20. avrotize/avrotools.py +168 -168
  21. avrotize/avrotoparquet.py +208 -208
  22. avrotize/avrotoproto.py +358 -358
  23. avrotize/avrotopython.py +622 -622
  24. avrotize/avrotorust.py +435 -435
  25. avrotize/avrotots.py +598 -598
  26. avrotize/avrotoxsd.py +344 -344
  27. avrotize/commands.json +2493 -2433
  28. avrotize/common.py +828 -828
  29. avrotize/constants.py +4 -4
  30. avrotize/csvtoavro.py +131 -131
  31. avrotize/datapackagetoavro.py +76 -76
  32. avrotize/dependency_resolver.py +348 -348
  33. avrotize/jsonstoavro.py +1698 -1698
  34. avrotize/jsonstostructure.py +2642 -2642
  35. avrotize/jstructtoavro.py +878 -878
  36. avrotize/kstructtoavro.py +93 -93
  37. avrotize/kustotoavro.py +455 -455
  38. avrotize/parquettoavro.py +157 -157
  39. avrotize/proto2parser.py +497 -497
  40. avrotize/proto3parser.py +402 -402
  41. avrotize/prototoavro.py +382 -382
  42. avrotize/structuretocsharp.py +2005 -2005
  43. avrotize/structuretojsons.py +498 -498
  44. avrotize/structuretopython.py +772 -772
  45. avrotize/structuretots.py +653 -0
  46. avrotize/xsdtoavro.py +413 -413
  47. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/METADATA +848 -805
  48. structurize-2.16.5.dist-info/RECORD +52 -0
  49. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/licenses/LICENSE +200 -200
  50. structurize-2.16.2.dist-info/RECORD +0 -51
  51. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/WHEEL +0 -0
  52. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/entry_points.txt +0 -0
  53. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/top_level.txt +0 -0
@@ -1,772 +1,772 @@
1
- # pylint: disable=line-too-long
2
-
3
- """ StructureToPython class for converting JSON Structure schema to Python classes """
4
-
5
- import json
6
- import os
7
- import re
8
- import random
9
- from typing import Any, Dict, List, Set, Tuple, Union, Optional
10
-
11
- from avrotize.common import pascal, process_template
12
- from avrotize.jstructtoavro import JsonStructureToAvro
13
-
14
- JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
15
-
16
- INDENT = ' '
17
-
18
-
19
- def is_python_reserved_word(word: str) -> bool:
20
- """Checks if a word is a Python reserved word"""
21
- reserved_words = [
22
- 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
23
- 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
24
- 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
25
- 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return',
26
- 'try', 'while', 'with', 'yield', 'record', 'self', 'cls'
27
- ]
28
- return word in reserved_words
29
-
30
-
31
- class StructureToPython:
32
- """ Converts JSON Structure schema to Python classes """
33
-
34
- def __init__(self, base_package: str = '', dataclasses_json_annotation=False, avro_annotation=False) -> None:
35
- self.base_package = base_package
36
- self.dataclasses_json_annotation = dataclasses_json_annotation
37
- self.avro_annotation = avro_annotation
38
- self.output_dir = os.getcwd()
39
- self.schema_doc: JsonNode = None
40
- self.generated_types: Dict[str, str] = {}
41
- self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
42
- self.type_dict: Dict[str, Dict] = {}
43
- self.definitions: Dict[str, Any] = {}
44
- self.schema_registry: Dict[str, Dict] = {}
45
-
46
- def get_qualified_name(self, namespace: str, name: str) -> str:
47
- """ Concatenates namespace and name with a dot separator """
48
- return f"{namespace}.{name}" if namespace != '' else name
49
-
50
- def concat_namespace(self, namespace: str, name: str) -> str:
51
- """ Concatenates namespace and name with a dot separator """
52
- if namespace and name:
53
- return f"{namespace}.{name}"
54
- elif namespace:
55
- return namespace
56
- else:
57
- return name
58
-
59
- def map_primitive_to_python(self, structure_type: str) -> str:
60
- """ Maps JSON Structure primitive types to Python types """
61
- mapping = {
62
- 'null': 'None',
63
- 'boolean': 'bool',
64
- 'string': 'str',
65
- 'integer': 'int',
66
- 'number': 'float',
67
- 'int8': 'int',
68
- 'uint8': 'int',
69
- 'int16': 'int',
70
- 'uint16': 'int',
71
- 'int32': 'int',
72
- 'uint32': 'int',
73
- 'int64': 'int',
74
- 'uint64': 'int',
75
- 'int128': 'int',
76
- 'uint128': 'int',
77
- 'float8': 'float',
78
- 'float': 'float',
79
- 'double': 'float',
80
- 'binary32': 'float',
81
- 'binary64': 'float',
82
- 'decimal': 'decimal.Decimal',
83
- 'binary': 'bytes',
84
- 'date': 'datetime.date',
85
- 'time': 'datetime.time',
86
- 'datetime': 'datetime.datetime',
87
- 'timestamp': 'datetime.datetime',
88
- 'duration': 'datetime.timedelta',
89
- 'uuid': 'uuid.UUID',
90
- 'uri': 'str',
91
- 'jsonpointer': 'str',
92
- 'any': 'typing.Any'
93
- }
94
- qualified_class_name = self.get_qualified_name(
95
- self.base_package.lower(), structure_type.lower())
96
- if qualified_class_name in self.generated_types:
97
- result = qualified_class_name
98
- else:
99
- result = mapping.get(structure_type, 'typing.Any')
100
- return result
101
-
102
- def is_python_primitive(self, type_name: str) -> bool:
103
- """ Checks if a type is a Python primitive type """
104
- return type_name in ['None', 'bool', 'int', 'float', 'str', 'bytes']
105
-
106
- def is_python_typing_struct(self, type_name: str) -> bool:
107
- """ Checks if a type is a Python typing type """
108
- return type_name.startswith('typing.Dict[') or type_name.startswith('typing.List[') or \
109
- type_name.startswith('typing.Optional[') or type_name.startswith('typing.Union[') or \
110
- type_name == 'typing.Any'
111
-
112
- def safe_name(self, name: str) -> str:
113
- """Converts a name to a safe Python name"""
114
- if is_python_reserved_word(name):
115
- return name + "_"
116
- return name
117
-
118
- def pascal_type_name(self, ref: str) -> str:
119
- """Converts a reference to a type name"""
120
- return '_'.join([pascal(part) for part in ref.split('.')[-1].split('_')])
121
-
122
- def python_package_from_structure_type(self, namespace: str, type_name: str) -> str:
123
- """Gets the Python package from a type name"""
124
- type_name_package = '.'.join([part.lower() for part in type_name.split('.')]) if '.' in type_name else type_name.lower()
125
- if '.' in type_name:
126
- package = type_name_package
127
- else:
128
- namespace_package = '.'.join([part.lower() for part in namespace.split('.')]) if namespace else ''
129
- package = namespace_package + ('.' if namespace_package and type_name_package else '') + type_name_package
130
- if self.base_package:
131
- package = self.base_package + '.' + package
132
- return package
133
-
134
- def python_type_from_structure_type(self, type_name: str) -> str:
135
- """Gets the Python class from a type name"""
136
- return self.pascal_type_name(type_name)
137
-
138
- def python_fully_qualified_name_from_structure_type(self, namespace: str, type_name: str) -> str:
139
- """Gets the fully qualified Python class name from a Structure type."""
140
- package = self.python_package_from_structure_type(namespace, type_name)
141
- return package + ('.' if package else '') + self.python_type_from_structure_type(type_name)
142
-
143
- def strip_package_from_fully_qualified_name(self, fully_qualified_name: str) -> str:
144
- """Strips the package from a fully qualified name"""
145
- return fully_qualified_name.split('.')[-1]
146
-
147
- def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
148
- """ Resolves a $ref to the actual schema definition """
149
- if not ref.startswith('#/'):
150
- if ref in self.schema_registry:
151
- return self.schema_registry[ref]
152
- return None
153
-
154
- path = ref[2:].split('/')
155
- schema = context_schema if context_schema else self.schema_doc
156
- for part in path:
157
- if not isinstance(schema, dict) or part not in schema:
158
- return None
159
- schema = schema[part]
160
- return schema
161
-
162
- def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
163
- """ Recursively registers schemas with $id keywords """
164
- if not isinstance(schema, dict):
165
- return
166
-
167
- if '$id' in schema:
168
- schema_id = schema['$id']
169
- if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
170
- from urllib.parse import urljoin
171
- schema_id = urljoin(base_uri, schema_id)
172
- self.schema_registry[schema_id] = schema
173
- base_uri = schema_id
174
-
175
- if 'definitions' in schema:
176
- for def_name, def_schema in schema['definitions'].items():
177
- if isinstance(def_schema, dict):
178
- self.register_schema_ids(def_schema, base_uri)
179
-
180
- if 'properties' in schema:
181
- for prop_name, prop_schema in schema['properties'].items():
182
- if isinstance(prop_schema, dict):
183
- self.register_schema_ids(prop_schema, base_uri)
184
-
185
- for key in ['items', 'values', 'additionalProperties']:
186
- if key in schema and isinstance(schema[key], dict):
187
- self.register_schema_ids(schema[key], base_uri)
188
-
189
- def convert_structure_type_to_python(self, class_name: str, field_name: str,
190
- structure_type: JsonNode, parent_namespace: str,
191
- import_types: Set[str]) -> str:
192
- """ Converts JSON Structure type to Python type """
193
- if isinstance(structure_type, str):
194
- python_type = self.map_primitive_to_python(structure_type)
195
- if python_type.startswith('datetime.') or python_type == 'decimal.Decimal' or python_type == 'uuid.UUID':
196
- import_types.add(python_type)
197
- return python_type
198
- elif isinstance(structure_type, list):
199
- # Handle type unions
200
- non_null_types = [t for t in structure_type if t != 'null']
201
- if len(non_null_types) == 1:
202
- inner_type = self.convert_structure_type_to_python(
203
- class_name, field_name, non_null_types[0], parent_namespace, import_types)
204
- if 'null' in structure_type:
205
- return f'typing.Optional[{inner_type}]'
206
- return inner_type
207
- else:
208
- union_types = [self.convert_structure_type_to_python(
209
- class_name, field_name, t, parent_namespace, import_types) for t in non_null_types]
210
- return f"typing.Union[{', '.join(union_types)}]"
211
- elif isinstance(structure_type, dict):
212
- # Handle $ref
213
- if '$ref' in structure_type:
214
- ref_schema = self.resolve_ref(structure_type['$ref'], self.schema_doc)
215
- if ref_schema:
216
- ref_path = structure_type['$ref'].split('/')
217
- type_name = ref_path[-1]
218
- ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
219
- ref = self.generate_class_or_choice(ref_schema, ref_namespace, write_file=True, explicit_name=type_name)
220
- import_types.add(ref)
221
- return self.strip_package_from_fully_qualified_name(ref)
222
- return 'typing.Any'
223
-
224
- # Handle enum keyword
225
- if 'enum' in structure_type:
226
- enum_ref = self.generate_enum(structure_type, field_name, parent_namespace, write_file=True)
227
- import_types.add(enum_ref)
228
- return self.strip_package_from_fully_qualified_name(enum_ref)
229
-
230
- # Handle type keyword
231
- if 'type' not in structure_type:
232
- return 'typing.Any'
233
-
234
- struct_type = structure_type['type']
235
-
236
- # Handle complex types
237
- if struct_type == 'object':
238
- class_ref = self.generate_class(structure_type, parent_namespace, write_file=True)
239
- import_types.add(class_ref)
240
- return self.strip_package_from_fully_qualified_name(class_ref)
241
- elif struct_type == 'array':
242
- items_type = self.convert_structure_type_to_python(
243
- class_name, field_name+'List', structure_type.get('items', {'type': 'any'}),
244
- parent_namespace, import_types)
245
- return f"typing.List[{items_type}]"
246
- elif struct_type == 'set':
247
- items_type = self.convert_structure_type_to_python(
248
- class_name, field_name+'Set', structure_type.get('items', {'type': 'any'}),
249
- parent_namespace, import_types)
250
- return f"typing.Set[{items_type}]"
251
- elif struct_type == 'map':
252
- values_type = self.convert_structure_type_to_python(
253
- class_name, field_name+'Map', structure_type.get('values', {'type': 'any'}),
254
- parent_namespace, import_types)
255
- return f"typing.Dict[str, {values_type}]"
256
- elif struct_type == 'choice':
257
- # Generate choice returns a Union type and populates import_types with the choice types
258
- return self.generate_choice(structure_type, parent_namespace, write_file=True, import_types=import_types)
259
- elif struct_type == 'tuple':
260
- tuple_ref = self.generate_tuple(structure_type, parent_namespace, write_file=True)
261
- import_types.add(tuple_ref)
262
- return self.strip_package_from_fully_qualified_name(tuple_ref)
263
- else:
264
- return self.convert_structure_type_to_python(class_name, field_name, struct_type, parent_namespace, import_types)
265
- return 'typing.Any'
266
-
267
- def generate_class_or_choice(self, structure_schema: Dict, parent_namespace: str,
268
- write_file: bool = True, explicit_name: str = '') -> str:
269
- """ Generates a Class or Choice """
270
- struct_type = structure_schema.get('type', 'object')
271
- if struct_type == 'object':
272
- return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
273
- elif struct_type == 'choice':
274
- return self.generate_choice(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
275
- elif struct_type == 'tuple':
276
- return self.generate_tuple(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
277
- return 'typing.Any'
278
-
279
- def generate_class(self, structure_schema: Dict, parent_namespace: str,
280
- write_file: bool, explicit_name: str = '') -> str:
281
- """ Generates a Python dataclass from JSON Structure object type """
282
- import_types: Set[str] = set()
283
-
284
- # Get name and namespace
285
- class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
286
- schema_namespace = structure_schema.get('namespace', parent_namespace)
287
- namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
288
- python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
289
-
290
- if python_qualified_name in self.generated_types:
291
- return python_qualified_name
292
-
293
- # Check if this is an abstract type
294
- is_abstract = structure_schema.get('abstract', False)
295
-
296
- # Handle inheritance ($extends)
297
- base_class = None
298
- if '$extends' in structure_schema:
299
- base_ref = structure_schema['$extends']
300
- if isinstance(self.schema_doc, dict):
301
- base_schema = self.resolve_ref(base_ref, self.schema_doc)
302
- if base_schema:
303
- ref_path = base_ref.split('/')
304
- base_name = ref_path[-1]
305
- ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
306
- base_class = self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
307
- import_types.add(base_class)
308
-
309
- # Generate properties
310
- properties = structure_schema.get('properties', {})
311
- required_props = structure_schema.get('required', [])
312
-
313
- fields = []
314
- for prop_name, prop_schema in properties.items():
315
- field_def = self.generate_field(prop_name, prop_schema, class_name, schema_namespace,
316
- required_props, import_types)
317
- fields.append(field_def)
318
-
319
- # Get docstring
320
- doc = structure_schema.get('description', structure_schema.get('doc', class_name))
321
-
322
- # Generate field docstrings
323
- field_docstrings = [{
324
- 'name': self.safe_name(field['name']),
325
- 'original_name': field['name'],
326
- 'type': field['type'],
327
- 'is_primitive': field['is_primitive'],
328
- 'is_enum': field['is_enum'],
329
- 'docstring': self.generate_field_docstring(field, schema_namespace),
330
- 'test_value': self.generate_test_value(field),
331
- 'source_type': field.get('source_type', 'string'),
332
- } for field in fields]
333
-
334
- # If avro_annotation is enabled, convert JSON Structure schema to Avro schema
335
- # This is embedded in the generated class for runtime Avro serialization
336
- avro_schema_json = ''
337
- if self.avro_annotation:
338
- # Use JsonStructureToAvro to convert the schema
339
- converter = JsonStructureToAvro()
340
- schema_copy = structure_schema.copy()
341
- avro_schema = converter.convert(schema_copy)
342
- avro_schema_json = json.dumps(avro_schema).replace('\\"', '\'').replace('"', '\\"')
343
-
344
- # Process template
345
- class_definition = process_template(
346
- "structuretopython/dataclass_core.jinja",
347
- class_name=class_name,
348
- docstring=doc,
349
- fields=field_docstrings,
350
- import_types=import_types,
351
- base_package=self.base_package,
352
- dataclasses_json_annotation=self.dataclasses_json_annotation,
353
- avro_annotation=self.avro_annotation,
354
- avro_schema_json=avro_schema_json,
355
- is_abstract=is_abstract,
356
- base_class=base_class,
357
- )
358
-
359
- if write_file:
360
- self.write_to_file(namespace, class_name, class_definition)
361
- self.generate_test_class(namespace, class_name, field_docstrings, import_types)
362
-
363
- self.generated_types[python_qualified_name] = 'class'
364
- self.generated_structure_types[python_qualified_name] = structure_schema
365
- return python_qualified_name
366
-
367
- def generate_field(self, prop_name: str, prop_schema: Dict, class_name: str,
368
- parent_namespace: str, required_props: List, import_types: Set[str]) -> Dict:
369
- """ Generates a field for a Python dataclass """
370
- field_name = prop_name
371
-
372
- # Check if this is a const field
373
- if 'const' in prop_schema:
374
- # Const fields are treated as class variables with default values
375
- prop_type = self.convert_structure_type_to_python(
376
- class_name, field_name, prop_schema, parent_namespace, import_types)
377
- return {
378
- 'name': field_name,
379
- 'type': prop_type,
380
- 'is_primitive': self.is_python_primitive(prop_type) or self.is_python_typing_struct(prop_type),
381
- 'is_enum': False,
382
- 'is_const': True,
383
- 'const_value': prop_schema['const'],
384
- 'source_type': prop_schema.get('type', 'string')
385
- }
386
-
387
- # Determine if required
388
- is_required = prop_name in required_props if not isinstance(required_props, list) or \
389
- len(required_props) == 0 or not isinstance(required_props[0], list) else \
390
- any(prop_name in req_set for req_set in required_props)
391
-
392
- # Get property type
393
- prop_type = self.convert_structure_type_to_python(
394
- class_name, field_name, prop_schema, parent_namespace, import_types)
395
-
396
- # Add Optional if not required
397
- if not is_required and not prop_type.startswith('typing.Optional['):
398
- prop_type = f'typing.Optional[{prop_type}]'
399
-
400
- # Get source type from structure schema
401
- source_type = prop_schema.get('type', 'string') if isinstance(prop_schema.get('type'), str) else 'object'
402
-
403
- return {
404
- 'name': field_name,
405
- 'type': prop_type,
406
- 'is_primitive': self.is_python_primitive(prop_type) or self.is_python_typing_struct(prop_type),
407
- 'is_enum': prop_type in self.generated_types and self.generated_types[prop_type] == 'enum',
408
- 'is_const': False,
409
- 'source_type': source_type
410
- }
411
-
412
- def generate_field_docstring(self, field: Dict, parent_namespace: str) -> str:
413
- """Generates a field docstring for a Python dataclass"""
414
- field_type = field['type']
415
- field_name = self.safe_name(field['name'])
416
- field_docstring = f"{field_name} ({field_type})"
417
- return field_docstring
418
-
419
- def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str,
420
- write_file: bool) -> str:
421
- """ Generates a Python enum from JSON Structure enum """
422
- # Generate enum name from field name if not provided
423
- class_name = pascal(structure_schema.get('name', field_name + 'Enum'))
424
- schema_namespace = structure_schema.get('namespace', parent_namespace)
425
- namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
426
- python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
427
-
428
- if python_qualified_name in self.generated_types:
429
- return python_qualified_name
430
-
431
- symbols = [symbol if not is_python_reserved_word(symbol) else symbol + "_"
432
- for symbol in structure_schema.get('enum', [])]
433
-
434
- doc = structure_schema.get('description', structure_schema.get('doc', f'A {class_name} enum.'))
435
-
436
- enum_definition = process_template(
437
- "structuretopython/enum_core.jinja",
438
- class_name=class_name,
439
- docstring=doc,
440
- symbols=symbols,
441
- )
442
-
443
- if write_file:
444
- self.write_to_file(namespace, class_name, enum_definition)
445
- self.generate_test_enum(namespace, class_name, symbols)
446
-
447
- self.generated_types[python_qualified_name] = 'enum'
448
- return python_qualified_name
449
-
450
- def generate_choice(self, structure_schema: Dict, parent_namespace: str,
451
- write_file: bool, explicit_name: str = '', import_types: Optional[Set[str]] = None) -> str:
452
- """ Generates a Python Union type from JSON Structure choice """
453
- choice_name = explicit_name if explicit_name else structure_schema.get('name', 'UnnamedChoice')
454
- schema_namespace = structure_schema.get('namespace', parent_namespace)
455
- if import_types is None:
456
- import_types = set()
457
-
458
- # If the choice extends a base class, generate the base and derived classes first
459
- if '$extends' in structure_schema:
460
- base_ref = structure_schema['$extends']
461
- if isinstance(self.schema_doc, dict):
462
- base_schema = self.resolve_ref(base_ref, self.schema_doc)
463
- if base_schema:
464
- # Generate the base class
465
- ref_path = base_ref.split('/')
466
- base_name = ref_path[-1]
467
- ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
468
- self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
469
-
470
- # Generate types for each choice
471
- choice_types = []
472
- choices = structure_schema.get('choices', {})
473
-
474
- for choice_key, choice_schema in choices.items():
475
- if isinstance(choice_schema, dict):
476
- if '$ref' in choice_schema:
477
- # Resolve reference and generate the type
478
- ref_schema = self.resolve_ref(choice_schema['$ref'], self.schema_doc if isinstance(self.schema_doc, dict) else None)
479
- if ref_schema:
480
- ref_path = choice_schema['$ref'].split('/')
481
- ref_name = ref_path[-1]
482
- ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
483
- qualified_name = self.generate_class(ref_schema, ref_namespace, write_file=True, explicit_name=ref_name)
484
- import_types.add(qualified_name)
485
- choice_types.append(qualified_name.split('.')[-1])
486
- elif 'type' in choice_schema:
487
- # Generate inline type
488
- python_type = self.convert_structure_type_to_python(choice_name, choice_key, choice_schema, schema_namespace, import_types)
489
- choice_types.append(python_type)
490
-
491
- # Return Union type
492
- if len(choice_types) == 0:
493
- return 'typing.Any'
494
- elif len(choice_types) == 1:
495
- return choice_types[0]
496
- else:
497
- return f"typing.Union[{', '.join(choice_types)}]"
498
-
499
- def generate_tuple(self, structure_schema: Dict, parent_namespace: str,
500
- write_file: bool, explicit_name: str = '') -> str:
501
- """ Generates a Python Tuple type from JSON Structure tuple """
502
- # For now, return typing.Any as tuples need special handling
503
- return 'typing.Any'
504
-
505
- def generate_map_alias(self, structure_schema: Dict, parent_namespace: str,
506
- write_file: bool) -> str:
507
- """ Generates a Python TypeAlias for a top-level map type """
508
- import_types: Set[str] = set()
509
-
510
- # Get name and namespace
511
- class_name = pascal(structure_schema.get('name', 'UnnamedMap'))
512
- schema_namespace = structure_schema.get('namespace', parent_namespace)
513
- namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
514
- python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
515
-
516
- if python_qualified_name in self.generated_types:
517
- return python_qualified_name
518
-
519
- # Get the value type
520
- values_schema = structure_schema.get('values', {'type': 'any'})
521
- values_type = self.convert_structure_type_to_python(
522
- class_name, 'Values', values_schema, schema_namespace, import_types)
523
-
524
- # Get docstring
525
- doc = structure_schema.get('description', structure_schema.get('doc', f'A {class_name} map type.'))
526
-
527
- # Generate the type alias module
528
- map_definition = process_template(
529
- "structuretopython/map_alias.jinja",
530
- class_name=class_name,
531
- docstring=doc,
532
- values_type=values_type,
533
- import_types=import_types,
534
- base_package=self.base_package
535
- )
536
-
537
- if write_file:
538
- self.write_to_file(namespace, class_name, map_definition)
539
-
540
- self.generated_types[python_qualified_name] = 'map'
541
- return python_qualified_name
542
-
543
- def generate_test_value(self, field: Dict) -> Any:
544
- """Generates a test value for a given field"""
545
- field_type = field['type']
546
-
547
- def generate_value(field_type: str):
548
- test_values = {
549
- 'str': chr(39) + ''.join([chr(random.randint(97, 122)) for _ in range(0, 20)]) + chr(39),
550
- 'bool': str(random.choice([True, False])),
551
- 'int': f'int({random.randint(0, 100)})',
552
- 'float': f'float({random.uniform(0, 100)})',
553
- 'bytes': 'b"test_bytes"',
554
- 'None': 'None',
555
- 'datetime.date': 'datetime.date.today()',
556
- 'datetime.datetime': 'datetime.datetime.now(datetime.timezone.utc)',
557
- 'datetime.time': 'datetime.datetime.now(datetime.timezone.utc).time()',
558
- 'decimal.Decimal': f'decimal.Decimal("{random.randint(0, 100)}.{random.randint(0, 100)}")',
559
- 'datetime.timedelta': 'datetime.timedelta(days=1)',
560
- 'uuid.UUID': 'uuid.uuid4()',
561
- 'typing.Any': '{"test": "test"}'
562
- }
563
-
564
- def resolve(field_type: str) -> str:
565
- pattern = re.compile(r'^(?:typing\.)*(Optional|List|Dict|Union|Set)\[(.+)\]$')
566
- match = pattern.match(field_type)
567
- if not match:
568
- return field_type
569
- outer_type, inner_type = match.groups()
570
- if outer_type == 'Optional':
571
- return inner_type
572
- elif outer_type in ['List', 'Set']:
573
- return resolve(inner_type)
574
- elif outer_type == 'Dict':
575
- _, value_type = inner_type.split(',', 1)
576
- return resolve(value_type.strip())
577
- elif outer_type == 'Union':
578
- first_type = inner_type.split(',', 1)[0]
579
- return resolve(first_type.strip())
580
- return field_type
581
-
582
- if field_type.startswith('typing.Optional['):
583
- field_type = resolve(field_type)
584
-
585
- if field_type.startswith('typing.List[') or field_type.startswith('typing.Set['):
586
- field_type = resolve(field_type)
587
- array_range = random.randint(1, 5)
588
- return f"[{', '.join([generate_value(field_type) for _ in range(array_range)])}]"
589
- elif field_type.startswith('typing.Dict['):
590
- field_type = resolve(field_type)
591
- dict_range = random.randint(1, 5)
592
- dict_data = {}
593
- for _ in range(dict_range):
594
- dict_data[''.join([chr(random.randint(97, 122)) for _ in range(0, 20)])] = generate_value(field_type)
595
- return f"{{{', '.join([chr(39)+key+chr(39)+f': {value}' for key, value in dict_data.items()])}}}"
596
- elif field_type.startswith('typing.Union['):
597
- field_type = resolve(field_type)
598
- return generate_value(field_type)
599
- return test_values.get(field_type, 'Test_' + field_type + '.create_instance()')
600
-
601
- return generate_value(field_type)
602
-
603
- def generate_test_class(self, package_name: str, class_name: str, fields: List[Dict[str, str]],
604
- import_types: Set[str]) -> None:
605
- """Generates a unit test class for a Python dataclass"""
606
- test_class_name = f"Test_{class_name}"
607
- tests_package_name = "test_" + package_name.replace('.', '_').lower()
608
- test_class_definition = process_template(
609
- "structuretopython/test_class.jinja",
610
- package_name=package_name,
611
- class_name=class_name,
612
- test_class_name=test_class_name,
613
- fields=fields,
614
- import_types=import_types,
615
- avro_annotation=self.avro_annotation,
616
- dataclasses_json_annotation=self.dataclasses_json_annotation
617
- )
618
-
619
- base_dir = os.path.join(self.output_dir, "tests")
620
- test_file_path = os.path.join(base_dir, f"{tests_package_name.replace('.', '_').lower()}.py")
621
- if not os.path.exists(os.path.dirname(test_file_path)):
622
- os.makedirs(os.path.dirname(test_file_path), exist_ok=True)
623
- with open(test_file_path, 'w', encoding='utf-8') as file:
624
- file.write(test_class_definition)
625
-
626
- def generate_test_enum(self, package_name: str, class_name: str, symbols: List[str]) -> None:
627
- """Generates a unit test class for a Python enum"""
628
- test_class_name = f"Test_{class_name}"
629
- tests_package_name = "test_" + package_name.replace('.', '_').lower()
630
- test_class_definition = process_template(
631
- "structuretopython/test_enum.jinja",
632
- package_name=package_name,
633
- class_name=class_name,
634
- test_class_name=test_class_name,
635
- symbols=symbols
636
- )
637
- base_dir = os.path.join(self.output_dir, "tests")
638
- test_file_path = os.path.join(base_dir, f"{tests_package_name.replace('.', '_').lower()}.py")
639
- if not os.path.exists(os.path.dirname(test_file_path)):
640
- os.makedirs(os.path.dirname(test_file_path), exist_ok=True)
641
- with open(test_file_path, 'w', encoding='utf-8') as file:
642
- file.write(test_class_definition)
643
-
644
- def write_to_file(self, package: str, class_name: str, python_code: str):
645
- """Writes a Python class to a file"""
646
- # Add 'struct' module to the package path
647
- full_package = f"{package}.struct"
648
- parent_package_name = '.'.join(full_package.split('.')[:-1])
649
- parent_package_path = os.sep.join(parent_package_name.split('.')).lower()
650
- directory_path = os.path.join(self.output_dir, "src", parent_package_path)
651
- if not os.path.exists(directory_path):
652
- os.makedirs(directory_path, exist_ok=True)
653
- file_path = os.path.join(directory_path, f"{class_name.lower()}.py")
654
-
655
- with open(file_path, 'w', encoding='utf-8') as file:
656
- file.write(python_code)
657
-
658
- def write_init_files(self):
659
- """Writes __init__.py files to the output directories"""
660
- def organize_generated_types():
661
- generated_types_tree = {}
662
- for generated_type, _ in self.generated_types.items():
663
- parts = generated_type.split('.')
664
- if len(parts) < 2:
665
- continue
666
- class_name = parts[-1]
667
- module_name = parts[-2]
668
- package_parts = parts[:-2]
669
- current_node = generated_types_tree
670
- for part in package_parts:
671
- if part not in current_node:
672
- current_node[part] = {}
673
- current_node = current_node[part]
674
- current_node[module_name] = class_name
675
- return generated_types_tree
676
-
677
- def collect_class_names(node):
678
- class_names = []
679
- for key, value in node.items():
680
- if isinstance(value, dict):
681
- class_names.extend(collect_class_names(value))
682
- else:
683
- class_names.append(value)
684
- return class_names
685
-
686
- def write_init_files_recursive(generated_types_tree, current_package: str):
687
- import_statements = []
688
- all_statement = []
689
- for package_or_module_name, content in generated_types_tree.items():
690
- if isinstance(content, dict):
691
- class_names = collect_class_names(content)
692
- if class_names:
693
- import_statements.append(f"from .{package_or_module_name} import {', '.join(class_names)}")
694
- all_statement.extend([f'"{name}"' for name in class_names])
695
- write_init_files_recursive(content, current_package + ('.' if current_package else '') + package_or_module_name)
696
- else:
697
- class_name = content
698
- import_statements.append(f"from .{package_or_module_name} import {class_name}")
699
- all_statement.append(f'"{class_name}"')
700
- if current_package and (import_statements or all_statement):
701
- package_path = os.path.join(self.output_dir, 'src', current_package.replace('.', os.sep).lower())
702
- init_file_path = os.path.join(package_path, '__init__.py')
703
- if not os.path.exists(package_path):
704
- os.makedirs(package_path, exist_ok=True)
705
- with open(init_file_path, 'w', encoding='utf-8') as file:
706
- file.write('\n'.join(import_statements) + '\n\n__all__ = [' + ', '.join(all_statement) + ']\n')
707
-
708
- write_init_files_recursive(organize_generated_types(), '')
709
-
710
- def write_pyproject_toml(self):
711
- """Writes pyproject.toml file to the output directory"""
712
- pyproject_content = process_template(
713
- "structuretopython/pyproject_toml.jinja",
714
- package_name=self.base_package.replace('_', '-'),
715
- dataclasses_json_annotation=self.dataclasses_json_annotation,
716
- avro_annotation=self.avro_annotation
717
- )
718
- with open(os.path.join(self.output_dir, 'pyproject.toml'), 'w', encoding='utf-8') as file:
719
- file.write(pyproject_content)
720
-
721
- def convert_schemas(self, structure_schemas: List, output_dir: str):
722
- """ Converts JSON Structure schemas to Python dataclasses"""
723
- self.output_dir = output_dir
724
- if not os.path.exists(self.output_dir):
725
- os.makedirs(self.output_dir, exist_ok=True)
726
-
727
- # Register all schema IDs first
728
- for structure_schema in structure_schemas:
729
- self.register_schema_ids(structure_schema)
730
-
731
- for structure_schema in structure_schemas:
732
- self.schema_doc = structure_schema
733
- if 'definitions' in structure_schema:
734
- self.definitions = structure_schema['definitions']
735
-
736
- if 'enum' in structure_schema:
737
- self.generate_enum(structure_schema, structure_schema.get('name', 'Enum'),
738
- structure_schema.get('namespace', ''), write_file=True)
739
- elif structure_schema.get('type') == 'object':
740
- self.generate_class(structure_schema, structure_schema.get('namespace', ''), write_file=True)
741
- elif structure_schema.get('type') == 'choice':
742
- self.generate_choice(structure_schema, structure_schema.get('namespace', ''), write_file=True)
743
- elif structure_schema.get('type') == 'map':
744
- self.generate_map_alias(structure_schema, structure_schema.get('namespace', ''), write_file=True)
745
-
746
- self.write_init_files()
747
- self.write_pyproject_toml()
748
-
749
- def convert(self, structure_schema_path: str, output_dir: str):
750
- """Converts JSON Structure schema to Python dataclasses"""
751
- with open(structure_schema_path, 'r', encoding='utf-8') as file:
752
- schema = json.load(file)
753
- if isinstance(schema, dict):
754
- schema = [schema]
755
- return self.convert_schemas(schema, output_dir)
756
-
757
-
758
- def convert_structure_to_python(structure_schema_path, py_file_path, package_name='', dataclasses_json_annotation=False, avro_annotation=False):
759
- """Converts JSON Structure schema to Python dataclasses"""
760
- if not package_name:
761
- package_name = os.path.splitext(os.path.basename(structure_schema_path))[0].lower().replace('-', '_')
762
-
763
- structure_to_python = StructureToPython(package_name, dataclasses_json_annotation=dataclasses_json_annotation, avro_annotation=avro_annotation)
764
- structure_to_python.convert(structure_schema_path, py_file_path)
765
-
766
-
767
- def convert_structure_schema_to_python(structure_schema, py_file_path, package_name='', dataclasses_json_annotation=False):
768
- """Converts JSON Structure schema to Python dataclasses"""
769
- structure_to_python = StructureToPython(package_name, dataclasses_json_annotation=dataclasses_json_annotation)
770
- if isinstance(structure_schema, dict):
771
- structure_schema = [structure_schema]
772
- structure_to_python.convert_schemas(structure_schema, py_file_path)
1
+ # pylint: disable=line-too-long
2
+
3
+ """ StructureToPython class for converting JSON Structure schema to Python classes """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import random
9
+ from typing import Any, Dict, List, Set, Tuple, Union, Optional
10
+
11
+ from avrotize.common import pascal, process_template
12
+ from avrotize.jstructtoavro import JsonStructureToAvro
13
+
14
+ JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
15
+
16
+ INDENT = ' '
17
+
18
+
19
+ def is_python_reserved_word(word: str) -> bool:
20
+ """Checks if a word is a Python reserved word"""
21
+ reserved_words = [
22
+ 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
23
+ 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
24
+ 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
25
+ 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return',
26
+ 'try', 'while', 'with', 'yield', 'record', 'self', 'cls'
27
+ ]
28
+ return word in reserved_words
29
+
30
+
31
+ class StructureToPython:
32
+ """ Converts JSON Structure schema to Python classes """
33
+
34
+ def __init__(self, base_package: str = '', dataclasses_json_annotation=False, avro_annotation=False) -> None:
35
+ self.base_package = base_package
36
+ self.dataclasses_json_annotation = dataclasses_json_annotation
37
+ self.avro_annotation = avro_annotation
38
+ self.output_dir = os.getcwd()
39
+ self.schema_doc: JsonNode = None
40
+ self.generated_types: Dict[str, str] = {}
41
+ self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
42
+ self.type_dict: Dict[str, Dict] = {}
43
+ self.definitions: Dict[str, Any] = {}
44
+ self.schema_registry: Dict[str, Dict] = {}
45
+
46
+ def get_qualified_name(self, namespace: str, name: str) -> str:
47
+ """ Concatenates namespace and name with a dot separator """
48
+ return f"{namespace}.{name}" if namespace != '' else name
49
+
50
+ def concat_namespace(self, namespace: str, name: str) -> str:
51
+ """ Concatenates namespace and name with a dot separator """
52
+ if namespace and name:
53
+ return f"{namespace}.{name}"
54
+ elif namespace:
55
+ return namespace
56
+ else:
57
+ return name
58
+
59
+ def map_primitive_to_python(self, structure_type: str) -> str:
60
+ """ Maps JSON Structure primitive types to Python types """
61
+ mapping = {
62
+ 'null': 'None',
63
+ 'boolean': 'bool',
64
+ 'string': 'str',
65
+ 'integer': 'int',
66
+ 'number': 'float',
67
+ 'int8': 'int',
68
+ 'uint8': 'int',
69
+ 'int16': 'int',
70
+ 'uint16': 'int',
71
+ 'int32': 'int',
72
+ 'uint32': 'int',
73
+ 'int64': 'int',
74
+ 'uint64': 'int',
75
+ 'int128': 'int',
76
+ 'uint128': 'int',
77
+ 'float8': 'float',
78
+ 'float': 'float',
79
+ 'double': 'float',
80
+ 'binary32': 'float',
81
+ 'binary64': 'float',
82
+ 'decimal': 'decimal.Decimal',
83
+ 'binary': 'bytes',
84
+ 'date': 'datetime.date',
85
+ 'time': 'datetime.time',
86
+ 'datetime': 'datetime.datetime',
87
+ 'timestamp': 'datetime.datetime',
88
+ 'duration': 'datetime.timedelta',
89
+ 'uuid': 'uuid.UUID',
90
+ 'uri': 'str',
91
+ 'jsonpointer': 'str',
92
+ 'any': 'typing.Any'
93
+ }
94
+ qualified_class_name = self.get_qualified_name(
95
+ self.base_package.lower(), structure_type.lower())
96
+ if qualified_class_name in self.generated_types:
97
+ result = qualified_class_name
98
+ else:
99
+ result = mapping.get(structure_type, 'typing.Any')
100
+ return result
101
+
102
+ def is_python_primitive(self, type_name: str) -> bool:
103
+ """ Checks if a type is a Python primitive type """
104
+ return type_name in ['None', 'bool', 'int', 'float', 'str', 'bytes']
105
+
106
+ def is_python_typing_struct(self, type_name: str) -> bool:
107
+ """ Checks if a type is a Python typing type """
108
+ return type_name.startswith('typing.Dict[') or type_name.startswith('typing.List[') or \
109
+ type_name.startswith('typing.Optional[') or type_name.startswith('typing.Union[') or \
110
+ type_name == 'typing.Any'
111
+
112
+ def safe_name(self, name: str) -> str:
113
+ """Converts a name to a safe Python name"""
114
+ if is_python_reserved_word(name):
115
+ return name + "_"
116
+ return name
117
+
118
+ def pascal_type_name(self, ref: str) -> str:
119
+ """Converts a reference to a type name"""
120
+ return '_'.join([pascal(part) for part in ref.split('.')[-1].split('_')])
121
+
122
+ def python_package_from_structure_type(self, namespace: str, type_name: str) -> str:
123
+ """Gets the Python package from a type name"""
124
+ type_name_package = '.'.join([part.lower() for part in type_name.split('.')]) if '.' in type_name else type_name.lower()
125
+ if '.' in type_name:
126
+ package = type_name_package
127
+ else:
128
+ namespace_package = '.'.join([part.lower() for part in namespace.split('.')]) if namespace else ''
129
+ package = namespace_package + ('.' if namespace_package and type_name_package else '') + type_name_package
130
+ if self.base_package:
131
+ package = self.base_package + '.' + package
132
+ return package
133
+
134
+ def python_type_from_structure_type(self, type_name: str) -> str:
135
+ """Gets the Python class from a type name"""
136
+ return self.pascal_type_name(type_name)
137
+
138
+ def python_fully_qualified_name_from_structure_type(self, namespace: str, type_name: str) -> str:
139
+ """Gets the fully qualified Python class name from a Structure type."""
140
+ package = self.python_package_from_structure_type(namespace, type_name)
141
+ return package + ('.' if package else '') + self.python_type_from_structure_type(type_name)
142
+
143
+ def strip_package_from_fully_qualified_name(self, fully_qualified_name: str) -> str:
144
+ """Strips the package from a fully qualified name"""
145
+ return fully_qualified_name.split('.')[-1]
146
+
147
+ def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
148
+ """ Resolves a $ref to the actual schema definition """
149
+ if not ref.startswith('#/'):
150
+ if ref in self.schema_registry:
151
+ return self.schema_registry[ref]
152
+ return None
153
+
154
+ path = ref[2:].split('/')
155
+ schema = context_schema if context_schema else self.schema_doc
156
+ for part in path:
157
+ if not isinstance(schema, dict) or part not in schema:
158
+ return None
159
+ schema = schema[part]
160
+ return schema
161
+
162
+ def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
163
+ """ Recursively registers schemas with $id keywords """
164
+ if not isinstance(schema, dict):
165
+ return
166
+
167
+ if '$id' in schema:
168
+ schema_id = schema['$id']
169
+ if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
170
+ from urllib.parse import urljoin
171
+ schema_id = urljoin(base_uri, schema_id)
172
+ self.schema_registry[schema_id] = schema
173
+ base_uri = schema_id
174
+
175
+ if 'definitions' in schema:
176
+ for def_name, def_schema in schema['definitions'].items():
177
+ if isinstance(def_schema, dict):
178
+ self.register_schema_ids(def_schema, base_uri)
179
+
180
+ if 'properties' in schema:
181
+ for prop_name, prop_schema in schema['properties'].items():
182
+ if isinstance(prop_schema, dict):
183
+ self.register_schema_ids(prop_schema, base_uri)
184
+
185
+ for key in ['items', 'values', 'additionalProperties']:
186
+ if key in schema and isinstance(schema[key], dict):
187
+ self.register_schema_ids(schema[key], base_uri)
188
+
189
+ def convert_structure_type_to_python(self, class_name: str, field_name: str,
190
+ structure_type: JsonNode, parent_namespace: str,
191
+ import_types: Set[str]) -> str:
192
+ """ Converts JSON Structure type to Python type """
193
+ if isinstance(structure_type, str):
194
+ python_type = self.map_primitive_to_python(structure_type)
195
+ if python_type.startswith('datetime.') or python_type == 'decimal.Decimal' or python_type == 'uuid.UUID':
196
+ import_types.add(python_type)
197
+ return python_type
198
+ elif isinstance(structure_type, list):
199
+ # Handle type unions
200
+ non_null_types = [t for t in structure_type if t != 'null']
201
+ if len(non_null_types) == 1:
202
+ inner_type = self.convert_structure_type_to_python(
203
+ class_name, field_name, non_null_types[0], parent_namespace, import_types)
204
+ if 'null' in structure_type:
205
+ return f'typing.Optional[{inner_type}]'
206
+ return inner_type
207
+ else:
208
+ union_types = [self.convert_structure_type_to_python(
209
+ class_name, field_name, t, parent_namespace, import_types) for t in non_null_types]
210
+ return f"typing.Union[{', '.join(union_types)}]"
211
+ elif isinstance(structure_type, dict):
212
+ # Handle $ref
213
+ if '$ref' in structure_type:
214
+ ref_schema = self.resolve_ref(structure_type['$ref'], self.schema_doc)
215
+ if ref_schema:
216
+ ref_path = structure_type['$ref'].split('/')
217
+ type_name = ref_path[-1]
218
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
219
+ ref = self.generate_class_or_choice(ref_schema, ref_namespace, write_file=True, explicit_name=type_name)
220
+ import_types.add(ref)
221
+ return self.strip_package_from_fully_qualified_name(ref)
222
+ return 'typing.Any'
223
+
224
+ # Handle enum keyword
225
+ if 'enum' in structure_type:
226
+ enum_ref = self.generate_enum(structure_type, field_name, parent_namespace, write_file=True)
227
+ import_types.add(enum_ref)
228
+ return self.strip_package_from_fully_qualified_name(enum_ref)
229
+
230
+ # Handle type keyword
231
+ if 'type' not in structure_type:
232
+ return 'typing.Any'
233
+
234
+ struct_type = structure_type['type']
235
+
236
+ # Handle complex types
237
+ if struct_type == 'object':
238
+ class_ref = self.generate_class(structure_type, parent_namespace, write_file=True)
239
+ import_types.add(class_ref)
240
+ return self.strip_package_from_fully_qualified_name(class_ref)
241
+ elif struct_type == 'array':
242
+ items_type = self.convert_structure_type_to_python(
243
+ class_name, field_name+'List', structure_type.get('items', {'type': 'any'}),
244
+ parent_namespace, import_types)
245
+ return f"typing.List[{items_type}]"
246
+ elif struct_type == 'set':
247
+ items_type = self.convert_structure_type_to_python(
248
+ class_name, field_name+'Set', structure_type.get('items', {'type': 'any'}),
249
+ parent_namespace, import_types)
250
+ return f"typing.Set[{items_type}]"
251
+ elif struct_type == 'map':
252
+ values_type = self.convert_structure_type_to_python(
253
+ class_name, field_name+'Map', structure_type.get('values', {'type': 'any'}),
254
+ parent_namespace, import_types)
255
+ return f"typing.Dict[str, {values_type}]"
256
+ elif struct_type == 'choice':
257
+ # Generate choice returns a Union type and populates import_types with the choice types
258
+ return self.generate_choice(structure_type, parent_namespace, write_file=True, import_types=import_types)
259
+ elif struct_type == 'tuple':
260
+ tuple_ref = self.generate_tuple(structure_type, parent_namespace, write_file=True)
261
+ import_types.add(tuple_ref)
262
+ return self.strip_package_from_fully_qualified_name(tuple_ref)
263
+ else:
264
+ return self.convert_structure_type_to_python(class_name, field_name, struct_type, parent_namespace, import_types)
265
+ return 'typing.Any'
266
+
267
+ def generate_class_or_choice(self, structure_schema: Dict, parent_namespace: str,
268
+ write_file: bool = True, explicit_name: str = '') -> str:
269
+ """ Generates a Class or Choice """
270
+ struct_type = structure_schema.get('type', 'object')
271
+ if struct_type == 'object':
272
+ return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
273
+ elif struct_type == 'choice':
274
+ return self.generate_choice(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
275
+ elif struct_type == 'tuple':
276
+ return self.generate_tuple(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
277
+ return 'typing.Any'
278
+
279
+ def generate_class(self, structure_schema: Dict, parent_namespace: str,
280
+ write_file: bool, explicit_name: str = '') -> str:
281
+ """ Generates a Python dataclass from JSON Structure object type """
282
+ import_types: Set[str] = set()
283
+
284
+ # Get name and namespace
285
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
286
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
287
+ namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
288
+ python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
289
+
290
+ if python_qualified_name in self.generated_types:
291
+ return python_qualified_name
292
+
293
+ # Check if this is an abstract type
294
+ is_abstract = structure_schema.get('abstract', False)
295
+
296
+ # Handle inheritance ($extends)
297
+ base_class = None
298
+ if '$extends' in structure_schema:
299
+ base_ref = structure_schema['$extends']
300
+ if isinstance(self.schema_doc, dict):
301
+ base_schema = self.resolve_ref(base_ref, self.schema_doc)
302
+ if base_schema:
303
+ ref_path = base_ref.split('/')
304
+ base_name = ref_path[-1]
305
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
306
+ base_class = self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
307
+ import_types.add(base_class)
308
+
309
+ # Generate properties
310
+ properties = structure_schema.get('properties', {})
311
+ required_props = structure_schema.get('required', [])
312
+
313
+ fields = []
314
+ for prop_name, prop_schema in properties.items():
315
+ field_def = self.generate_field(prop_name, prop_schema, class_name, schema_namespace,
316
+ required_props, import_types)
317
+ fields.append(field_def)
318
+
319
+ # Get docstring
320
+ doc = structure_schema.get('description', structure_schema.get('doc', class_name))
321
+
322
+ # Generate field docstrings
323
+ field_docstrings = [{
324
+ 'name': self.safe_name(field['name']),
325
+ 'original_name': field['name'],
326
+ 'type': field['type'],
327
+ 'is_primitive': field['is_primitive'],
328
+ 'is_enum': field['is_enum'],
329
+ 'docstring': self.generate_field_docstring(field, schema_namespace),
330
+ 'test_value': self.generate_test_value(field),
331
+ 'source_type': field.get('source_type', 'string'),
332
+ } for field in fields]
333
+
334
+ # If avro_annotation is enabled, convert JSON Structure schema to Avro schema
335
+ # This is embedded in the generated class for runtime Avro serialization
336
+ avro_schema_json = ''
337
+ if self.avro_annotation:
338
+ # Use JsonStructureToAvro to convert the schema
339
+ converter = JsonStructureToAvro()
340
+ schema_copy = structure_schema.copy()
341
+ avro_schema = converter.convert(schema_copy)
342
+ avro_schema_json = json.dumps(avro_schema).replace('\\"', '\'').replace('"', '\\"')
343
+
344
+ # Process template
345
+ class_definition = process_template(
346
+ "structuretopython/dataclass_core.jinja",
347
+ class_name=class_name,
348
+ docstring=doc,
349
+ fields=field_docstrings,
350
+ import_types=import_types,
351
+ base_package=self.base_package,
352
+ dataclasses_json_annotation=self.dataclasses_json_annotation,
353
+ avro_annotation=self.avro_annotation,
354
+ avro_schema_json=avro_schema_json,
355
+ is_abstract=is_abstract,
356
+ base_class=base_class,
357
+ )
358
+
359
+ if write_file:
360
+ self.write_to_file(namespace, class_name, class_definition)
361
+ self.generate_test_class(namespace, class_name, field_docstrings, import_types)
362
+
363
+ self.generated_types[python_qualified_name] = 'class'
364
+ self.generated_structure_types[python_qualified_name] = structure_schema
365
+ return python_qualified_name
366
+
367
+ def generate_field(self, prop_name: str, prop_schema: Dict, class_name: str,
368
+ parent_namespace: str, required_props: List, import_types: Set[str]) -> Dict:
369
+ """ Generates a field for a Python dataclass """
370
+ field_name = prop_name
371
+
372
+ # Check if this is a const field
373
+ if 'const' in prop_schema:
374
+ # Const fields are treated as class variables with default values
375
+ prop_type = self.convert_structure_type_to_python(
376
+ class_name, field_name, prop_schema, parent_namespace, import_types)
377
+ return {
378
+ 'name': field_name,
379
+ 'type': prop_type,
380
+ 'is_primitive': self.is_python_primitive(prop_type) or self.is_python_typing_struct(prop_type),
381
+ 'is_enum': False,
382
+ 'is_const': True,
383
+ 'const_value': prop_schema['const'],
384
+ 'source_type': prop_schema.get('type', 'string')
385
+ }
386
+
387
+ # Determine if required
388
+ is_required = prop_name in required_props if not isinstance(required_props, list) or \
389
+ len(required_props) == 0 or not isinstance(required_props[0], list) else \
390
+ any(prop_name in req_set for req_set in required_props)
391
+
392
+ # Get property type
393
+ prop_type = self.convert_structure_type_to_python(
394
+ class_name, field_name, prop_schema, parent_namespace, import_types)
395
+
396
+ # Add Optional if not required
397
+ if not is_required and not prop_type.startswith('typing.Optional['):
398
+ prop_type = f'typing.Optional[{prop_type}]'
399
+
400
+ # Get source type from structure schema
401
+ source_type = prop_schema.get('type', 'string') if isinstance(prop_schema.get('type'), str) else 'object'
402
+
403
+ return {
404
+ 'name': field_name,
405
+ 'type': prop_type,
406
+ 'is_primitive': self.is_python_primitive(prop_type) or self.is_python_typing_struct(prop_type),
407
+ 'is_enum': prop_type in self.generated_types and self.generated_types[prop_type] == 'enum',
408
+ 'is_const': False,
409
+ 'source_type': source_type
410
+ }
411
+
412
+ def generate_field_docstring(self, field: Dict, parent_namespace: str) -> str:
413
+ """Generates a field docstring for a Python dataclass"""
414
+ field_type = field['type']
415
+ field_name = self.safe_name(field['name'])
416
+ field_docstring = f"{field_name} ({field_type})"
417
+ return field_docstring
418
+
419
+ def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str,
420
+ write_file: bool) -> str:
421
+ """ Generates a Python enum from JSON Structure enum """
422
+ # Generate enum name from field name if not provided
423
+ class_name = pascal(structure_schema.get('name', field_name + 'Enum'))
424
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
425
+ namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
426
+ python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
427
+
428
+ if python_qualified_name in self.generated_types:
429
+ return python_qualified_name
430
+
431
+ symbols = [symbol if not is_python_reserved_word(symbol) else symbol + "_"
432
+ for symbol in structure_schema.get('enum', [])]
433
+
434
+ doc = structure_schema.get('description', structure_schema.get('doc', f'A {class_name} enum.'))
435
+
436
+ enum_definition = process_template(
437
+ "structuretopython/enum_core.jinja",
438
+ class_name=class_name,
439
+ docstring=doc,
440
+ symbols=symbols,
441
+ )
442
+
443
+ if write_file:
444
+ self.write_to_file(namespace, class_name, enum_definition)
445
+ self.generate_test_enum(namespace, class_name, symbols)
446
+
447
+ self.generated_types[python_qualified_name] = 'enum'
448
+ return python_qualified_name
449
+
450
+ def generate_choice(self, structure_schema: Dict, parent_namespace: str,
451
+ write_file: bool, explicit_name: str = '', import_types: Optional[Set[str]] = None) -> str:
452
+ """ Generates a Python Union type from JSON Structure choice """
453
+ choice_name = explicit_name if explicit_name else structure_schema.get('name', 'UnnamedChoice')
454
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
455
+ if import_types is None:
456
+ import_types = set()
457
+
458
+ # If the choice extends a base class, generate the base and derived classes first
459
+ if '$extends' in structure_schema:
460
+ base_ref = structure_schema['$extends']
461
+ if isinstance(self.schema_doc, dict):
462
+ base_schema = self.resolve_ref(base_ref, self.schema_doc)
463
+ if base_schema:
464
+ # Generate the base class
465
+ ref_path = base_ref.split('/')
466
+ base_name = ref_path[-1]
467
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
468
+ self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
469
+
470
+ # Generate types for each choice
471
+ choice_types = []
472
+ choices = structure_schema.get('choices', {})
473
+
474
+ for choice_key, choice_schema in choices.items():
475
+ if isinstance(choice_schema, dict):
476
+ if '$ref' in choice_schema:
477
+ # Resolve reference and generate the type
478
+ ref_schema = self.resolve_ref(choice_schema['$ref'], self.schema_doc if isinstance(self.schema_doc, dict) else None)
479
+ if ref_schema:
480
+ ref_path = choice_schema['$ref'].split('/')
481
+ ref_name = ref_path[-1]
482
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
483
+ qualified_name = self.generate_class(ref_schema, ref_namespace, write_file=True, explicit_name=ref_name)
484
+ import_types.add(qualified_name)
485
+ choice_types.append(qualified_name.split('.')[-1])
486
+ elif 'type' in choice_schema:
487
+ # Generate inline type
488
+ python_type = self.convert_structure_type_to_python(choice_name, choice_key, choice_schema, schema_namespace, import_types)
489
+ choice_types.append(python_type)
490
+
491
+ # Return Union type
492
+ if len(choice_types) == 0:
493
+ return 'typing.Any'
494
+ elif len(choice_types) == 1:
495
+ return choice_types[0]
496
+ else:
497
+ return f"typing.Union[{', '.join(choice_types)}]"
498
+
499
+ def generate_tuple(self, structure_schema: Dict, parent_namespace: str,
500
+ write_file: bool, explicit_name: str = '') -> str:
501
+ """ Generates a Python Tuple type from JSON Structure tuple """
502
+ # For now, return typing.Any as tuples need special handling
503
+ return 'typing.Any'
504
+
505
+ def generate_map_alias(self, structure_schema: Dict, parent_namespace: str,
506
+ write_file: bool) -> str:
507
+ """ Generates a Python TypeAlias for a top-level map type """
508
+ import_types: Set[str] = set()
509
+
510
+ # Get name and namespace
511
+ class_name = pascal(structure_schema.get('name', 'UnnamedMap'))
512
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
513
+ namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
514
+ python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
515
+
516
+ if python_qualified_name in self.generated_types:
517
+ return python_qualified_name
518
+
519
+ # Get the value type
520
+ values_schema = structure_schema.get('values', {'type': 'any'})
521
+ values_type = self.convert_structure_type_to_python(
522
+ class_name, 'Values', values_schema, schema_namespace, import_types)
523
+
524
+ # Get docstring
525
+ doc = structure_schema.get('description', structure_schema.get('doc', f'A {class_name} map type.'))
526
+
527
+ # Generate the type alias module
528
+ map_definition = process_template(
529
+ "structuretopython/map_alias.jinja",
530
+ class_name=class_name,
531
+ docstring=doc,
532
+ values_type=values_type,
533
+ import_types=import_types,
534
+ base_package=self.base_package
535
+ )
536
+
537
+ if write_file:
538
+ self.write_to_file(namespace, class_name, map_definition)
539
+
540
+ self.generated_types[python_qualified_name] = 'map'
541
+ return python_qualified_name
542
+
543
+ def generate_test_value(self, field: Dict) -> Any:
544
+ """Generates a test value for a given field"""
545
+ field_type = field['type']
546
+
547
+ def generate_value(field_type: str):
548
+ test_values = {
549
+ 'str': chr(39) + ''.join([chr(random.randint(97, 122)) for _ in range(0, 20)]) + chr(39),
550
+ 'bool': str(random.choice([True, False])),
551
+ 'int': f'int({random.randint(0, 100)})',
552
+ 'float': f'float({random.uniform(0, 100)})',
553
+ 'bytes': 'b"test_bytes"',
554
+ 'None': 'None',
555
+ 'datetime.date': 'datetime.date.today()',
556
+ 'datetime.datetime': 'datetime.datetime.now(datetime.timezone.utc)',
557
+ 'datetime.time': 'datetime.datetime.now(datetime.timezone.utc).time()',
558
+ 'decimal.Decimal': f'decimal.Decimal("{random.randint(0, 100)}.{random.randint(0, 100)}")',
559
+ 'datetime.timedelta': 'datetime.timedelta(days=1)',
560
+ 'uuid.UUID': 'uuid.uuid4()',
561
+ 'typing.Any': '{"test": "test"}'
562
+ }
563
+
564
+ def resolve(field_type: str) -> str:
565
+ pattern = re.compile(r'^(?:typing\.)*(Optional|List|Dict|Union|Set)\[(.+)\]$')
566
+ match = pattern.match(field_type)
567
+ if not match:
568
+ return field_type
569
+ outer_type, inner_type = match.groups()
570
+ if outer_type == 'Optional':
571
+ return inner_type
572
+ elif outer_type in ['List', 'Set']:
573
+ return resolve(inner_type)
574
+ elif outer_type == 'Dict':
575
+ _, value_type = inner_type.split(',', 1)
576
+ return resolve(value_type.strip())
577
+ elif outer_type == 'Union':
578
+ first_type = inner_type.split(',', 1)[0]
579
+ return resolve(first_type.strip())
580
+ return field_type
581
+
582
+ if field_type.startswith('typing.Optional['):
583
+ field_type = resolve(field_type)
584
+
585
+ if field_type.startswith('typing.List[') or field_type.startswith('typing.Set['):
586
+ field_type = resolve(field_type)
587
+ array_range = random.randint(1, 5)
588
+ return f"[{', '.join([generate_value(field_type) for _ in range(array_range)])}]"
589
+ elif field_type.startswith('typing.Dict['):
590
+ field_type = resolve(field_type)
591
+ dict_range = random.randint(1, 5)
592
+ dict_data = {}
593
+ for _ in range(dict_range):
594
+ dict_data[''.join([chr(random.randint(97, 122)) for _ in range(0, 20)])] = generate_value(field_type)
595
+ return f"{{{', '.join([chr(39)+key+chr(39)+f': {value}' for key, value in dict_data.items()])}}}"
596
+ elif field_type.startswith('typing.Union['):
597
+ field_type = resolve(field_type)
598
+ return generate_value(field_type)
599
+ return test_values.get(field_type, 'Test_' + field_type + '.create_instance()')
600
+
601
+ return generate_value(field_type)
602
+
603
+ def generate_test_class(self, package_name: str, class_name: str, fields: List[Dict[str, str]],
604
+ import_types: Set[str]) -> None:
605
+ """Generates a unit test class for a Python dataclass"""
606
+ test_class_name = f"Test_{class_name}"
607
+ tests_package_name = "test_" + package_name.replace('.', '_').lower()
608
+ test_class_definition = process_template(
609
+ "structuretopython/test_class.jinja",
610
+ package_name=package_name,
611
+ class_name=class_name,
612
+ test_class_name=test_class_name,
613
+ fields=fields,
614
+ import_types=import_types,
615
+ avro_annotation=self.avro_annotation,
616
+ dataclasses_json_annotation=self.dataclasses_json_annotation
617
+ )
618
+
619
+ base_dir = os.path.join(self.output_dir, "tests")
620
+ test_file_path = os.path.join(base_dir, f"{tests_package_name.replace('.', '_').lower()}.py")
621
+ if not os.path.exists(os.path.dirname(test_file_path)):
622
+ os.makedirs(os.path.dirname(test_file_path), exist_ok=True)
623
+ with open(test_file_path, 'w', encoding='utf-8') as file:
624
+ file.write(test_class_definition)
625
+
626
+ def generate_test_enum(self, package_name: str, class_name: str, symbols: List[str]) -> None:
627
+ """Generates a unit test class for a Python enum"""
628
+ test_class_name = f"Test_{class_name}"
629
+ tests_package_name = "test_" + package_name.replace('.', '_').lower()
630
+ test_class_definition = process_template(
631
+ "structuretopython/test_enum.jinja",
632
+ package_name=package_name,
633
+ class_name=class_name,
634
+ test_class_name=test_class_name,
635
+ symbols=symbols
636
+ )
637
+ base_dir = os.path.join(self.output_dir, "tests")
638
+ test_file_path = os.path.join(base_dir, f"{tests_package_name.replace('.', '_').lower()}.py")
639
+ if not os.path.exists(os.path.dirname(test_file_path)):
640
+ os.makedirs(os.path.dirname(test_file_path), exist_ok=True)
641
+ with open(test_file_path, 'w', encoding='utf-8') as file:
642
+ file.write(test_class_definition)
643
+
644
+ def write_to_file(self, package: str, class_name: str, python_code: str):
645
+ """Writes a Python class to a file"""
646
+ # Add 'struct' module to the package path
647
+ full_package = f"{package}.struct"
648
+ parent_package_name = '.'.join(full_package.split('.')[:-1])
649
+ parent_package_path = os.sep.join(parent_package_name.split('.')).lower()
650
+ directory_path = os.path.join(self.output_dir, "src", parent_package_path)
651
+ if not os.path.exists(directory_path):
652
+ os.makedirs(directory_path, exist_ok=True)
653
+ file_path = os.path.join(directory_path, f"{class_name.lower()}.py")
654
+
655
+ with open(file_path, 'w', encoding='utf-8') as file:
656
+ file.write(python_code)
657
+
658
+ def write_init_files(self):
659
+ """Writes __init__.py files to the output directories"""
660
+ def organize_generated_types():
661
+ generated_types_tree = {}
662
+ for generated_type, _ in self.generated_types.items():
663
+ parts = generated_type.split('.')
664
+ if len(parts) < 2:
665
+ continue
666
+ class_name = parts[-1]
667
+ module_name = parts[-2]
668
+ package_parts = parts[:-2]
669
+ current_node = generated_types_tree
670
+ for part in package_parts:
671
+ if part not in current_node:
672
+ current_node[part] = {}
673
+ current_node = current_node[part]
674
+ current_node[module_name] = class_name
675
+ return generated_types_tree
676
+
677
+ def collect_class_names(node):
678
+ class_names = []
679
+ for key, value in node.items():
680
+ if isinstance(value, dict):
681
+ class_names.extend(collect_class_names(value))
682
+ else:
683
+ class_names.append(value)
684
+ return class_names
685
+
686
+ def write_init_files_recursive(generated_types_tree, current_package: str):
687
+ import_statements = []
688
+ all_statement = []
689
+ for package_or_module_name, content in generated_types_tree.items():
690
+ if isinstance(content, dict):
691
+ class_names = collect_class_names(content)
692
+ if class_names:
693
+ import_statements.append(f"from .{package_or_module_name} import {', '.join(class_names)}")
694
+ all_statement.extend([f'"{name}"' for name in class_names])
695
+ write_init_files_recursive(content, current_package + ('.' if current_package else '') + package_or_module_name)
696
+ else:
697
+ class_name = content
698
+ import_statements.append(f"from .{package_or_module_name} import {class_name}")
699
+ all_statement.append(f'"{class_name}"')
700
+ if current_package and (import_statements or all_statement):
701
+ package_path = os.path.join(self.output_dir, 'src', current_package.replace('.', os.sep).lower())
702
+ init_file_path = os.path.join(package_path, '__init__.py')
703
+ if not os.path.exists(package_path):
704
+ os.makedirs(package_path, exist_ok=True)
705
+ with open(init_file_path, 'w', encoding='utf-8') as file:
706
+ file.write('\n'.join(import_statements) + '\n\n__all__ = [' + ', '.join(all_statement) + ']\n')
707
+
708
+ write_init_files_recursive(organize_generated_types(), '')
709
+
710
+ def write_pyproject_toml(self):
711
+ """Writes pyproject.toml file to the output directory"""
712
+ pyproject_content = process_template(
713
+ "structuretopython/pyproject_toml.jinja",
714
+ package_name=self.base_package.replace('_', '-'),
715
+ dataclasses_json_annotation=self.dataclasses_json_annotation,
716
+ avro_annotation=self.avro_annotation
717
+ )
718
+ with open(os.path.join(self.output_dir, 'pyproject.toml'), 'w', encoding='utf-8') as file:
719
+ file.write(pyproject_content)
720
+
721
+ def convert_schemas(self, structure_schemas: List, output_dir: str):
722
+ """ Converts JSON Structure schemas to Python dataclasses"""
723
+ self.output_dir = output_dir
724
+ if not os.path.exists(self.output_dir):
725
+ os.makedirs(self.output_dir, exist_ok=True)
726
+
727
+ # Register all schema IDs first
728
+ for structure_schema in structure_schemas:
729
+ self.register_schema_ids(structure_schema)
730
+
731
+ for structure_schema in structure_schemas:
732
+ self.schema_doc = structure_schema
733
+ if 'definitions' in structure_schema:
734
+ self.definitions = structure_schema['definitions']
735
+
736
+ if 'enum' in structure_schema:
737
+ self.generate_enum(structure_schema, structure_schema.get('name', 'Enum'),
738
+ structure_schema.get('namespace', ''), write_file=True)
739
+ elif structure_schema.get('type') == 'object':
740
+ self.generate_class(structure_schema, structure_schema.get('namespace', ''), write_file=True)
741
+ elif structure_schema.get('type') == 'choice':
742
+ self.generate_choice(structure_schema, structure_schema.get('namespace', ''), write_file=True)
743
+ elif structure_schema.get('type') == 'map':
744
+ self.generate_map_alias(structure_schema, structure_schema.get('namespace', ''), write_file=True)
745
+
746
+ self.write_init_files()
747
+ self.write_pyproject_toml()
748
+
749
+ def convert(self, structure_schema_path: str, output_dir: str):
750
+ """Converts JSON Structure schema to Python dataclasses"""
751
+ with open(structure_schema_path, 'r', encoding='utf-8') as file:
752
+ schema = json.load(file)
753
+ if isinstance(schema, dict):
754
+ schema = [schema]
755
+ return self.convert_schemas(schema, output_dir)
756
+
757
+
758
+ def convert_structure_to_python(structure_schema_path, py_file_path, package_name='', dataclasses_json_annotation=False, avro_annotation=False):
759
+ """Converts JSON Structure schema to Python dataclasses"""
760
+ if not package_name:
761
+ package_name = os.path.splitext(os.path.basename(structure_schema_path))[0].lower().replace('-', '_')
762
+
763
+ structure_to_python = StructureToPython(package_name, dataclasses_json_annotation=dataclasses_json_annotation, avro_annotation=avro_annotation)
764
+ structure_to_python.convert(structure_schema_path, py_file_path)
765
+
766
+
767
+ def convert_structure_schema_to_python(structure_schema, py_file_path, package_name='', dataclasses_json_annotation=False):
768
+ """Converts JSON Structure schema to Python dataclasses"""
769
+ structure_to_python = StructureToPython(package_name, dataclasses_json_annotation=dataclasses_json_annotation)
770
+ if isinstance(structure_schema, dict):
771
+ structure_schema = [structure_schema]
772
+ structure_to_python.convert_schemas(structure_schema, py_file_path)