Skip to content

Commit 51330f8

Browse files
authored
Merge pull request #2 from marcosschroh/test-schema-serialization
Test schema serialization
2 parents bbf2dd4 + 9b45290 commit 51330f8

12 files changed

Lines changed: 439 additions & 71 deletions

dataclasses_avroschema/fields.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
float: FLOAT,
2424
bytes: BYTES,
2525
str: STRING,
26-
list: ARRAY,
27-
tuple: ENUM,
28-
dict: MAP,
26+
list: {"type": ARRAY},
27+
tuple: {"type": ENUM},
28+
dict: {"type": MAP},
2929
}
3030

3131
# excluding tuple because is a container
@@ -43,6 +43,7 @@ class Field:
4343
name: str
4444
type: typing.Any # store the python type
4545
default: typing.Any = dataclasses.MISSING
46+
default_factory: typing.Any = None
4647

4748
# for avro array field
4849
items_type: typing.Any = None
@@ -77,17 +78,26 @@ def __post_init__(self):
7778

7879
self.type = origin
7980

80-
@property
81-
def to_avro_type(self) -> PythonPrimitiveTypes:
82-
if self.type in PYTHON_PRIMITIVE_TYPES:
83-
avro_type = PYTHON_TYPE_TO_AVRO.get(self.type)
81+
def get_avro_type(self) -> PythonPrimitiveTypes:
82+
avro_type = PYTHON_TYPE_TO_AVRO.get(self.type)
8483

84+
if self.type in PYTHON_INMUTABLE_TYPES:
8585
if self.default is not dataclasses.MISSING and self.type is not tuple:
8686
if self.default is not None:
8787
return [avro_type, NULL]
8888
# means that default value is None
8989
return [NULL, avro_type]
9090

91+
return avro_type
92+
elif self.type in PYTHON_PRIMITIVE_CONTAINERS:
93+
if self.items_type:
94+
avro_type["items"] = self.items_type
95+
elif self.values_type:
96+
avro_type["values"] = self.values_type
97+
elif self.symbols:
98+
avro_type["symbols"] = self.symbols
99+
100+
avro_type["name"] = self.name
91101
return avro_type
92102
else:
93103
# we need to see what to to when is a custom type
@@ -102,15 +112,22 @@ def get_default_value(self):
102112
elif self.type is list:
103113
if self.default is None:
104114
return []
105-
106-
assert isinstance(self.default, list), f"List is required as default for field {self.name}"
107-
return self.default
108115
elif self.type is dict:
109116
if self.default is None:
110117
return {}
118+
elif self.default_factory not in (dataclasses.MISSING, None):
119+
if self.type is list:
120+
# expeting a callable
121+
default = self.default_factory()
122+
assert isinstance(default, list), f"List is required as default for field {self.name}"
111123

112-
assert isinstance(self.default, dict), f"Dict is required as default for field {self.name}"
113-
return self.default
124+
return default
125+
elif self.type is dict:
126+
# expeting a callable
127+
default = self.default_factory()
128+
assert isinstance(default, dict), f"Dict is required as default for field {self.name}"
129+
130+
return default
114131

115132
def render(self) -> OrderedDict:
116133
"""
@@ -127,26 +144,19 @@ def render(self) -> OrderedDict:
127144
The default key is optional.
128145
129146
If self.type is:
130-
* list, the OrderedDict will contains the key items
131-
* tuple, he OrderedDict will contains the key symbols
132-
* dict, he OrderedDict will contains the key values
147+
* list, the OrderedDict will contains the key items inside type
148+
* tuple, he OrderedDict will contains the key symbols inside type
149+
* dict, he OrderedDict will contains the key values inside type
133150
"""
134151
template = OrderedDict([
135152
("name", self.name),
136-
("type", self.to_avro_type),
153+
("type", self.get_avro_type()),
137154
])
138155

139156
default = self.get_default_value()
140157
if default is not None:
141158
template["default"] = default
142159

143-
if self.items_type:
144-
template["items"] = self.items_type
145-
elif self.values_type:
146-
template["values"] = self.values_type
147-
elif self.symbols:
148-
template["symbols"] = self.symbols
149-
150160
return template
151161

152162
def to_json(self) -> str:

dataclasses_avroschema/schema_generator.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ def generate_dataclass(klass_or_instance):
2222

