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