Skip to content

Commit

Permalink
feat: option to exclude fields on the output schema added.
Browse files Browse the repository at this point in the history
  • Loading branch information
marcosschroh committed Sep 12, 2023
1 parent 483e0b7 commit 2a527f9
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 13 deletions.
19 changes: 13 additions & 6 deletions dataclasses_avroschema/schema_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ def __post_init__(self) -> None:
self.fields_map = {field.name: field for field in self.fields}

def parse_dataclasses_fields(self) -> typing.List[Field]:
exclude = self.metadata.exclude

if utils.is_faust_model(self.klass):
return self.parse_faust_fields()
return self.parse_faust_fields(exclude=exclude)
elif utils.is_pydantic_model(self.klass):
return self.parse_pydantic_fields()
return self.parse_fields()
return self.parse_pydantic_fields(exclude=exclude)
return self.parse_fields(exclude=exclude)

def parse_fields(self) -> typing.List[Field]:
def parse_fields(self, exclude: typing.List) -> typing.List[Field]:
return [
AvroField(
dataclass_field.name,
Expand All @@ -87,12 +89,16 @@ def parse_fields(self) -> typing.List[Field]:
parent=self.parent,
)
for dataclass_field in dataclasses.fields(self.klass)
if dataclass_field.name not in exclude
]

def parse_faust_fields(self) -> typing.List[Field]:
def parse_faust_fields(self, exclude: typing.List) -> typing.List[Field]:
schema_fields = []

for dataclass_field in dataclasses.fields(self.klass):
if dataclass_field.name in exclude:
continue

faust_field = dataclass_field.default
metadata = dataclass_field.metadata
default_factory = dataclasses.MISSING
Expand Down Expand Up @@ -122,7 +128,7 @@ def parse_faust_fields(self) -> typing.List[Field]:

return schema_fields

def parse_pydantic_fields(self) -> typing.List[Field]:
def parse_pydantic_fields(self, exclude: typing.List) -> typing.List[Field]:
return [
AvroField(
model_field.name,
Expand All @@ -136,6 +142,7 @@ def parse_pydantic_fields(self) -> typing.List[Field]:
parent=self.parent,
)
for model_field in self.klass.__fields__.values()
if model_field.name not in exclude
]

def get_rendered_fields(self) -> typing.List[OrderedDict]:
Expand Down
2 changes: 2 additions & 0 deletions dataclasses_avroschema/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class SchemaMetadata:
alias_nested_items: typing.Dict[str, str] = dataclasses.field(default_factory=dict)
dacite_config: typing.Optional[JsonDict] = None
field_order: typing.Optional[typing.List[str]] = None
exclude: typing.List[str] = dataclasses.field(default_factory=list)

@classmethod
def create(cls: typing.Type["SchemaMetadata"], klass: type) -> typing.Any:
Expand All @@ -105,6 +106,7 @@ def create(cls: typing.Type["SchemaMetadata"], klass: type) -> typing.Any:
alias_nested_items=getattr(klass, "alias_nested_items", {}),
dacite_config=getattr(klass, "dacite_config", None),
field_order=getattr(klass, "field_order", None),
exclude=getattr(klass, "exclude", []),
)

def get_alias_nested_items(self, name: str) -> typing.Optional[str]:
Expand Down
2 changes: 1 addition & 1 deletion docs/faust_records.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ from dataclasses_avroschema.faust import AvroRecord
class UserAdvance(AvroRecord):
name: str
age: int
pets: typing.List[str] = fields.StringField(required=False, default=['dog', 'cat'])
pets: typing.List[str] = fields.StringField(required=False, default=["dog", "cat"])
accounts: typing.Dict[str, int] = fields.IntegerField(required=False, default={"key": 1})
has_car: bool = fields.BooleanField(required=False, default=False)
favorite_colors: typing.Tuple[str] = fields.StringField(required=False, default=("BLUE", "YELLOW", "GREEN"))
Expand Down
54 changes: 51 additions & 3 deletions docs/records.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ User.avro_schema()

## Class Meta

