Marshalls¶
Imagine you need to serialize you data and adding some extra flavours on top of it. Now, imagine that Edgy models contain information that could be used but its not accessible directly upon the moment of serialization.
Here is where the marshalls
come into play.
The marshalls
will simply help you adding those extra validations on the top of your existing
model and add those same extras in the serialization process or even restrict the fields being
serialized, for instance, you might not want to show all the fields.
A marshall
is not designed to interact 100% with the database operations since that is done
by the Edgy model but it provides an interface that can also do that in case you want, the
save method.
Marshall¶
This is the main class that must be subclassed when creating a Marshall. There is where you declare all the extra fields and/or fields you want to serialize.
from edgy.core.marshalls import Marshall
When declaring the Marshall
you must declare a ConfigMarshall and then
all the extras you might want to add.
In a nutshell, this is how you can use a Marshall.
from typing import Any, Dict
from pydantic import BaseModel
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def display_name(self) -> str:
return f"Diplay name: {self.name}"
class UserExtra(BaseModel):
address: str
post_code: str
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(model=User, fields=["name", "email"])
details: fields.MarshallMethodField = fields.MarshallMethodField(field_type=str)
extra: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
def get_details(self, instance) -> str:
return instance.display_name
def get_extra(self, instance: edgy.Model) -> Dict[str, Any]:
extra = UserExtra(address="123 street", post_code="90210")
return extra.model_dump()
Ok, there is a lot to unwrap here but let us go step by step.
The Marshall
has a marshall_config
that must be declared specifying the model
and fields
.
The fields
is a list of the available fields of the model and it serves to specifically
specify which ones should the marshall serialize directly from the model.
Then, the extra
and details
are marshall fields
, that means, the fields that are not model fields
directly but must be serialized with the extra bit of information. You can check more details about
the Fields later on.
When the marshall is fully declared, you can simply do this:
data = {"name": "Edgy", "email": "edgy@example.com"}
marshall = UserMarshall(**data)
marshall.model_dump()
And the result will be:
{
"name": "Edgy",
"email": "edgy@example.com",
"details": "Diplay name: Edgy",
"extra": {"address": "123 street", "post_code": "90210"},
}
As you can see, the Marshall
is also a Pydantic model so you can take the full potential of it.
There are more operations and things you can do with marshalls regarding the fields that you can read in the next sections.
ConfigMarshall¶
To operate with the marshalls you will need to declare the marshall_config
which is simply a
typed dictionary containing the following keys:
- model - The Edgy model associated with the Marshall or a string
dotted.path
pointing to the model. - fields - A list of strings of the fields you want to include by default in the serialization of the marshall.
- exclude - A list of strings containing the name of the fields you don't want to have serialized.
Warning
There is a caveat though, you can only declare fields
or exclude
but not both and the model
is mandatory or else an exception is raised.
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
fields=["name", "email"],
)
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
The fields
also allow the use of __all__
. This means that you want all the fields declared in
your Edgy model.
Example
class CustomMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(model=User, fields=['__all__'])
Fields¶
Here is where the things get interesting. When declaring a Marshall
and want to add extra fields
to the serialization, you can do it by declaring two types of fields.
- MarshallField - Used the point to a
model
field, a pythonproperty
that is also declared inside the Edgy model or a function. - MarshallMethodField - Used to point to a function that is declared inside the marshall and not inside the model.
To use the fields, you can simply import it.
from edgy.core.marshalls import fields
All the fields have the mandatory attribute field_type
. This is used to declare which type
of field should be used for automatic validation of Pydantic.
Example
class CustomMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(model="myapp.models.MyModel", fields=['__all__'])
details: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
age: fields.MarshallField = fields.MarshallField(int, source="age")
MarshallField¶
This is the most common field you can declare in your marshall.
Parameters¶
- field_type - The Python type that is used by Pydantic to validate the data.
- source - The source of the field to be gathered from the model. It can be directly the model field, a property or a function.
All of the values passed in the source must come from the Edgy Model.
Example
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def age(self) -> int:
return 1
def details(self) -> str: ...
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
name: fields.MarshallField = fields.MarshallField(
field_type=str,
source="name",
)
age: fields.MarshallField = fields.MarshallField(
field_type=int,
source="age",
)
details: fields.MarshallField = fields.MarshallField(
field_type=str,
source="details",
)
MarshallMethodField¶
This function is used to get extra information that is provided by the Marshall
itself.
When declaring a MarshallMethodField
you must have the function get_
with the corresponding
name of the field used by the MarshallMethodField
.
When declaring the function, Edgy will automatically inject an object (instance) of the Edgy model
declared in the marshall_config
. This instance is not persisted in the database unless you
specifically save it, which means, the primary_key
will not be available until then but
the remaining object, functions, attributes and operations, are.
Parameters¶
- field_type - The Python type that is used by Pydantic to validate the data.
Example
from typing import Any, Dict
from pydantic import BaseModel
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def display_name(self) -> str:
return f"Diplay name: {self.name}"
class UserExtra(BaseModel):
address: str
post_code: str
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
display_name: fields.MarshallMethodField = fields.MarshallMethodField(field_type=str)
data: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
def get_display_name(self, instance: edgy.Model) -> str:
return instance.display_name()
def get_data(self, instance: edgy.Model) -> Dict[str, Any]:
extra = UserExtra(address="123 street", post_code="90210")
return extra.model_dump()
save()
¶
Since the Marshall is also a Pydantic base model, the same as Edgy, there may be some times where you would like to persist the data directly using the marshall instead of using complicated processes to make it happen.
This is also possible as Edgy made it simple for you. In the same way an Edgy model has the save()
so does the marshall
. In reality, what Edgy is doing is performing that same Edgy save()
operation
for you.
How does it work? In the same way it would work for a normal Edgy model.
Example¶
Let us assume the following example.
from typing import Any, Dict
from pydantic import BaseModel
from tests.settings import DATABASE_URL
import edgy
from edgy.core.marshalls import Marshall, fields
from edgy.core.marshalls.config import ConfigMarshall
from edgy.testclient import DatabaseTestClient as Database
database = Database(url=DATABASE_URL)
registry = edgy.Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=False)
email: str = edgy.EmailField(max_length=100, null=False)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
class Meta:
registry = registry
@property
def display_name(self) -> str:
return f"Diplay name: {self.name}"
class UserExtra(BaseModel):
address: str
post_code: str
class UserMarshall(Marshall):
marshall_config: ConfigMarshall = ConfigMarshall(
model=User,
exclude=["language"],
)
display_name: fields.MarshallMethodField = fields.MarshallMethodField(field_type=str)
data: fields.MarshallMethodField = fields.MarshallMethodField(field_type=Dict[str, Any])
def get_display_name(self, instance: edgy.Model) -> str:
return instance.display_name()
def get_data(self, instance: edgy.Model) -> Dict[str, Any]:
extra = UserExtra(address="123 street", post_code="90210")
return extra.model_dump()
Now, to create and save an instance of the model User
, we simply need to:
data = {
"name": "Edgy",
"email": "edgy@example.com",
"language": "EN",
"description": "Nice marshall"
}
marshall = UserMarshall(**data)
await marshall.save()
The marshall is smart enough to understand what fields belong to the model and what fields are custom and specific to the marshall and persists it.
Extra considerations¶
Creating a marshall
its easy and very intuitive but there are some considerations you must have.
Model fields with null=False
¶
When declaring the ConfigMarshall fields
, you
**must select at least the mandatory fields necessary, null=False
, or a MarshallFieldDefinitionError
will be raised.
This is used to prevent any unnecessary errors from happening when the creation of the model occurs.
Model validators¶
This remains exactly was it was before, meaning, if you want to validate the fields of the model
when creating an instance (persisted or not), that can and should be done using the normal
Pydantic @model_validator
and @field_validator
.