pydantic-avro 0.8.1__tar.gz → 0.9.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/PKG-INFO +1 -1
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/pyproject.toml +3 -2
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/setup.py +1 -1
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/to_avro/types.py +88 -14
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/LICENSE +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/README.md +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/__init__.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/__main__.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/base.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/from_avro/__init__.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/from_avro/avro_to_pydantic.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/from_avro/class_registery.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/from_avro/types.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/py.typed +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/to_avro/__init__.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/to_avro/base.py +0 -0
- {pydantic_avro-0.8.1 → pydantic_avro-0.9.1}/src/pydantic_avro/to_avro/config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pydantic-avro"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Converting pydantic classes to avro schemas"
|
|
5
5
|
authors = ["Peter van 't Hof' <peter.vanthof@godatadriven.com>"]
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ python = ">=3.8.1,<4.0"
|
|
|
18
18
|
pydantic = ">=1.4,<3.0"
|
|
19
19
|
|
|
20
20
|
[tool.poetry.dev-dependencies]
|
|
21
|
-
coverage = { version = "^7.
|
|
21
|
+
coverage = { version = "^7.6.1", extras = ["toml"] }
|
|
22
22
|
pytest = "^8.3.5"
|
|
23
23
|
pytest-mock = "^3.10.0"
|
|
24
24
|
pyproject-flake8 = "^7.0.0"
|
|
@@ -28,6 +28,7 @@ pytest-cov = "^5.0.0"
|
|
|
28
28
|
mypy = "^1.1.1"
|
|
29
29
|
avro = "^1.12.0"
|
|
30
30
|
fastavro = "^1.8.1"
|
|
31
|
+
typing-extensions = "^4.13.2"
|
|
31
32
|
|
|
32
33
|
[tool.poetry.scripts]
|
|
33
34
|
pydantic-avro = "pydantic_avro.__main__:root_main"
|
|
@@ -18,7 +18,7 @@ entry_points = \
|
|
|
18
18
|
|
|
19
19
|
setup_kwargs = {
|
|
20
20
|
'name': 'pydantic-avro',
|
|
21
|
-
'version': '0.
|
|
21
|
+
'version': '0.9.1',
|
|
22
22
|
'description': 'Converting pydantic classes to avro schemas',
|
|
23
23
|
'long_description': '[](https://github.com/godatadriven/pydantic-avro/actions/workflows/python-package.yml)\n[](https://codecov.io/gh/godatadriven/pydantic-avro)\n[](https://badge.fury.io/py/pydantic-avro)\n[](https://github.com/godatadriven/pydantic-avro/actions/workflows/codeql-analysis.yml)\n\n# pydantic-avro\n\nThis library can convert a pydantic class to a avro schema or generate python code from a avro schema.\n\n### Install\n\n```bash\npip install pydantic-avro\n```\n\n### Pydantic class to avro schema\n\n```python\nimport json\nfrom typing import Optional\n\nfrom pydantic_avro.base import AvroBase\n\n\nclass TestModel(AvroBase):\n key1: str\n key2: int\n key2: Optional[str]\n\n\nschema_dict: dict = TestModel.avro_schema()\nprint(json.dumps(schema_dict))\n\n```\n\n### Avro schema to pydantic\n\n```shell\n# Print to stdout\npydantic-avro avro_to_pydantic --asvc /path/to/schema.asvc\n\n# Save it to a file\npydantic-avro avro_to_pydantic --asvc /path/to/schema.asvc --output /path/to/output.py\n```\n\n### Specify expected Avro type\n\n```python\nfrom datetime import datetime\nfrom pydantic import Field\nfrom pydantic_avro.base import AvroBase \n\nclass ExampleModel(AvroBase):\n field1: int = Field(..., avro_type="long") # Explicitly set Avro type to "long"\n field2: datetime = Field(..., avro_type="timestamp-millis") # Explicitly set Avro type to "timestamp-millis"\n```\n\n### Install for developers\n\n###### Install package\n\n- Requirement: Poetry 1.*\n\n```shell\npoetry install\n```\n\n###### Run unit tests\n```shell\npytest\ncoverage run -m pytest # with coverage\n# or (depends on your local env) \npoetry run pytest\npoetry run coverage run -m pytest # with coverage\n```\n\n##### Run linting\n\nThe linting is checked in the github workflow. To fix and review issues run this:\n```shell\nblack . # Auto fix all issues\nisort . # Auto fix all issues\npflake . # Only display issues, fixing is manual\n```\n',
|
|
24
24
|
'author': "Peter van 't Hof'",
|
|
@@ -61,6 +61,8 @@ AVRO_TYPE_MAPPING = {
|
|
|
61
61
|
},
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
PRIMITVE_TYPES = ["int", "long", "float", "double", "boolean", "null"]
|
|
65
|
+
|
|
64
66
|
|
|
65
67
|
def get_definition(ref: str, schema: dict):
|
|
66
68
|
"""Reading definition of base schema for nested structs"""
|
|
@@ -133,10 +135,16 @@ class AvroTypeConverter:
|
|
|
133
135
|
at = field_props.get("avro_type")
|
|
134
136
|
if "allOf" in field_props and len(field_props["allOf"]) == 1:
|
|
135
137
|
r = field_props["allOf"][0]["$ref"]
|
|
138
|
+
if ("prefixItems" in field_props or ("minItems" in field_props and "maxItems" in field_props)) and t == "array":
|
|
139
|
+
t = "tuple"
|
|
136
140
|
u = field_props.get("anyOf")
|
|
141
|
+
o = field_props.get("oneOf")
|
|
142
|
+
discriminator = field_props.get("discriminator")
|
|
137
143
|
|
|
138
144
|
if u is not None:
|
|
139
|
-
return self._union_to_avro(u, avro_type_dict)
|
|
145
|
+
return self._union_to_avro(u, avro_type_dict, discriminator)
|
|
146
|
+
elif o is not None:
|
|
147
|
+
return self._union_to_avro(o, avro_type_dict, discriminator)
|
|
140
148
|
elif r is not None:
|
|
141
149
|
return self._handle_references(r, avro_type_dict)
|
|
142
150
|
elif t is None:
|
|
@@ -162,6 +170,8 @@ class AvroTypeConverter:
|
|
|
162
170
|
avro_type_dict["type"] = "boolean"
|
|
163
171
|
elif t == "null":
|
|
164
172
|
avro_type_dict["type"] = "null"
|
|
173
|
+
elif t == "tuple":
|
|
174
|
+
return self._tuple_to_avro(field_props, avro_type_dict)
|
|
165
175
|
else:
|
|
166
176
|
raise NotImplementedError(
|
|
167
177
|
f"Type '{t}' not support yet, "
|
|
@@ -216,34 +226,98 @@ class AvroTypeConverter:
|
|
|
216
226
|
def _object_to_avro(self, field_props: dict) -> dict:
|
|
217
227
|
"""Returns a type of an object field"""
|
|
218
228
|
a = field_props.get("additionalProperties")
|
|
219
|
-
|
|
229
|
+
if not a or isinstance(a, bool):
|
|
230
|
+
value_type = "string"
|
|
231
|
+
else:
|
|
232
|
+
value_type = self._get_avro_type_dict(a)["type"]
|
|
220
233
|
return {"type": "map", "values": value_type}
|
|
221
234
|
|
|
222
|
-
def _union_to_avro(self, field_props: list, avro_type_dict: dict) -> dict:
|
|
223
|
-
"""Returns a type of a union field"""
|
|
235
|
+
def _union_to_avro(self, field_props: list, avro_type_dict: dict, discriminator: Optional[dict] = None) -> dict:
|
|
236
|
+
"""Returns a type of a union field, including discriminated unions"""
|
|
237
|
+
# Handle discriminated unions
|
|
238
|
+
if discriminator is not None:
|
|
239
|
+
return self._discriminated_union_to_avro(field_props, avro_type_dict, discriminator)
|
|
240
|
+
|
|
241
|
+
# Standard union handling (unchanged)
|
|
224
242
|
avro_type_dict["type"] = []
|
|
225
243
|
for union_element in field_props:
|
|
226
244
|
t = self._get_avro_type_dict(union_element)
|
|
227
245
|
avro_type_dict["type"].append(t["type"])
|
|
228
246
|
return avro_type_dict
|
|
229
247
|
|
|
248
|
+
def _tuple_to_avro(self, field_props: dict, avro_type_dict: dict) -> dict:
|
|
249
|
+
"""Returns a type of a tuple field"""
|
|
250
|
+
prefix_items = field_props.get("prefixItems")
|
|
251
|
+
if not prefix_items:
|
|
252
|
+
# Pydantic v1 there is no prefixItems, but minItems and maxItems and items is a list.
|
|
253
|
+
prefix_items = field_props.get("items")
|
|
254
|
+
if not prefix_items:
|
|
255
|
+
raise ValueError(f"Tuple Field '{field_props}' does not have any items .")
|
|
256
|
+
possible_types = []
|
|
257
|
+
for prefix_item in prefix_items:
|
|
258
|
+
item_type = self._get_avro_type(prefix_item, avro_type_dict).get("type")
|
|
259
|
+
if not item_type:
|
|
260
|
+
raise ValueError(f"Field '{avro_type_dict}' does not have a defined type.")
|
|
261
|
+
if isinstance(item_type, list):
|
|
262
|
+
possible_types.extend([x for x in item_type if x not in possible_types])
|
|
263
|
+
elif item_type not in possible_types:
|
|
264
|
+
possible_types.append(item_type)
|
|
265
|
+
avro_type_dict["type"] = {"type": "array", "items": possible_types}
|
|
266
|
+
return avro_type_dict
|
|
267
|
+
|
|
230
268
|
def _array_to_avro(self, field_props: dict, avro_type_dict: dict) -> dict:
|
|
231
269
|
"""Returns a type of an array field"""
|
|
232
|
-
items = field_props
|
|
270
|
+
items = field_props.get("items")
|
|
271
|
+
# In pydantic v1. items is a list, we need to handle this case first
|
|
272
|
+
# E.g. [{'type': 'number'}, {'type': 'number'}]}
|
|
273
|
+
if isinstance(items, list):
|
|
274
|
+
if not len(items):
|
|
275
|
+
raise ValueError(f"Field '{field_props}' does not have valid items.")
|
|
276
|
+
items = items[0]
|
|
277
|
+
if not isinstance(items, dict):
|
|
278
|
+
raise ValueError(f"Field '{field_props}' does not have valid items.")
|
|
233
279
|
tn = self._get_avro_type_dict(items)
|
|
234
280
|
# If items in array are an object:
|
|
235
281
|
if "$ref" in items:
|
|
236
282
|
tn = tn["type"]
|
|
237
283
|
# If items in array are a logicalType
|
|
238
|
-
if (
|
|
239
|
-
isinstance(tn, dict)
|
|
240
|
-
and isinstance(tn.get("type", {}), dict)
|
|
241
|
-
and tn.get("type", {}).get("logicalType") is not None
|
|
242
|
-
):
|
|
284
|
+
if isinstance(tn, dict) and isinstance(tn.get("type", None), (dict, list)):
|
|
243
285
|
tn = tn["type"]
|
|
244
|
-
# If items in array are an array, the structure must be corrected
|
|
245
|
-
if isinstance(tn, dict) and isinstance(tn.get("type", {}), dict) and tn.get("type", {}).get("type") == "array":
|
|
246
|
-
items = tn["type"]["items"]
|
|
247
|
-
tn = {"type": "array", "items": items}
|
|
248
286
|
avro_type_dict["type"] = {"type": "array", "items": tn}
|
|
249
287
|
return avro_type_dict
|
|
288
|
+
|
|
289
|
+
def _discriminated_union_to_avro(self, variants: list, avro_type_dict: dict, discriminator: dict) -> dict:
|
|
290
|
+
"""Handles discriminated unions with a discriminator field"""
|
|
291
|
+
discriminator_property = discriminator.get("propertyName", "type")
|
|
292
|
+
variant_types = []
|
|
293
|
+
|
|
294
|
+
# Process each variant in the union
|
|
295
|
+
for variant in variants:
|
|
296
|
+
# Get the discriminator value for this variant
|
|
297
|
+
disc_value = None
|
|
298
|
+
if "properties" in variant and discriminator_property in variant.get("properties", {}):
|
|
299
|
+
prop = variant["properties"][discriminator_property]
|
|
300
|
+
if "const" in prop:
|
|
301
|
+
disc_value = prop["const"]
|
|
302
|
+
elif "enum" in prop and len(prop["enum"]) == 1:
|
|
303
|
+
disc_value = prop["enum"][0]
|
|
304
|
+
|
|
305
|
+
# If we can't determine the discriminator value, generate a regular record
|
|
306
|
+
if disc_value is None:
|
|
307
|
+
variant_type = self._get_avro_type_dict(variant)
|
|
308
|
+
else:
|
|
309
|
+
# Create a named record for this variant
|
|
310
|
+
variant_name = f"{disc_value}Variant"
|
|
311
|
+
if "$ref" in variant:
|
|
312
|
+
ref_schema = get_definition(variant["$ref"], self.root_schema)
|
|
313
|
+
variant_fields = self.fields_to_avro_dicts(ref_schema)
|
|
314
|
+
else:
|
|
315
|
+
variant_fields = self.fields_to_avro_dicts(variant)
|
|
316
|
+
|
|
317
|
+
variant_type = {"type": {"type": "record", "name": variant_name, "fields": variant_fields}}
|
|
318
|
+
|
|
319
|
+
variant_types.append(variant_type["type"])
|
|
320
|
+
|
|
321
|
+
# Set the union of all variant types
|
|
322
|
+
avro_type_dict["type"] = variant_types
|
|
323
|
+
return avro_type_dict
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|