The `class Meta` is used to specify schema attributes that are not represented by the class fields like `namespace`, `aliases` and whether to include the `schema documentation`. Also custom schema name (the default is the class' name) via `schema_name` attribute, and `alias_nested_items` when you have nested items and you want to use custom naming for them, `custom dacite` configuration can be provided and `field_order`
The `class Meta` is used to specify schema attributes that are not represented by the class fields like `namespace`, `aliases` and whether to include the `schema documentation`. Also custom schema name (the default is the class' name) via `schema_name` attribute, `alias_nested_items` when you have nested items and you want to use custom naming for them, `custom dacite` configuration can be provided, `field_order` and `exclude`.

```python title="Class Meta description"
class Meta:
Expand All @@ -55,6 +55,7 @@ class Meta:
aliases = ["User", "My favorite User"]
alias_nested_items = {"address": "Address"}
field_order = ["age", "name",]
exclude = ["last_name",]
dacite_config = {
"strict_unions_match": True,
"strict": True,
Expand All @@ -69,7 +70,9 @@ class Meta:

`alias_nested_items Optional[Dict[str, str]]`: Nested items names

`field_order Optiona[List[str]]`: List of field names to specify their order to the output schema
`field_order Optiona[List[str]]`: List of field names to specify their order to the output schema

`exclude Optiona[List[str]]`: List of field names to be excluded in the output schema

## Record to json and dict

Expand Down Expand Up @@ -421,7 +424,7 @@ class User(AvroModel):
field_order = ["has_pets",]
```

and now it represents the schame
which represents the schema

```json
{
Expand All @@ -439,3 +442,48 @@ and now it represents the schame

!!! warning
Schemas with the same fields but with different order are *NOT* the same schema. In avro the field order is important

## Excluding fields

It is possible to exclude fields from the schema using the `Meta.exclude` attribute. This can be helpful when we have fields that are not serializable.

```python
import dataclasses

from dataclasses_avroschema import AvroModel


class User(AvroModel):
"An User"
name: str
age: int
last_name: str = "Bond"

class Meta:
namespace = "test.com.ar/user/v1"
aliases = [
"User",
"My favorite User",
]
exclude = [
"last_name",
]
```

which represents the schema *whiout the field `last_name`*

```json
{
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "age", "type": "long"}
],
"doc": "An User",
"namespace": "test.com.ar/user/v1",
"aliases": ["User", "My favorite User"]
```

!!! warning
If a *required* field is excluded from the schema then the deserialization will *FAIL* because a default value is not provided
26 changes: 23 additions & 3 deletions tests/schemas/test_faust.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import uuid

import pytest
from faust.models import fields

from dataclasses_avroschema import types, utils
from dataclasses_avroschema.faust import AvroRecord
Expand Down Expand Up @@ -49,8 +50,8 @@ def test_faust_record_schema_complex_types_with_defaults(user_advance_with_defau
class UserAdvance(AvroRecord):
name: str
age: int
pets: typing.List[str] = dataclasses.field(default_factory=lambda: ["dog", "cat"])
accounts: typing.Dict[str, int] = dataclasses.field(default_factory=lambda: {"key": 1})
pets: typing.List[str] = fields.StringField(required=False, default=["dog", "cat"])
accounts: typing.Dict[str, int] = fields.IntegerField(required=False, default={"key": 1})
has_car: bool = False
favorite_colors: color_enum = color_enum.BLUE
country: str = "Argentina"
Expand Down Expand Up @@ -238,7 +239,6 @@ class UnionSchema(AvroRecord):
def test_field_metadata() -> None:
field_matadata = {"aliases": ["username"]}

@dataclasses.dataclass
class UserRecord(AvroRecord):
name: str = dataclasses.field(metadata=field_matadata)

Expand All @@ -247,6 +247,26 @@ class UserRecord(AvroRecord):
assert schema["fields"][0]["aliases"] == field_matadata["aliases"]


def test_exclude_field_from_schema(user_extra_avro_attributes):
class User(AvroRecord):
"An User"
name: str
age: int
last_name: fields.StringField = fields.StringField(required=False, defualt="Bond")

class Meta:
namespace = "test.com.ar/user/v1"
aliases = [
"User",
"My favorite User",
]
exclude = [
"last_name",
]

assert User.avro_schema() == json.dumps(user_extra_avro_attributes)


def test_not_faust_not_installed(monkeypatch):
monkeypatch.setattr(utils, "faust", None)

Expand Down
22 changes: 22 additions & 0 deletions tests/schemas/test_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,25 @@ class Message(AvroBaseModel):

assert Message.deserialize(event, serialization_type="avro-json") == message
assert Message.deserialize(event, serialization_type="avro-json", create_instance=False) != message.dict()


def test_exclude_field_from_schema(user_extra_avro_attributes):
class User(AvroBaseModel):
"An User"
name: str
age: int
last_name: str = "Bond"

class Meta:
namespace = "test.com.ar/user/v1"
aliases = [
"User",
"My favorite User",
]
exclude = [
"last_name",
]

user = User.fake()
assert User.avro_schema() == json.dumps(user_extra_avro_attributes)
assert User.deserialize(user.serialize()) == user
22 changes: 22 additions & 0 deletions tests/schemas/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,28 @@ class Meta:
assert User.avro_schema() == json.dumps(order_fields_schema)


def test_exclude_field_from_schema(user_extra_avro_attributes):
class User(AvroModel):
"An User"
name: str
age: int
last_name: str = "Bond"

class Meta:
namespace = "test.com.ar/user/v1"
aliases = [
"User",
"My favorite User",
]
exclude = [
"last_name",
]

user = User.fake()
assert User.avro_schema() == json.dumps(user_extra_avro_attributes)
assert User.deserialize(user.serialize()) == user


def test_validate():
@dataclass
class User(AvroModel):
Expand Down

0 comments on commit 2a527f9

Please sign in to comment.