Fields¶
Fields are what is used within model declaration (data types) and defines wich types are going to be generated in the SQL database when generated.
Data types¶
As Edgy is a new approach on the top of Encode ORM, the following keyword arguments are supported in all field types.
Check
The data types are also very familiar for those with experience with Django model fields.
- primary_key - A boolean. Determine if a column is primary key. Check the primary_key restrictions with Edgy.
- exclude - An bool indicating if the field is included in model_dump
- default - A value or a callable (function).
- index - A boolean. Determine if a database index should be created.
- inherit - A boolean. Determine if a field can be inherited in submodels. Default is True. It is used by PKField, RelatedField and the injected ID Field.
- skip_absorption_check - A boolean. Default False. Dangerous option! By default when defining a CompositeField with embedded fields and the
absorb_existing_fields
option it is checked that the field type of the absorbed field is compatible with the field type of the embedded field. This option skips the check. - unique - A boolean. Determine if a unique constraint should be created for the field. Check the unique_together for more details.
All the fields are required unless on the the following is set:
-
null - A boolean. Determine if a column allows null.
Set default to
None
-
server_default - instance, str, Unicode or a SQLAlchemy
sqlalchemy.sql.expression.text
construct representing the DDL DEFAULT value for the column. - comment - A comment to be added with the field in the SQL database.
- secret - A special attribute that allows to call the exclude_secrets and avoid accidental leakage of sensitive data.
Available fields¶
All the values you can pass in any Pydantic Field are also 100% allowed within Mongoz fields.
Importing fields¶
You have a few ways of doing this and those are the following:
import edgy
From edgy
you can access all the available fields.
from edgy.core.db import fields
From fields
you should be able to access the fields directly.
from edgy.core.db.fields import BigIntegerField
You can import directly the desired field.
All the fields have specific parameters beisdes the ones mentioned in data types.
BigIntegerField¶
import edgy
class MyModel(edgy.Model):
big_number: int = edgy.BigIntegerField(default=0)
another_big_number: int = edgy.BigIntegerField(minimum=10)
...
This field is used as a default field for the id
of a model.
Parameters:¶
- minimum - An integer, float or decimal indicating the minimum.
- maximum - An integer, float or decimal indicating the maximum.
- max_digits - Maximum digits allowed.
- multiple_of - An integer, float or decimal indicating the multiple of.
- decimal_places - The total decimal places.
IntegerField¶
import edgy
class MyModel(edgy.Model):
a_number: int = edgy.IntegerField(default=0)
another_number: int = edgy.IntegerField(minimum=10)
...
Parameters:¶
- minimum - An integer, float or decimal indicating the minimum.
- maximum - An integer, float or decimal indicating the maximum.
- max_digits - Maximum digits allowed.
- multiple_of - An integer, float or decimal indicating the multiple of.
- decimal_places - The total decimal places.
BooleanField¶
import edgy
class MyModel(edgy.Model):
is_active: bool = edgy.BooleanField(default=True)
is_completed: bool = edgy.BooleanField(default=False)
...
CharField¶
import edgy
class MyModel(edgy.Model):
description: str = edgy.CharField(max_length=255)
title: str = edgy.CharField(max_length=50, min_length=200)
...
Parameters:¶
- max_length - An integer indicating the total length of string.
- min_length - An integer indicating the minimum length of string.
ChoiceField¶
from enum import Enum
import edgy
class Status(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class MyModel(edgy.Model):
status: Status = edgy.ChoiceField(choices=Status, default=Status.ACTIVE)
...
Parameters¶
- choices - An enum containing the choices for the field.
CompositeField¶
The CompositeField is a little bit different from the normal fields. It takes a parameter inner_fields
and distributes write or read access to the fields
referenced in inner_fields
. It hasn't currently all field parameters. Especially not the server parameters.
For distributing the field parameters it uses the Descriptor Protocol
for reading and to_model
for writing.
Optionally a pydantic model can be provided via the model argument.
CompositeField defines no columns as it is a meta field.
Note there is also BaseCompositeField. It can be used for implementing own CompositeField-like fields.
import edgy
class MyModel(edgy.Model):
email: str = edgy.EmailField(max_length=60, null=True)
sent: datetime.datetime = edgy.DateTimeField(, null=True)
composite: edgy.CompositeField = edgy.CompositeField(inner_fields=["email", "sent"])
...
class MyModel(edgy.Model):
email: str = edgy.EmailField(max_length=60, null=True, read_only=True)
sent: datetime.datetime = edgy.DateTimeField(, null=True, read_only=True)
composite = edgy.CompositeField(inner_fields=["email", "sent"])
...
obj = MyModel()
obj.composite = {"email": "foobar@example.com", "sent": datetime.datetime.now()}
# retrieve as dict
ddict = obj.composite
The contained fields are serialized like normal fields. So if this is not wanted, the fields need the exclude attribute/parameter set.
!!! Note: The inherit flag is set to False for all fields created by a composite. This is because of inheritance.
Parameters¶
- inner_fields - Required. A sequence containing the external field names mixed with embedded field definitions (name, Field) tuples. As an alternative it is possible to provide an Edgy Model (abstract or non-abstract) or a dictionary in the format: key=name, value=Field
- unsafe_json_serialization - Default False. Normally when serializing in json mode, CompositeFields are ignored when they don't have a pydantic model set. This option includes such CompositeFields in the dump.
- absorb_existing_fields - Default False. Don't fail if fields speficied with (name, Field) tuples already exists. Treat them as internal fields. The existing fields are checked if they are a subclass of the Field or have the attribute
skip_absorption_check
set - model - Default None (not set).Return a pydantic model instead of a dict.
- prefix_embedded - Default "". Prefix the field names of embedded fields (not references to external fields). Useful for implementing embeddables
Note: embedded fields are shallow-copied. This way it is safe to provide the same inner_fields object to multiple CompositeFields.
Note: there is a special parameter for model: ConditionalRedirect
.
It changes the behaviour of CompositeField in this way:
inner_fields
with 1 element: Reads and writes are redirected to this field. When setting a dict or pydantic BaseModel the value is tried to be extracted like in the normal mode. Otherwise the logic of the single field is used. When returning only the value of the single field is returnedinner_fields
with >1 element: normal behavior. Dicts are returned
Inheritance¶
CompositeFields are evaluated in non-abstract models. When overwritten by another field and the evaluation didn't take place yet no fields are generated.
When overwritten after evaluation the fields are still lingering around.
You can also overwrite from CompositeField generated fields in subclasses regardless if the CompositeField used absorb_existing_fields inside.
You may want to use ExcludeField to remove fields.
DateField¶
import datetime
import edgy
class MyModel(edgy.Model):
created_at: datetime.date = edgy.DateField(default=datetime.date.today)
...
Parameters¶
- auto_now - A boolean indicating the
auto_now
enabled. Useful for auto updates. - auto_now_add - A boolean indicating the
auto_now_add
enabled. This will ensure that it is only added once.
DateTimeField¶
import datetime
import edgy
class MyModel(edgy.Model):
created_at: datetime.datetime = edgy.DateTimeField(default=datetime.datetime.now)
...
Parameters¶
- auto_now - A boolean indicating the
auto_now
enabled. Useful for auto updates. - auto_now_add - A boolean indicating the
auto_now_add
enabled. This will ensure that it is only added once.
DecimalField¶
import decimal
import edgy
class MyModel(edgy.Model):
price: decimal.Decimal = edgy.DecimalField(max_digits=5, decimal_places=2, null=True)
...
Parameters¶
- minimum - An integer indicating the minimum.
- maximum - An integer indicating the maximum.
- max_digits - An integer indicating the total maximum digits.
- decimal_places - An integer indicating the total decimal places.
- multiple_of - An integer, float or decimal indicating the multiple of.
EmailField¶
import edgy
class MyModel(edgy.Model):
email: str = edgy.EmailField(max_length=60, null=True)
...
Derives from the same as CharField and validates the email value.
ExcludeField¶
Remove inherited fields by masking them from the model. This way a field can be removed in subclasses.
ExcludeField is a stub field and can be overwritten itself in case a Model wants to readd the field.
When given an argument in the constructor the argument is silently ignored. When trying to set/access the attribute on a model instance, an AttributeError is raised.
import edgy
class AbstractModel(edgy.Model):
email: str = edgy.EmailField(max_length=60, null=True)
price: float = edgy.FloatField(null=True)
class Meta:
abstract = True
class ConcreteModel(AbstractModel):
email: Type[None] = edgy.ExcludeField()
# works
obj = ConcreteModel(email="foo@example.com", price=1.5)
# fails with AttributeError
variable = obj.email
obj.email = "foo@example.com"
FloatField¶
import edgy
class MyModel(edgy.Model):
price: float = edgy.FloatField(null=True)
...
Derives from the same as IntergerField and validates the decimal float.
ForeignKey¶
import edgy
class User(edgy.Model):
is_active: bool = edgy.BooleanField(default=True)
class Profile(edgy.Model):
is_enabled: bool = edgy.BooleanField(default=True)
class MyModel(edgy.Model):
user: User = edgy.ForeignKey("User", on_delete=edgy.CASCADE)
profile: Profile = edgy.ForeignKey(Profile, on_delete=edgy.CASCADE, related_name="my_models")
...
Hint you can change the base for the reverse end with embed_parent:
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class Address(edgy.Model):
street = edgy.CharField(max_length=100)
city = edgy.CharField(max_length=100)
class Meta:
# we don't want a table just a template
abstract = True
class User(edgy.Model):
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
class Profile(edgy.Model):
name: str = edgy.CharField(max_length=100)
user: User = edgy.OneToOne("User", on_delete=edgy.CASCADE, embed_parent=("address", "profile"))
address: Address = Address
class Meta:
registry = models
user = edgy.run_sync(User.query.create())
edgy.run_sync(Profile.query.create(name="edgy", user=user, address={"street": "Rainbowstreet 123", "city": "London"}))
# use the reverse link
address = edgy.run_sync(user.profile.get())
# access the profile
address.profile
when on the user model the profile
reverse link is queried, by default the address embeddable is returned.
Queries continue to use the Profile Model as base because address isn't a RelationshipField.
The Profile object can be accessed by the profile
attribute we choosed as second parameter.
When the second parameter is empty, the parent object is not included as attribute.
Parameters¶
- to - A string model name or a class object of that same model.
- related_name - The name to use for the relation from the related object back to this one. Can be set to
False
to disable a reverse connection. Note: Setting toFalse
will also prevent prefetching and reversing via__
. See also related_name for defaults - related_fields - The columns or fields to use for the foreign key. If unset or empty, the primary key(s) are used.
- embed_parent (to_attr, as_attr) - When accessing the reverse relation part, return to_attr instead and embed the parent object in as_attr (when as_attr is not empty). Default None (which disables it).
- no_constraint - Skip creating a constraint. Note: if set and index=True an index will be created instead.
- on_delete - A string indicating the behaviour that should happen on delete of a specific
model. The available values are
CASCADE
,SET_NULL
,RESTRICT
and those can also be imported fromedgy
. - on_update - A string indicating the behaviour that should happen on update of a specific
model. The available values are
CASCADE
,SET_NULL
,RESTRICT
and those can also be imported fromedgy
.from edgy import CASCADE, SET_NULL, RESTRICT
- relation_fn - Optionally drop a function which returns a Relation for the reverse side. This will be used by the RelatedField (if it is created). Used by the ManyToMany field.
- reverse_path_fn - Optionally drop a function which handles the traversal from the reverse side. Used by the ManyToMany field.
!!! Note:
The index parameter can improve the performance and is strongly recommended especially with no_constraint
but also
ForeignKeys with constraint will benefit. By default off because conflicts are easily to provoke when reinitializing models (tests with database fixture scope="function").
This is no concern for webservers where models are initialized once.
unique
uses internally an index and index=False
will be ignored.
!!! Note:
There is a reverse_name
argument which can be used when related_name=False
to specify a field for reverse relations.
It is useless except if related_name is False
because it is otherwise overwritten.
The reverse_name
argument is used for finding the reverse field of the relationship.
!!! Note:
When embed_parent
is set, queries start to use the second parameter of embed_parent
if it is a RelationshipField.
If it is empty, queries cannot access the parent anymore when the first parameter points to a RelationshipField
.
This is mode is analogue to ManyToMany fields.
Otherwise, the first parameter points not to a RelationshipField
(e.g. embeddable, CompositeField), queries use still the model, without the prefix stripped.
RefForeignKey¶
from edgy import RefForeignKey
This is unique to Edgy and has dedicated place in the documentation just to explain what it is and how to use it.
ManyToMany¶
from typing import List
import edgy
class User(edgy.Model):
is_active: bool = edgy.BooleanField(default=True)
class Organisation(edgy.Model):
is_enabled: bool = edgy.BooleanField(default=True)
class MyModel(edgy.Model):
users: List[User] = edgy.ManyToMany(User)
organisations: List[Organisation] = edgy.ManyToMany("Organisation")
Tip
You can use edgy.ManyToManyField
as alternative to ManyToMany
instead.
Parameters¶
- to - A string model name or a class object of that same model.
- from_fields - Provide the related_fields for the implicitly generated ForeignKey to the owner model.
- to_fields - Provide the related_fields for the implicitly generated ForeignKey to the child model.
- related_name - The name to use for the relation from the related object back to this one.
- through - The model to be used for the relationship. Edgy generates the model by default if None is provided or through is an abstract model.
- embed_through - When traversing, embed the through object in this attribute. Otherwise it is not accessable from the result. if an empty string was provided, the old behaviour is used to query from the through model as base (default). if False, the base is transformed to the target and source model (full proxying). You cannot select the through model via path traversal anymore (except from the through model itself). If not an empty string, the same behaviour like with False applies except that you can select the through model fields via path traversal with the provided name.
!!! Note: If through is an abstract model it will be used as a template (a new model is generated with through as base).
!!! Note: The index parameter is passed through to the ForeignKey fields but is not required. The intern ForeignKey fields create with their primary key constraint and unique_together fallback their own index. You should be warned that the same for ForeignKey fields applies here for index, so you most probably don't want to use an index here.
IPAddressField¶
import edgy
class MyModel(edgy.Model):
ip_address: str = edgy.IPAddressField()
...
Derives from the same as CharField and validates the value of an IP. It currently
supports ipv4
and ipv6
.
JSONField¶
from typing import Dict, Any
import edgy
class MyModel(edgy.Model):
data: Dict[str, Any] = edgy.JSONField(default={})
...
Simple JSON representation object.
OneToOne¶
import edgy
class User(edgy.Model):
is_active: bool = edgy.BooleanField(default=True)
class MyModel(edgy.Model):
user: User = edgy.OneToOne("User")
...
Derives from the same as ForeignKey and applies a One to One direction.
Tip
You can use edgy.OneToOneField
as alternative to OneToOne
instead. Or if you want the basic ForeignKey with unique=True.
Note
The index parameter is here ignored.
TextField¶
import edgy
class MyModel(edgy.Model):
data: str = edgy.TextField(null=True, blank=True)
...
Similar to CharField but has no max_length
restrictions.
PasswordField¶
import edgy
class MyModel(edgy.Model):
data: str = edgy.PasswordField(null=False, max_length=255)
...
Similar to CharField and it can be used to represent a password text.
TimeField¶
import datetime
import edgy
def get_time():
return datetime.datetime.now().time()
class MyModel(edgy.Model):
time: datetime.time = edgy.TimeField(default=get_time)
...
URLField¶
import edgy
class MyModel(edgy.Model):
url: str = fields.URLField(null=True, max_length=1024)
...
Derives from the same as CharField and validates the value of an URL.
UUIDField¶
from uuid import UUID
import edgy
class MyModel(edgy.Model):
uuid: UUID = fields.UUIDField()
...
Derives from the same as CharField and validates the value of an UUID.
Custom Fields¶
Simple fields¶
If you merely want to customize an existing field in edgy.db.fields.core
you can just inherit from it and provide the customization via the FieldFactory
(or you can FieldFactory
for handling a new sqlalchemy type).
Valid methods to overwrite are __new__
, get_column_type
, get_pydatic_type
, get_constraints
and validate
For examples look in the mentioned path (replace dots with slashes).
Extended, special fields¶
If you want to customize the entire field (e.g. checks), you have to split the field in 2 parts:
- One inherits from
edgy.db.fields.base.BaseField
(or one of the derived classes) and provides the missing parts. It shall not be used for the Enduser (though possible). - One inherits from
edgy.db.fields.factories.FieldFactory
. Here the _bases attribute is adjusted to point to the Field from the first step.
Fields have to inherit from edgy.db.fields.base.BaseField
and to provide following methods to work:
- get_columns(self, field_name) - returns the sqlalchemy columns which should be created by this field.
- clean(self, field_name, value, to_query) - returns the cleaned column values. to_query specifies if clean is used by the query sanitizer and must be more strict (no partial values).
Additional they can provide following methods:
* __get__(self, instance, owner=None)
- Descriptor protocol like get access customization. Second parameter contains the class where the field was specified.
* __set__(self, instance, value)
- Descriptor protocol like set access customization. Dangerous to use. Better use to_model.
* to_model(self, field_name, phase="") - like clean, just for setting attributes or initializing a model. It is also used when setting attributes or in initialization (phase contains the phase where it is called). This way it is much more powerful than __set__
* get_embedded_fields(self, field_name, fields_mapping) - Define internal fields.
* get_default_values(self, field_name, cleaned_data) - returns the default values for the field. Can provide default values for embedded fields. If your field spans only one column you can also use the simplified get_default_value instead. This way you don't have to check for collisions. By default get_default_value is used internally.
* get_default_value(self) - return default value for one column fields.
* get_global_constraints(self, field_name, columns) - takes as second parameter (self excluded) the columns defined by this field (by get_columns). Returns a global constraint, which can be multi-column.
You should also provide an init method which sets following attributes:
- column_type - either None (default) or the sqlalchemy column type
Note: instance checks can also be done against the field_type
attribute in case you want to check the compatibility with other fields (composite style)
The annotation
parameter is for pydantic, the __type___
parameter is transformed into the field_type
attribute
for examples have a look in tests/fields/test_composite_fields.py
or in edgy/core/db/fields/core.py
Customizing fields¶
When a model was created it is safe to update the fields or fields_mapping as long as invalidate()
of meta is called.
It is auto-called when a new fields_mapping is assigned to meta but with the unfortune side-effect that additionally the fields attribute of the model must be set again.
You can for example set the inherit flag to False to disable inheriting a Field or set other field attributes.
You shouldn't remove fields (use ExcludeField for this) and be carefull when adding fields (maybe the model must be updated, this is no magic, have a look in the metaclasses file)