2323
def parse_dataclasses_fields(self) -> typing.List[Field]:
2424
return [
25-
Field(dataclass_field.name, dataclass_field.type, dataclass_field.default)
25+
Field(
26+
dataclass_field.name,
27+
dataclass_field.type,
28+
dataclass_field.default,
29+
dataclass_field.default_factory
30+
)
2631
for dataclass_field in dataclasses.fields(self.dataclass)
2732
]
2833

@@ -40,7 +45,7 @@ def avro_schema(self) -> str:
4045
return json.dumps(self.generate_schema())
4146

4247
def to_python(self) -> typing.Dict[str, typing.Any]:
43-
return json.loads(self.render())
48+
return json.loads(self.avro_schema())
4449

4550
@property
4651
def get_fields(self) -> typing.List[Field]:

docs/avro_schema.md

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,107 @@ class User:
5454
has_pets: bool
5555
money: float
5656

57-
avro_schema = SchemaGenerator(User).avro_schema()
57+
SchemaGenerator(User).avro_schema()
58+
59+
{
60+
"type": "record",
61+
"name": "User",
62+
"fields": [
63+
{
64+
"name": "name",
65+
"type": "string"
66+
}
67+
,
68+
{
69+
"name": "age",
70+
"type": "int"
71+
},
72+
{
73+
"name": "has_pets",
74+
"type": "boolean"
75+
},
76+
{
77+
"name": "money",
78+
"type": "float"
79+
}
80+
],
81+
"doc": "User(name: str, age: int, has_pets: bool, money: float)"
82+
}'
5883
```
5984

60-
and that is it!! Each python field is related with a avro type. You can find the field relationships here:
85+
and that is it!! Each python field is related with a avro type. You can find the field relationships (here)[https://marcosschroh.github.io/dataclasses-avroschema/fields_specification/]:
86+
87+
### Enum, Array and Map fields
88+
89+
```python
90+
class UserAdvance:
91+
name: str
92+
age: int
93+
pets: typing.List[str] = dataclasses.field(default_factory=lambda: ['dog', 'cat']) # array field with default
94+
accounts: typing.Dict[str, int] = dataclasses.field(default_factory=lambda: {"key": 1}) # map field with default
95+
has_car: bool = False
96+
favorite_colors: typing.Tuple[str] = ("BLUE", "YELLOW", "GREEN") # enum field
97+
country: str = "Argentina"
98+
address: str = None
99+
100+
SchemaGenerator(UserAdvance, include_schema_doc=False).avro_schema()
101+
102+
'{
103+
"type": "record",
104+
"name": "UserAdvance",
105+
"fields": [
106+
{
107+
"name": "name",
108+
"type": "string"
109+
},
110+
{
111+
"name": "age",
112+
"type": "int"
113+
},
114+
{
115+
"name": "pets",
116+
"type": {
117+
"type": "array",
118+
"items": "string",
119+
"name": "pets"
120+
},
121+
"default": ["dog", "cat"]
122+
},
123+
{
124+
"name": "accounts",
125+
"type": {
126+
"type": "map",
127+
"values": "int",
128+
"name": "accounts"
129+
},
130+
"default": {"key": 1}
131+
},
132+
{
133+
"name": "has_car",
134+
"type": ["boolean", "null"],
135+
"default": false
136+
},
137+
{
138+
"name": "favorite_colors",
139+
"type": {
140+
"type": "enum",
141+
"symbols": ["BLUE", "YELLOW", "GREEN"],
142+
"name": "favorite_colors"
143+
}
144+
},
145+
{
146+
"name": "country",
147+
"type": ["string", "null"],
148+
"default": "Argentina"
149+
},
150+
{
151+
"name": "address",
152+
"type": ["null", "string"],
153+
"default": "null"
154+
}
155+
]
156+
}'
157+
```
61158

62159
### Special Avro attributes
63160

docs/fields_specification.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ So, the previous types can be matched to:
2626
| float | float |
2727
| double | float |
2828
| null | None |
29-
| double | wip |
3029
| bytes | wip |
3130

3231
Example:

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
fastavro>=0.22.3
2+
13
# Code quality
24
# ------------------------------------------------------------------------------
35
black==19.3b0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from setuptools import setup, find_packages
77

8-
__version__ = "0.1.0"
8+
__version__ = "0.1.1"
99

1010
with open("README.md") as readme_file:
1111
long_description = readme_file.read()

tests/conftest.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import os
2-
32
import json
4-
53
import dataclasses
6-
74
import typing
8-
95
import pytest
106

117

@@ -78,6 +74,21 @@ class UserAdvance:
7874
return UserAdvance
7975

8076

77+
@pytest.fixture
78+
def user_advance_with_defaults_dataclass():
79+
class UserAdvance:
80+
name: str
81+
age: int
82+
pets: typing.List[str] = dataclasses.field(default_factory=lambda: ['dog', 'cat'])
83+
accounts: typing.Dict[str, int] = dataclasses.field(default_factory=lambda: {"key": 1})
84+
has_car: bool = False
85+
favorite_colors: typing.Tuple[str] = ("BLUE", "YELLOW", "GREEN")
86+
country: str = "Argentina"
87+
address: str = None
88+
89+
return UserAdvance
90+
91+
8192
@pytest.fixture
8293
def user_avro_json():
8394
user_avro_path = os.path.join(AVRO_SCHEMAS_DIR, "user.avsc")
@@ -99,24 +110,15 @@ def user_advance_avro_json():
99110
return json.loads(schema_string)
100111

101112

113+
@pytest.fixture
114+
def user_advance_with_defaults_avro_json():
115+
user_avro_path = os.path.join(AVRO_SCHEMAS_DIR, "user_advance_with_defaults.avsc")
116+
schema_string = load(user_avro_path)
117+
return json.loads(schema_string)
118+
119+
102120
@pytest.fixture
103121
def user_extra_avro_attributes():
104122
user_avro_path = os.path.join(AVRO_SCHEMAS_DIR, "user_extra_avro_attributes.avsc")
105123
schema_string = load(user_avro_path)
106124
return json.loads(schema_string)
107-
108-
# private static final Logger log = LoggerFactory.getLogger(Main.class);
109-
110-
# public class ProductAccumulator {
111-
# private String omnitureId;
112-
# private String variantId;
113-
# private Double cev;
114-
# private Long total;
115-
116-
# public ProductAccumulator(String omnitureId, String variantId, Double cev, Long total) {
117-
# this.omnitureId = omnitureId;
118-
# this.variantId = variantId;
119-
# this.cev = cev;
120-
# this.total = total;
121-
# }
122-
# }

tests/schemas/avro/user_advance.avsc

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,52 @@
22
"type": "record",
33
"name": "UserAdvance",
44
"fields": [
5-
{"name": "name", "type": "string"},
6-
{"name": "age", "type": "int"},
7-
{"name": "pets", "type": "array", "items": "string"},
8-
{"name": "accounts", "type": "map", "values": "int"},
9-
{"name": "has_car", "type": ["boolean", "null"], "default": false},
10-
{"name": "favorite_colors", "type": "enum", "symbols": ["BLUE", "YELLOW", "GREEN"]},
11-
{"name": "country", "type": ["string", "null"], "default": "Argentina"},
12-
{"name": "address", "type": ["null", "string"], "default": "null"}
5+
{
6+
"name": "name",
7+
"type": "string"
8+
},
9+
{
10+
"name": "age",
11+
"type": "int"
12+
},
13+
{
14+
"name": "pets",
15+
"type": {
16+
"type": "array",
17+
"items": "string",
18+
"name": "pets"
19+
}
20+
},
21+
{
22+
"name": "accounts",
23+
"type": {
24+
"type": "map",
25+
"values": "int",
26+
"name": "accounts"
27+
}
28+
},
29+
{
30+
"name": "has_car",
31+
"type": ["boolean", "null"],
32+
"default": false
33+
},
34+
{
35+
"name": "favorite_colors",
36+
"type": {
37+
"type": "enum",
38+
"symbols": ["BLUE", "YELLOW", "GREEN"],
39+
"name": "favorite_colors"
40+
}
41+
},
42+
{
43+
"name": "country",
44+
"type": ["string", "null"],
45+
"default": "Argentina"
46+
},
47+
{
48+
"name": "address",
49+
"type": ["null", "string"],
50+
"default": "null"
51+
}
1352
]
1453
}

0 commit comments

Comments
 (0)