refactor methaclass functions into helper files, add docstrings
This commit is contained in:
@ -87,9 +87,9 @@ class BaseField(FieldInfo):
|
|||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
field_name not in ["default", "default_factory", "alias"]
|
field_name not in ["default", "default_factory", "alias"]
|
||||||
and not field_name.startswith("__")
|
and not field_name.startswith("__")
|
||||||
and hasattr(cls, field_name)
|
and hasattr(cls, field_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -180,7 +180,7 @@ class BaseField(FieldInfo):
|
|||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
return cls.default is not None or (
|
return cls.default is not None or (
|
||||||
cls.server_default is not None and use_server
|
cls.server_default is not None and use_server
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -199,9 +199,12 @@ class BaseField(FieldInfo):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def construct_contraints(cls) -> List:
|
def construct_contraints(cls) -> List:
|
||||||
return [sqlalchemy.schema.ForeignKey(
|
return [
|
||||||
con.name, ondelete=con.ondelete, onupdate=con.onupdate
|
sqlalchemy.schema.ForeignKey(
|
||||||
) for con in cls.constraints]
|
con.name, ondelete=con.ondelete, onupdate=con.onupdate
|
||||||
|
)
|
||||||
|
for con in cls.constraints
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_column(cls, name: str) -> sqlalchemy.Column:
|
def get_column(cls, name: str) -> sqlalchemy.Column:
|
||||||
@ -229,11 +232,11 @@ class BaseField(FieldInfo):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def expand_relationship(
|
def expand_relationship(
|
||||||
cls,
|
cls,
|
||||||
value: Any,
|
value: Any,
|
||||||
child: Union["Model", "NewBaseModel"],
|
child: Union["Model", "NewBaseModel"],
|
||||||
to_register: bool = True,
|
to_register: bool = True,
|
||||||
relation_name: str = None,
|
relation_name: str = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Function overwritten for relations, in basic field the value is returned as is.
|
Function overwritten for relations, in basic field the value is returned as is.
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import uuid
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
|
from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
from pydantic import BaseModel, create_model
|
from pydantic import BaseModel, create_model
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
|
|
||||||
@ -46,8 +45,8 @@ def create_dummy_instance(fk: Type["Model"], pk: Any = None) -> "Model":
|
|||||||
|
|
||||||
|
|
||||||
def create_dummy_model(
|
def create_dummy_model(
|
||||||
base_model: Type["Model"],
|
base_model: Type["Model"],
|
||||||
pk_field: Type[Union[BaseField, "ForeignKeyField", "ManyToManyField"]],
|
pk_field: Type[Union[BaseField, "ForeignKeyField", "ManyToManyField"]],
|
||||||
) -> Type["BaseModel"]:
|
) -> Type["BaseModel"]:
|
||||||
"""
|
"""
|
||||||
Used to construct a dummy pydantic model for type hints and pydantic validation.
|
Used to construct a dummy pydantic model for type hints and pydantic validation.
|
||||||
@ -84,16 +83,16 @@ class ForeignKeyConstraint:
|
|||||||
|
|
||||||
|
|
||||||
def ForeignKey( # noqa CFQ002
|
def ForeignKey( # noqa CFQ002
|
||||||
to: Type["Model"],
|
to: Type["Model"],
|
||||||
*,
|
*,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
unique: bool = False,
|
unique: bool = False,
|
||||||
nullable: bool = True,
|
nullable: bool = True,
|
||||||
related_name: str = None,
|
related_name: str = None,
|
||||||
virtual: bool = False,
|
virtual: bool = False,
|
||||||
onupdate: str = None,
|
onupdate: str = None,
|
||||||
ondelete: str = None,
|
ondelete: str = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Despite a name it's a function that returns constructed ForeignKeyField.
|
Despite a name it's a function that returns constructed ForeignKeyField.
|
||||||
@ -140,7 +139,9 @@ def ForeignKey( # noqa CFQ002
|
|||||||
name=kwargs.pop("real_name", None),
|
name=kwargs.pop("real_name", None),
|
||||||
nullable=nullable,
|
nullable=nullable,
|
||||||
constraints=[
|
constraints=[
|
||||||
ForeignKeyConstraint(name=fk_string, ondelete=ondelete, onupdate=onupdate)
|
ForeignKeyConstraint(
|
||||||
|
name=fk_string, ondelete=ondelete, onupdate=onupdate # type: ignore
|
||||||
|
)
|
||||||
],
|
],
|
||||||
unique=unique,
|
unique=unique,
|
||||||
column_type=to_field.column_type,
|
column_type=to_field.column_type,
|
||||||
@ -168,7 +169,7 @@ class ForeignKeyField(BaseField):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _extract_model_from_sequence(
|
def _extract_model_from_sequence(
|
||||||
cls, value: List, child: "Model", to_register: bool, relation_name: str
|
cls, value: List, child: "Model", to_register: bool, relation_name: str
|
||||||
) -> List["Model"]:
|
) -> List["Model"]:
|
||||||
"""
|
"""
|
||||||
Takes a list of Models and registers them on parent.
|
Takes a list of Models and registers them on parent.
|
||||||
@ -197,7 +198,7 @@ class ForeignKeyField(BaseField):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _register_existing_model(
|
def _register_existing_model(
|
||||||
cls, value: "Model", child: "Model", to_register: bool, relation_name: str
|
cls, value: "Model", child: "Model", to_register: bool, relation_name: str
|
||||||
) -> "Model":
|
) -> "Model":
|
||||||
"""
|
"""
|
||||||
Takes already created instance and registers it for parent.
|
Takes already created instance and registers it for parent.
|
||||||
@ -220,7 +221,7 @@ class ForeignKeyField(BaseField):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _construct_model_from_dict(
|
def _construct_model_from_dict(
|
||||||
cls, value: dict, child: "Model", to_register: bool, relation_name: str
|
cls, value: dict, child: "Model", to_register: bool, relation_name: str
|
||||||
) -> "Model":
|
) -> "Model":
|
||||||
"""
|
"""
|
||||||
Takes a dictionary, creates a instance and registers it for parent.
|
Takes a dictionary, creates a instance and registers it for parent.
|
||||||
@ -247,7 +248,7 @@ class ForeignKeyField(BaseField):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _construct_model_from_pk(
|
def _construct_model_from_pk(
|
||||||
cls, value: Any, child: "Model", to_register: bool, relation_name: str
|
cls, value: Any, child: "Model", to_register: bool, relation_name: str
|
||||||
) -> "Model":
|
) -> "Model":
|
||||||
"""
|
"""
|
||||||
Takes a pk value, creates a dummy instance and registers it for parent.
|
Takes a pk value, creates a dummy instance and registers it for parent.
|
||||||
@ -279,7 +280,7 @@ class ForeignKeyField(BaseField):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_relation(
|
def register_relation(
|
||||||
cls, model: "Model", child: "Model", relation_name: str
|
cls, model: "Model", child: "Model", relation_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Registers relation between parent and child in relation manager.
|
Registers relation between parent and child in relation manager.
|
||||||
@ -303,11 +304,11 @@ class ForeignKeyField(BaseField):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def expand_relationship(
|
def expand_relationship(
|
||||||
cls,
|
cls,
|
||||||
value: Any,
|
value: Any,
|
||||||
child: Union["Model", "NewBaseModel"],
|
child: Union["Model", "NewBaseModel"],
|
||||||
to_register: bool = True,
|
to_register: bool = True,
|
||||||
relation_name: str = None,
|
relation_name: str = None,
|
||||||
) -> Optional[Union["Model", List["Model"]]]:
|
) -> Optional[Union["Model", List["Model"]]]:
|
||||||
"""
|
"""
|
||||||
For relations the child model is first constructed (if needed),
|
For relations the child model is first constructed (if needed),
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from ormar.models.newbasemodel import NewBaseModel # noqa I100
|
from ormar.models.newbasemodel import NewBaseModel # noqa I100
|
||||||
from ormar.models.model import Model # noqa I100
|
from ormar.models.model import Model # noqa I100
|
||||||
from ormar.models.metaclass import expand_reverse_relationships # noqa I100
|
|
||||||
|
|
||||||
__all__ = ["NewBaseModel", "Model", "expand_reverse_relationships"]
|
__all__ = ["NewBaseModel", "Model"]
|
||||||
|
|||||||
0
ormar/models/helpers/__init__.py
Normal file
0
ormar/models/helpers/__init__.py
Normal file
39
ormar/models/helpers/models.py
Normal file
39
ormar/models/helpers/models.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from typing import Dict, List, Optional, TYPE_CHECKING, Type
|
||||||
|
|
||||||
|
from ormar import ModelDefinitionError
|
||||||
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma no cover
|
||||||
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
|
def validate_related_names_in_relations(
|
||||||
|
model_fields: Dict, new_model: Type["Model"]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Performs a validation of relation_names in relation fields.
|
||||||
|
If multiple fields are leading to the same related model
|
||||||
|
only one can have empty related_name param
|
||||||
|
(populated by default as model.name.lower()+'s').
|
||||||
|
Also related_names have to be unique for given related model.
|
||||||
|
|
||||||
|
:raises: ModelDefinitionError if validation of related_names fail
|
||||||
|
:param model_fields: dictionary of declared ormar model fields
|
||||||
|
:type model_fields: Dict[str, ormar.Field]
|
||||||
|
:param new_model:
|
||||||
|
:type new_model: Model class
|
||||||
|
"""
|
||||||
|
already_registered: Dict[str, List[Optional[str]]] = dict()
|
||||||
|
for field in model_fields.values():
|
||||||
|
if issubclass(field, ForeignKeyField):
|
||||||
|
previous_related_names = already_registered.setdefault(field.to, [])
|
||||||
|
if field.related_name in previous_related_names:
|
||||||
|
raise ModelDefinitionError(
|
||||||
|
f"Multiple fields declared on {new_model.get_name(lower=False)} "
|
||||||
|
f"model leading to {field.to.get_name(lower=False)} model without "
|
||||||
|
f"related_name property set. \nThere can be only one relation with "
|
||||||
|
f"default/empty name: '{new_model.get_name() + 's'}'"
|
||||||
|
f"\nTip: provide different related_name for FK and/or M2M fields"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
previous_related_names.append(field.related_name)
|
||||||
221
ormar/models/helpers/pydantic.py
Normal file
221
ormar/models/helpers/pydantic.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import warnings
|
||||||
|
from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
|
from pydantic import BaseConfig
|
||||||
|
from pydantic.fields import ModelField
|
||||||
|
from pydantic.utils import lenient_issubclass
|
||||||
|
|
||||||
|
import ormar # noqa: I100, I202
|
||||||
|
from ormar.fields import BaseField, ManyToManyField
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma no cover
|
||||||
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_field_not_already_registered(
|
||||||
|
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if child is already registered in parents pydantic fields.
|
||||||
|
|
||||||
|
:param child: related Model class
|
||||||
|
:type child: ormar.models.metaclass.ModelMetaclass
|
||||||
|
:param child_model_name: related_name of the child if provided
|
||||||
|
:type child_model_name: str
|
||||||
|
:param parent_model: parent Model class
|
||||||
|
:type parent_model: ormar.models.metaclass.ModelMetaclass
|
||||||
|
:return: result of the check
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
child_model_name not in parent_model.__fields__
|
||||||
|
and child.get_name() not in parent_model.__fields__
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_pydantic_field(
|
||||||
|
field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Registers pydantic field on through model that leads to passed model
|
||||||
|
and is registered as field_name passed.
|
||||||
|
|
||||||
|
Through model is fetched from through attributed on passed model_field.
|
||||||
|
|
||||||
|
:param field_name: field name to register
|
||||||
|
:type field_name: str
|
||||||
|
:param model: type of field to register
|
||||||
|
:type model: Model class
|
||||||
|
:param model_field: relation field from which through model is extracted
|
||||||
|
:type model_field: ManyToManyField class
|
||||||
|
"""
|
||||||
|
model_field.through.__fields__[field_name] = ModelField(
|
||||||
|
name=field_name,
|
||||||
|
type_=model,
|
||||||
|
model_config=model.__config__,
|
||||||
|
required=False,
|
||||||
|
class_validators={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField":
|
||||||
|
"""
|
||||||
|
Extracts field type and if it's required from Model model_fields by passed
|
||||||
|
field_name. Returns a pydantic field with type of field_name field type.
|
||||||
|
|
||||||
|
:param field_name: field name to fetch from Model and name of pydantic field
|
||||||
|
:type field_name: str
|
||||||
|
:param model: type of field to register
|
||||||
|
:type model: Model class
|
||||||
|
:return: newly created pydantic field
|
||||||
|
:rtype: pydantic.ModelField
|
||||||
|
"""
|
||||||
|
return ModelField(
|
||||||
|
name=field_name,
|
||||||
|
type_=model.Meta.model_fields[field_name].__type__, # type: ignore
|
||||||
|
model_config=model.__config__,
|
||||||
|
required=not model.Meta.model_fields[field_name].nullable,
|
||||||
|
class_validators={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def populate_default_pydantic_field_value(
|
||||||
|
ormar_field: Type[BaseField], field_name: str, attrs: dict
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Grabs current value of the ormar Field in class namespace
|
||||||
|
(so the default_value declared on ormar model if set)
|
||||||
|
and converts it to pydantic.FieldInfo
|
||||||
|
that pydantic is able to extract later.
|
||||||
|
|
||||||
|
On FieldInfo there are saved all needed params like max_length of the string
|
||||||
|
and other constraints that pydantic can use to build
|
||||||
|
it's own field validation used by ormar.
|
||||||
|
|
||||||
|
:param ormar_field: field to convert
|
||||||
|
:type ormar_field: ormar Field
|
||||||
|
:param field_name: field to convert name
|
||||||
|
:type field_name: str
|
||||||
|
:param attrs: current class namespace
|
||||||
|
:type attrs: Dict
|
||||||
|
:return: updated namespace dict
|
||||||
|
:rtype: Dict
|
||||||
|
"""
|
||||||
|
curr_def_value = attrs.get(field_name, ormar.Undefined)
|
||||||
|
if lenient_issubclass(curr_def_value, ormar.fields.BaseField):
|
||||||
|
curr_def_value = ormar.Undefined
|
||||||
|
if curr_def_value is None:
|
||||||
|
attrs[field_name] = ormar_field.convert_to_pydantic_field_info(allow_null=True)
|
||||||
|
else:
|
||||||
|
attrs[field_name] = ormar_field.convert_to_pydantic_field_info()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:
|
||||||
|
"""
|
||||||
|
Extracts ormar fields from annotations (deprecated) and from namespace
|
||||||
|
dictionary of the class. Fields declared on model are all subclasses of the
|
||||||
|
BaseField class.
|
||||||
|
|
||||||
|
Trigger conversion of ormar field into pydantic FieldInfo, which has all needed
|
||||||
|
paramaters saved.
|
||||||
|
|
||||||
|
Overwrites the annotations of ormar fields to corresponding types declared on
|
||||||
|
ormar fields (constructed dynamically for relations).
|
||||||
|
Those annotations are later used by pydantic to construct it's own fields.
|
||||||
|
|
||||||
|
:param attrs: current class namespace
|
||||||
|
:type attrs: Dict
|
||||||
|
:return: namespace of the class updated, dict of extracted model_fields
|
||||||
|
:rtype: Tuple[Dict, Dict]
|
||||||
|
"""
|
||||||
|
model_fields = {}
|
||||||
|
potential_fields = {
|
||||||
|
k: v
|
||||||
|
for k, v in attrs["__annotations__"].items()
|
||||||
|
if lenient_issubclass(v, BaseField)
|
||||||
|
}
|
||||||
|
if potential_fields:
|
||||||
|
warnings.warn(
|
||||||
|
"Using ormar.Fields as type Model annotation has been deprecated,"
|
||||||
|
" check documentation of current version",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
potential_fields.update(get_potential_fields(attrs))
|
||||||
|
for field_name, field in potential_fields.items():
|
||||||
|
field.name = field_name
|
||||||
|
attrs = populate_default_pydantic_field_value(field, field_name, attrs)
|
||||||
|
model_fields[field_name] = field
|
||||||
|
attrs["__annotations__"][field_name] = (
|
||||||
|
field.__type__ if not field.nullable else Optional[field.__type__]
|
||||||
|
)
|
||||||
|
return attrs, model_fields
|
||||||
|
|
||||||
|
|
||||||
|
def get_pydantic_base_orm_config() -> Type[BaseConfig]:
|
||||||
|
"""
|
||||||
|
Returns empty pydantic Config with orm_mode set to True.
|
||||||
|
|
||||||
|
:return: empty default config with orm_mode set.
|
||||||
|
:rtype: pydantic Config
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Config(BaseConfig):
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
return Config
|
||||||
|
|
||||||
|
|
||||||
|
def populate_default_options_values(
|
||||||
|
new_model: Type["Model"], model_fields: Dict
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Sets all optional Meta values to it's defaults
|
||||||
|
and set model_fields that were already previously extracted.
|
||||||
|
|
||||||
|
Here should live all options that are not overwritten/set for all models.
|
||||||
|
|
||||||
|
Current options are:
|
||||||
|
* constraints = []
|
||||||
|
* abstract = False
|
||||||
|
|
||||||
|
:param new_model: newly constructed Model
|
||||||
|
:type new_model: Model class
|
||||||
|
:param model_fields:
|
||||||
|
:type model_fields: Union[Dict[str, type], Dict]
|
||||||
|
"""
|
||||||
|
if not hasattr(new_model.Meta, "constraints"):
|
||||||
|
new_model.Meta.constraints = []
|
||||||
|
if not hasattr(new_model.Meta, "model_fields"):
|
||||||
|
new_model.Meta.model_fields = model_fields
|
||||||
|
if not hasattr(new_model.Meta, "abstract"):
|
||||||
|
new_model.Meta.abstract = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_potential_fields(attrs: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Gets all the fields in current class namespace that are Fields.
|
||||||
|
|
||||||
|
:param attrs: current class namespace
|
||||||
|
:type attrs: Dict
|
||||||
|
:return: extracted fields that are ormar Fields
|
||||||
|
:rtype: Dict
|
||||||
|
"""
|
||||||
|
return {k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_annotations_and_default_vals(attrs: Dict) -> Tuple[Dict, Dict]:
|
||||||
|
"""
|
||||||
|
Extracts annotations from class namespace dict and triggers
|
||||||
|
extraction of ormar model_fields.
|
||||||
|
|
||||||
|
:param attrs: namespace of the class created
|
||||||
|
:type attrs: Dict
|
||||||
|
:return: namespace of the class updated, dict of extracted model_fields
|
||||||
|
:rtype: Tuple[Dict, Dict]
|
||||||
|
"""
|
||||||
|
key = "__annotations__"
|
||||||
|
attrs[key] = attrs.get(key, {})
|
||||||
|
attrs, model_fields = populate_pydantic_default_values(attrs)
|
||||||
|
return attrs, model_fields
|
||||||
138
ormar/models/helpers/relations.py
Normal file
138
ormar/models/helpers/relations.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
|
from ormar import ForeignKey, ManyToMany
|
||||||
|
from ormar.fields import ManyToManyField
|
||||||
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
|
from ormar.models.helpers.pydantic import reverse_field_not_already_registered
|
||||||
|
from ormar.models.helpers.sqlalchemy import adjust_through_many_to_many_model
|
||||||
|
from ormar.relations import AliasManager
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma no cover
|
||||||
|
from ormar import Model
|
||||||
|
|
||||||
|
alias_manager = AliasManager()
|
||||||
|
|
||||||
|
|
||||||
|
def register_relation_on_build(new_model: Type["Model"], field_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Registers ForeignKey relation in alias_manager to set a table_prefix.
|
||||||
|
Registration include also reverse relation side to be able to join both sides.
|
||||||
|
|
||||||
|
Relation is registered by model name and relation field name to allow for multiple
|
||||||
|
relations between two Models that needs to have different
|
||||||
|
aliases for proper sql joins.
|
||||||
|
|
||||||
|
:param new_model: constructed model
|
||||||
|
:type new_model: Model class
|
||||||
|
:param field_name: name of the related field
|
||||||
|
:type field_name: str
|
||||||
|
"""
|
||||||
|
alias_manager.add_relation_type(new_model, field_name)
|
||||||
|
|
||||||
|
|
||||||
|
def register_many_to_many_relation_on_build(
|
||||||
|
new_model: Type["Model"], field: Type[ManyToManyField]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Registers connection between through model and both sides of the m2m relation.
|
||||||
|
Registration include also reverse relation side to be able to join both sides.
|
||||||
|
|
||||||
|
Relation is registered by model name and relation field name to allow for multiple
|
||||||
|
relations between two Models that needs to have different
|
||||||
|
aliases for proper sql joins.
|
||||||
|
|
||||||
|
By default relation name is a model.name.lower().
|
||||||
|
|
||||||
|
:param new_model: model on which m2m field is declared
|
||||||
|
:type new_model: Model class
|
||||||
|
:param field: relation field
|
||||||
|
:type field: ManyToManyField class
|
||||||
|
"""
|
||||||
|
alias_manager.add_relation_type(field.through, new_model.get_name(), is_multi=True)
|
||||||
|
alias_manager.add_relation_type(field.through, field.to.get_name(), is_multi=True)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_reverse_relationships(model: Type["Model"]) -> None:
|
||||||
|
"""
|
||||||
|
Iterates through model_fields of given model and verifies if all reverse
|
||||||
|
relation have been populated on related models.
|
||||||
|
|
||||||
|
If the reverse relation has not been set before it's set here.
|
||||||
|
|
||||||
|
:param model: model on which relation should be checked and registered
|
||||||
|
:type model: Model class
|
||||||
|
"""
|
||||||
|
for model_field in model.Meta.model_fields.values():
|
||||||
|
if issubclass(model_field, ForeignKeyField):
|
||||||
|
child_model_name = model_field.related_name or model.get_name() + "s"
|
||||||
|
parent_model = model_field.to
|
||||||
|
child = model
|
||||||
|
if reverse_field_not_already_registered(
|
||||||
|
child, child_model_name, parent_model
|
||||||
|
):
|
||||||
|
register_reverse_model_fields(
|
||||||
|
parent_model, child, child_model_name, model_field
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_reverse_model_fields(
|
||||||
|
model: Type["Model"],
|
||||||
|
child: Type["Model"],
|
||||||
|
related_name: str,
|
||||||
|
model_field: Type["ForeignKeyField"],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Registers reverse ForeignKey field on related model.
|
||||||
|
By default it's name.lower()+'s' of the model on which relation is defined.
|
||||||
|
|
||||||
|
But if the related_model name is provided it's registered with that name.
|
||||||
|
Autogenerated reverse fields also set related_name to the original field name.
|
||||||
|
|
||||||
|
:param model: related model on which reverse field should be defined
|
||||||
|
:type model: Model class
|
||||||
|
:param child: parent model with relation definition
|
||||||
|
:type child: Model class
|
||||||
|
:param related_name: name by which reverse key should be registered
|
||||||
|
:type related_name: str
|
||||||
|
:param model_field: original relation ForeignKey field
|
||||||
|
:type model_field: relation Field
|
||||||
|
"""
|
||||||
|
if issubclass(model_field, ManyToManyField):
|
||||||
|
model.Meta.model_fields[related_name] = ManyToMany(
|
||||||
|
child,
|
||||||
|
through=model_field.through,
|
||||||
|
name=related_name,
|
||||||
|
virtual=True,
|
||||||
|
related_name=model_field.name,
|
||||||
|
)
|
||||||
|
# register foreign keys on through model
|
||||||
|
adjust_through_many_to_many_model(model, child, model_field)
|
||||||
|
else:
|
||||||
|
model.Meta.model_fields[related_name] = ForeignKey(
|
||||||
|
child, real_name=related_name, virtual=True, related_name=model_field.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_relation_in_alias_manager(
|
||||||
|
new_model: Type["Model"], field: Type[ForeignKeyField], field_name: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Registers the relation (and reverse relation) in alias manager.
|
||||||
|
The m2m relations require registration of through model between
|
||||||
|
actual end models of the relation.
|
||||||
|
|
||||||
|
Delegates the actual registration to:
|
||||||
|
m2m - register_many_to_many_relation_on_build
|
||||||
|
fk - register_relation_on_build
|
||||||
|
|
||||||
|
:param new_model: model on which relation field is declared
|
||||||
|
:type new_model: Model class
|
||||||
|
:param field: relation field
|
||||||
|
:type field: ForeignKey or ManyToManyField class
|
||||||
|
:param field_name: name of the relation key
|
||||||
|
:type field_name: str
|
||||||
|
"""
|
||||||
|
if issubclass(field, ManyToManyField):
|
||||||
|
register_many_to_many_relation_on_build(new_model=new_model, field=field)
|
||||||
|
elif issubclass(field, ForeignKeyField):
|
||||||
|
register_relation_on_build(new_model=new_model, field_name=field_name)
|
||||||
208
ormar/models/helpers/sqlalchemy.py
Normal file
208
ormar/models/helpers/sqlalchemy.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from ormar import ForeignKey, Integer, ModelDefinitionError # noqa: I202
|
||||||
|
from ormar.fields import BaseField, ManyToManyField
|
||||||
|
from ormar.models.helpers.models import validate_related_names_in_relations
|
||||||
|
from ormar.models.helpers.pydantic import create_pydantic_field
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma no cover
|
||||||
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
|
def adjust_through_many_to_many_model(
|
||||||
|
model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Registers m2m relation on through model.
|
||||||
|
Sets ormar.ForeignKey from through model to both child and parent models.
|
||||||
|
Sets sqlalchemy.ForeignKey to both child and parent models.
|
||||||
|
Sets pydantic fields with child and parent model types.
|
||||||
|
|
||||||
|
:param model: model on which relation is declared
|
||||||
|
:type model: Model class
|
||||||
|
:param child: model to which m2m relation leads
|
||||||
|
:type child: Model class
|
||||||
|
:param model_field: relation field defined in parent model
|
||||||
|
:type model_field: ManyToManyField
|
||||||
|
"""
|
||||||
|
model_field.through.Meta.model_fields[model.get_name()] = ForeignKey(
|
||||||
|
model, real_name=model.get_name(), ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
model_field.through.Meta.model_fields[child.get_name()] = ForeignKey(
|
||||||
|
child, real_name=child.get_name(), ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
create_and_append_m2m_fk(model, model_field)
|
||||||
|
create_and_append_m2m_fk(child, model_field)
|
||||||
|
|
||||||
|
create_pydantic_field(model.get_name(), model, model_field)
|
||||||
|
create_pydantic_field(child.get_name(), child, model_field)
|
||||||
|
|
||||||
|
|
||||||
|
def create_and_append_m2m_fk(
|
||||||
|
model: Type["Model"], model_field: Type[ManyToManyField]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Registers sqlalchemy Column with sqlalchemy.ForeignKey leadning to the model.
|
||||||
|
|
||||||
|
Newly created field is added to m2m relation through model Meta columns and table.
|
||||||
|
|
||||||
|
:param model: Model class to which FK should be created
|
||||||
|
:type model: Model class
|
||||||
|
:param model_field: field with ManyToMany relation
|
||||||
|
:type model_field: ManyToManyField field
|
||||||
|
"""
|
||||||
|
column = sqlalchemy.Column(
|
||||||
|
model.get_name(),
|
||||||
|
model.Meta.table.columns.get(model.get_column_alias(model.Meta.pkname)).type,
|
||||||
|
sqlalchemy.schema.ForeignKey(
|
||||||
|
model.Meta.tablename + "." + model.get_column_alias(model.Meta.pkname),
|
||||||
|
ondelete="CASCADE",
|
||||||
|
onupdate="CASCADE",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
model_field.through.Meta.columns.append(column)
|
||||||
|
model_field.through.Meta.table.append_column(column)
|
||||||
|
|
||||||
|
|
||||||
|
def check_pk_column_validity(
|
||||||
|
field_name: str, field: BaseField, pkname: Optional[str]
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Receives the field marked as primary key and verifies if the pkname
|
||||||
|
was not already set (only one allowed per model) and if field is not marked
|
||||||
|
as pydantic_only as it needs to be a database field.
|
||||||
|
|
||||||
|
:raises: ModelDefintionError if pkname already set or field is pydantic_only
|
||||||
|
:param field_name: name of field
|
||||||
|
:type field_name: str
|
||||||
|
:param field: ormar.Field
|
||||||
|
:type field: BaseField
|
||||||
|
:param pkname: already set pkname
|
||||||
|
:type pkname: Optional[str]
|
||||||
|
:return: name of the field that should be set as pkname
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
if pkname is not None:
|
||||||
|
raise ModelDefinitionError("Only one primary key column is allowed.")
|
||||||
|
if field.pydantic_only:
|
||||||
|
raise ModelDefinitionError("Primary key column cannot be pydantic only")
|
||||||
|
return field_name
|
||||||
|
|
||||||
|
|
||||||
|
def sqlalchemy_columns_from_model_fields(
|
||||||
|
model_fields: Dict, new_model: Type["Model"]
|
||||||
|
) -> Tuple[Optional[str], List[sqlalchemy.Column]]:
|
||||||
|
"""
|
||||||
|
Iterates over declared on Model model fields and extracts fields that
|
||||||
|
should be treated as database fields.
|
||||||
|
|
||||||
|
If the model is empty it sets mandatory id field as primary key
|
||||||
|
(used in through models in m2m relations).
|
||||||
|
|
||||||
|
Triggers a validation of relation_names in relation fields. If multiple fields
|
||||||
|
are leading to the same related model only one can have empty related_name param.
|
||||||
|
Also related_names have to be unique.
|
||||||
|
|
||||||
|
Trigger validation of primary_key - only one and required pk can be set,
|
||||||
|
cannot be pydantic_only.
|
||||||
|
|
||||||
|
Append fields to columns if it's not pydantic_only,
|
||||||
|
virtual ForeignKey or ManyToMany field.
|
||||||
|
|
||||||
|
:raises: ModelDefinitionError if validation of related_names fail,
|
||||||
|
or pkname validation fails.
|
||||||
|
:param model_fields: dictionary of declared ormar model fields
|
||||||
|
:type model_fields: Dict[str, ormar.Field]
|
||||||
|
:param new_model:
|
||||||
|
:type new_model: Model class
|
||||||
|
:return: pkname, list of sqlalchemy columns
|
||||||
|
:rtype: Tuple[Optional[str], List[sqlalchemy.Column]]
|
||||||
|
"""
|
||||||
|
columns = []
|
||||||
|
pkname = None
|
||||||
|
if len(model_fields.keys()) == 0:
|
||||||
|
model_fields["id"] = Integer(name="id", primary_key=True)
|
||||||
|
logging.warning(
|
||||||
|
"Table {table_name} had no fields so auto "
|
||||||
|
"Integer primary key named `id` created."
|
||||||
|
)
|
||||||
|
validate_related_names_in_relations(model_fields, new_model)
|
||||||
|
for field_name, field in model_fields.items():
|
||||||
|
if field.primary_key:
|
||||||
|
pkname = check_pk_column_validity(field_name, field, pkname)
|
||||||
|
if (
|
||||||
|
not field.pydantic_only
|
||||||
|
and not field.virtual
|
||||||
|
and not issubclass(field, ManyToManyField)
|
||||||
|
):
|
||||||
|
columns.append(field.get_column(field.get_alias()))
|
||||||
|
return pkname, columns
|
||||||
|
|
||||||
|
|
||||||
|
def populate_meta_tablename_columns_and_pk(
|
||||||
|
name: str, new_model: Type["Model"]
|
||||||
|
) -> Type["Model"]:
|
||||||
|
"""
|
||||||
|
Sets Model tablename if it's not already set in Meta.
|
||||||
|
Default tablename if not present is class name lower + s (i.e. Bed becomes -> beds)
|
||||||
|
|
||||||
|
Checks if Model's Meta have pkname and columns set.
|
||||||
|
If not calls the sqlalchemy_columns_from_model_fields to populate
|
||||||
|
columns from ormar.fields definitions.
|
||||||
|
|
||||||
|
:raises: if pkname is not present raises ModelDefinitionError.
|
||||||
|
Each model has to have pk.
|
||||||
|
|
||||||
|
:param name: name of the current Model
|
||||||
|
:type name: str
|
||||||
|
:param new_model: currently constructed Model
|
||||||
|
:type new_model: ormar.models.metaclass.ModelMetaclass
|
||||||
|
:return: Model with populated pkname and columns in Meta
|
||||||
|
:rtype: ormar.models.metaclass.ModelMetaclass
|
||||||
|
"""
|
||||||
|
tablename = name.lower() + "s"
|
||||||
|
new_model.Meta.tablename = (
|
||||||
|
new_model.Meta.tablename if hasattr(new_model.Meta, "tablename") else tablename
|
||||||
|
)
|
||||||
|
pkname: Optional[str]
|
||||||
|
|
||||||
|
if hasattr(new_model.Meta, "columns"):
|
||||||
|
columns = new_model.Meta.table.columns
|
||||||
|
pkname = new_model.Meta.pkname
|
||||||
|
else:
|
||||||
|
pkname, columns = sqlalchemy_columns_from_model_fields(
|
||||||
|
new_model.Meta.model_fields, new_model
|
||||||
|
)
|
||||||
|
|
||||||
|
if pkname is None:
|
||||||
|
raise ModelDefinitionError("Table has to have a primary key.")
|
||||||
|
|
||||||
|
new_model.Meta.columns = columns
|
||||||
|
new_model.Meta.pkname = pkname
|
||||||
|
return new_model
|
||||||
|
|
||||||
|
|
||||||
|
def populate_meta_sqlalchemy_table_if_required(
|
||||||
|
new_model: Type["Model"],
|
||||||
|
) -> Type["Model"]:
|
||||||
|
"""
|
||||||
|
Constructs sqlalchemy table out of columns and parameters set on Meta class.
|
||||||
|
It populates name, metadata, columns and constraints.
|
||||||
|
|
||||||
|
:param new_model: class without sqlalchemy table constructed
|
||||||
|
:type new_model: Model class
|
||||||
|
:return: class with populated Meta.table
|
||||||
|
:rtype: Model class
|
||||||
|
"""
|
||||||
|
if not hasattr(new_model.Meta, "table"):
|
||||||
|
new_model.Meta.table = sqlalchemy.Table(
|
||||||
|
new_model.Meta.tablename,
|
||||||
|
new_model.Meta.metadata,
|
||||||
|
*new_model.Meta.columns,
|
||||||
|
*new_model.Meta.constraints,
|
||||||
|
)
|
||||||
|
return new_model
|
||||||
@ -1,6 +1,3 @@
|
|||||||
import copy
|
|
||||||
import logging
|
|
||||||
import warnings
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Dict,
|
Dict,
|
||||||
@ -11,21 +8,36 @@ from typing import (
|
|||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
import databases
|
import databases
|
||||||
import pydantic
|
import pydantic
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from pydantic import BaseConfig
|
from pydantic.fields import FieldInfo
|
||||||
from pydantic.fields import FieldInfo, ModelField
|
|
||||||
from pydantic.utils import lenient_issubclass
|
|
||||||
from sqlalchemy.sql.schema import ColumnCollectionConstraint
|
from sqlalchemy.sql.schema import ColumnCollectionConstraint
|
||||||
|
|
||||||
import ormar # noqa I100
|
import ormar # noqa I100
|
||||||
from ormar import ForeignKey, Integer, ModelDefinitionError # noqa I100
|
from ormar import ForeignKey, Integer, ModelDefinitionError # noqa I100
|
||||||
from ormar.fields import BaseField
|
from ormar.fields import BaseField
|
||||||
from ormar.fields.foreign_key import ForeignKeyField
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
from ormar.fields.many_to_many import ManyToMany, ManyToManyField
|
from ormar.fields.many_to_many import ManyToManyField
|
||||||
|
from ormar.models.helpers.pydantic import (
|
||||||
|
extract_annotations_and_default_vals,
|
||||||
|
get_potential_fields,
|
||||||
|
get_pydantic_base_orm_config,
|
||||||
|
get_pydantic_field,
|
||||||
|
populate_default_options_values,
|
||||||
|
)
|
||||||
|
from ormar.models.helpers.relations import (
|
||||||
|
alias_manager,
|
||||||
|
register_relation_in_alias_manager,
|
||||||
|
)
|
||||||
|
from ormar.models.helpers.relations import expand_reverse_relationships
|
||||||
|
from ormar.models.helpers.sqlalchemy import (
|
||||||
|
populate_meta_sqlalchemy_table_if_required,
|
||||||
|
populate_meta_tablename_columns_and_pk,
|
||||||
|
)
|
||||||
from ormar.models.quick_access_views import quick_access_set
|
from ormar.models.quick_access_views import quick_access_set
|
||||||
from ormar.queryset import QuerySet
|
from ormar.queryset import QuerySet
|
||||||
from ormar.relations.alias_manager import AliasManager
|
from ormar.relations.alias_manager import AliasManager
|
||||||
@ -34,7 +46,6 @@ from ormar.signals import Signal, SignalEmitter
|
|||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
from ormar import Model
|
from ormar import Model
|
||||||
|
|
||||||
alias_manager = AliasManager()
|
|
||||||
PARSED_FIELDS_KEY = "__parsed_fields__"
|
PARSED_FIELDS_KEY = "__parsed_fields__"
|
||||||
CONFIG_KEY = "Config"
|
CONFIG_KEY = "Config"
|
||||||
|
|
||||||
@ -56,295 +67,6 @@ class ModelMeta:
|
|||||||
abstract: bool
|
abstract: bool
|
||||||
|
|
||||||
|
|
||||||
def register_relation_on_build_new(new_model: Type["Model"], field_name: str) -> None:
|
|
||||||
alias_manager.add_relation_type_new(new_model, field_name)
|
|
||||||
|
|
||||||
|
|
||||||
def register_many_to_many_relation_on_build_new(
|
|
||||||
new_model: Type["Model"], field: Type[ManyToManyField]
|
|
||||||
) -> None:
|
|
||||||
alias_manager.add_relation_type_new(
|
|
||||||
field.through, new_model.get_name(), is_multi=True
|
|
||||||
)
|
|
||||||
alias_manager.add_relation_type_new(
|
|
||||||
field.through, field.to.get_name(), is_multi=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_field_not_already_registered(
|
|
||||||
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
|
|
||||||
) -> bool:
|
|
||||||
return (
|
|
||||||
child_model_name not in parent_model.__fields__
|
|
||||||
and child.get_name() not in parent_model.__fields__
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def expand_reverse_relationships(model: Type["Model"]) -> None:
|
|
||||||
for model_field in model.Meta.model_fields.values():
|
|
||||||
if issubclass(model_field, ForeignKeyField):
|
|
||||||
child_model_name = model_field.related_name or model.get_name() + "s"
|
|
||||||
parent_model = model_field.to
|
|
||||||
child = model
|
|
||||||
if reverse_field_not_already_registered(
|
|
||||||
child, child_model_name, parent_model
|
|
||||||
):
|
|
||||||
register_reverse_model_fields(
|
|
||||||
parent_model, child, child_model_name, model_field
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register_reverse_model_fields(
|
|
||||||
model: Type["Model"],
|
|
||||||
child: Type["Model"],
|
|
||||||
child_model_name: str,
|
|
||||||
model_field: Type["ForeignKeyField"],
|
|
||||||
) -> None:
|
|
||||||
if issubclass(model_field, ManyToManyField):
|
|
||||||
model.Meta.model_fields[child_model_name] = ManyToMany(
|
|
||||||
child,
|
|
||||||
through=model_field.through,
|
|
||||||
name=child_model_name,
|
|
||||||
virtual=True,
|
|
||||||
related_name=model_field.name,
|
|
||||||
)
|
|
||||||
# register foreign keys on through model
|
|
||||||
adjust_through_many_to_many_model(model, child, model_field, child_model_name)
|
|
||||||
else:
|
|
||||||
model.Meta.model_fields[child_model_name] = ForeignKey(
|
|
||||||
child,
|
|
||||||
real_name=child_model_name,
|
|
||||||
virtual=True,
|
|
||||||
related_name=model_field.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def adjust_through_many_to_many_model(
|
|
||||||
model: Type["Model"],
|
|
||||||
child: Type["Model"],
|
|
||||||
model_field: Type[ManyToManyField],
|
|
||||||
child_model_name: str,
|
|
||||||
) -> None:
|
|
||||||
model_field.through.Meta.model_fields[model.get_name()] = ForeignKey(
|
|
||||||
model, real_name=model.get_name(), ondelete="CASCADE"
|
|
||||||
)
|
|
||||||
model_field.through.Meta.model_fields[child.get_name()] = ForeignKey(
|
|
||||||
child, real_name=child.get_name(), ondelete="CASCADE"
|
|
||||||
)
|
|
||||||
|
|
||||||
create_and_append_m2m_fk(model, model_field)
|
|
||||||
create_and_append_m2m_fk(child, model_field)
|
|
||||||
|
|
||||||
create_pydantic_field(model.get_name(), model, model_field)
|
|
||||||
create_pydantic_field(child.get_name(), child, model_field)
|
|
||||||
|
|
||||||
|
|
||||||
def create_pydantic_field(
|
|
||||||
field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]
|
|
||||||
) -> None:
|
|
||||||
model_field.through.__fields__[field_name] = ModelField(
|
|
||||||
name=field_name,
|
|
||||||
type_=model,
|
|
||||||
model_config=model.__config__,
|
|
||||||
required=False,
|
|
||||||
class_validators={},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField":
|
|
||||||
return ModelField(
|
|
||||||
name=field_name,
|
|
||||||
type_=model.Meta.model_fields[field_name].__type__, # type: ignore
|
|
||||||
model_config=model.__config__,
|
|
||||||
required=not model.Meta.model_fields[field_name].nullable,
|
|
||||||
class_validators={},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_and_append_m2m_fk(
|
|
||||||
model: Type["Model"], model_field: Type[ManyToManyField]
|
|
||||||
) -> None:
|
|
||||||
column = sqlalchemy.Column(
|
|
||||||
model.get_name(),
|
|
||||||
model.Meta.table.columns.get(model.get_column_alias(model.Meta.pkname)).type,
|
|
||||||
sqlalchemy.schema.ForeignKey(
|
|
||||||
model.Meta.tablename + "." + model.get_column_alias(model.Meta.pkname),
|
|
||||||
ondelete="CASCADE",
|
|
||||||
onupdate="CASCADE",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
model_field.through.Meta.columns.append(column)
|
|
||||||
model_field.through.Meta.table.append_column(column)
|
|
||||||
|
|
||||||
|
|
||||||
def check_pk_column_validity(
|
|
||||||
field_name: str, field: BaseField, pkname: Optional[str]
|
|
||||||
) -> Optional[str]:
|
|
||||||
if pkname is not None:
|
|
||||||
raise ModelDefinitionError("Only one primary key column is allowed.")
|
|
||||||
if field.pydantic_only:
|
|
||||||
raise ModelDefinitionError("Primary key column cannot be pydantic only")
|
|
||||||
return field_name
|
|
||||||
|
|
||||||
|
|
||||||
def validate_related_names_in_relations(
|
|
||||||
model_fields: Dict, new_model: Type["Model"]
|
|
||||||
) -> None:
|
|
||||||
already_registered: Dict[str, List[Optional[str]]] = dict()
|
|
||||||
for field in model_fields.values():
|
|
||||||
if issubclass(field, ForeignKeyField):
|
|
||||||
previous_related_names = already_registered.setdefault(field.to, [])
|
|
||||||
if field.related_name in previous_related_names:
|
|
||||||
raise ModelDefinitionError(
|
|
||||||
f"Multiple fields declared on {new_model.get_name(lower=False)} "
|
|
||||||
f"model leading to {field.to.get_name(lower=False)} model without "
|
|
||||||
f"related_name property set. \nThere can be only one relation with "
|
|
||||||
f"default/empty name: '{new_model.get_name() + 's'}'"
|
|
||||||
f"\nTip: provide different related_name for FK and/or M2M fields"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
previous_related_names.append(field.related_name)
|
|
||||||
|
|
||||||
|
|
||||||
def sqlalchemy_columns_from_model_fields(
|
|
||||||
model_fields: Dict, new_model: Type["Model"]
|
|
||||||
) -> Tuple[Optional[str], List[sqlalchemy.Column]]:
|
|
||||||
columns = []
|
|
||||||
pkname = None
|
|
||||||
if len(model_fields.keys()) == 0:
|
|
||||||
model_fields["id"] = Integer(name="id", primary_key=True)
|
|
||||||
logging.warning(
|
|
||||||
"Table {table_name} had no fields so auto "
|
|
||||||
"Integer primary key named `id` created."
|
|
||||||
)
|
|
||||||
validate_related_names_in_relations(model_fields, new_model)
|
|
||||||
for field_name, field in model_fields.items():
|
|
||||||
if field.primary_key:
|
|
||||||
pkname = check_pk_column_validity(field_name, field, pkname)
|
|
||||||
if (
|
|
||||||
not field.pydantic_only
|
|
||||||
and not field.virtual
|
|
||||||
and not issubclass(field, ManyToManyField)
|
|
||||||
):
|
|
||||||
columns.append(field.get_column(field.get_alias()))
|
|
||||||
return pkname, columns
|
|
||||||
|
|
||||||
|
|
||||||
def register_relation_in_alias_manager_new(
|
|
||||||
new_model: Type["Model"], field: Type[ForeignKeyField], field_name: str
|
|
||||||
) -> None:
|
|
||||||
if issubclass(field, ManyToManyField):
|
|
||||||
register_many_to_many_relation_on_build_new(new_model=new_model, field=field)
|
|
||||||
elif issubclass(field, ForeignKeyField):
|
|
||||||
register_relation_on_build_new(new_model=new_model, field_name=field_name)
|
|
||||||
|
|
||||||
|
|
||||||
def populate_default_pydantic_field_value(
|
|
||||||
ormar_field: Type[BaseField], field_name: str, attrs: dict
|
|
||||||
) -> dict:
|
|
||||||
curr_def_value = attrs.get(field_name, ormar.Undefined)
|
|
||||||
if lenient_issubclass(curr_def_value, ormar.fields.BaseField):
|
|
||||||
curr_def_value = ormar.Undefined
|
|
||||||
if curr_def_value is None:
|
|
||||||
attrs[field_name] = ormar_field.convert_to_pydantic_field_info(allow_null=True)
|
|
||||||
else:
|
|
||||||
attrs[field_name] = ormar_field.convert_to_pydantic_field_info()
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:
|
|
||||||
model_fields = {}
|
|
||||||
potential_fields = {
|
|
||||||
k: v
|
|
||||||
for k, v in attrs["__annotations__"].items()
|
|
||||||
if lenient_issubclass(v, BaseField)
|
|
||||||
}
|
|
||||||
if potential_fields:
|
|
||||||
warnings.warn(
|
|
||||||
"Using ormar.Fields as type Model annotation has been deprecated,"
|
|
||||||
" check documentation of current version",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
potential_fields.update(get_potential_fields(attrs))
|
|
||||||
for field_name, field in potential_fields.items():
|
|
||||||
field.name = field_name
|
|
||||||
attrs = populate_default_pydantic_field_value(field, field_name, attrs)
|
|
||||||
model_fields[field_name] = field
|
|
||||||
attrs["__annotations__"][field_name] = (
|
|
||||||
field.__type__ if not field.nullable else Optional[field.__type__]
|
|
||||||
)
|
|
||||||
return attrs, model_fields
|
|
||||||
|
|
||||||
|
|
||||||
def extract_annotations_and_default_vals(attrs: dict) -> Tuple[Dict, Dict]:
|
|
||||||
key = "__annotations__"
|
|
||||||
attrs[key] = attrs.get(key, {})
|
|
||||||
attrs, model_fields = populate_pydantic_default_values(attrs)
|
|
||||||
return attrs, model_fields
|
|
||||||
|
|
||||||
|
|
||||||
def populate_meta_tablename_columns_and_pk(
|
|
||||||
name: str, new_model: Type["Model"]
|
|
||||||
) -> Type["Model"]:
|
|
||||||
tablename = name.lower() + "s"
|
|
||||||
new_model.Meta.tablename = (
|
|
||||||
new_model.Meta.tablename if hasattr(new_model.Meta, "tablename") else tablename
|
|
||||||
)
|
|
||||||
pkname: Optional[str]
|
|
||||||
|
|
||||||
if hasattr(new_model.Meta, "columns"):
|
|
||||||
columns = new_model.Meta.table.columns
|
|
||||||
pkname = new_model.Meta.pkname
|
|
||||||
else:
|
|
||||||
pkname, columns = sqlalchemy_columns_from_model_fields(
|
|
||||||
new_model.Meta.model_fields, new_model
|
|
||||||
)
|
|
||||||
|
|
||||||
if pkname is None:
|
|
||||||
raise ModelDefinitionError("Table has to have a primary key.")
|
|
||||||
|
|
||||||
new_model.Meta.columns = columns
|
|
||||||
new_model.Meta.pkname = pkname
|
|
||||||
return new_model
|
|
||||||
|
|
||||||
|
|
||||||
def populate_meta_sqlalchemy_table_if_required(
|
|
||||||
new_model: Type["Model"],
|
|
||||||
) -> Type["Model"]:
|
|
||||||
"""
|
|
||||||
Constructs sqlalchemy table out of columns and parameters set on Meta class.
|
|
||||||
It populates name, metadata, columns and constraints.
|
|
||||||
|
|
||||||
:param new_model: class without sqlalchemy table constructed
|
|
||||||
:type new_model: Model class
|
|
||||||
:return: class with populated Meta.table
|
|
||||||
:rtype: Model class
|
|
||||||
"""
|
|
||||||
if not hasattr(new_model.Meta, "table"):
|
|
||||||
new_model.Meta.table = sqlalchemy.Table(
|
|
||||||
new_model.Meta.tablename,
|
|
||||||
new_model.Meta.metadata,
|
|
||||||
*new_model.Meta.columns,
|
|
||||||
*new_model.Meta.constraints,
|
|
||||||
)
|
|
||||||
return new_model
|
|
||||||
|
|
||||||
|
|
||||||
def get_pydantic_base_orm_config() -> Type[BaseConfig]:
|
|
||||||
"""
|
|
||||||
Returns empty pydantic Config with orm_mode set to True.
|
|
||||||
|
|
||||||
:return: empty default config with orm_mode set.
|
|
||||||
:rtype: pydantic Config
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Config(BaseConfig):
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
return Config
|
|
||||||
|
|
||||||
|
|
||||||
def check_if_field_has_choices(field: Type[BaseField]) -> bool:
|
def check_if_field_has_choices(field: Type[BaseField]) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if given field has choices populated.
|
Checks if given field has choices populated.
|
||||||
@ -400,32 +122,6 @@ def populate_choices_validators(model: Type["Model"]) -> None: # noqa CCR001
|
|||||||
model.__pre_root_validators__ = validators
|
model.__pre_root_validators__ = validators
|
||||||
|
|
||||||
|
|
||||||
def populate_default_options_values(
|
|
||||||
new_model: Type["Model"], model_fields: Dict
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Sets all optional Meta values to it's defaults
|
|
||||||
and set model_fields that were already previously extracted.
|
|
||||||
|
|
||||||
Here should live all options that are not overwritten/set for all models.
|
|
||||||
|
|
||||||
Current options are:
|
|
||||||
* constraints = []
|
|
||||||
* abstract = False
|
|
||||||
|
|
||||||
:param new_model: newly constructed Model
|
|
||||||
:type new_model: Model class
|
|
||||||
:param model_fields:
|
|
||||||
:type model_fields: Union[Dict[str, type], Dict]
|
|
||||||
"""
|
|
||||||
if not hasattr(new_model.Meta, "constraints"):
|
|
||||||
new_model.Meta.constraints = []
|
|
||||||
if not hasattr(new_model.Meta, "model_fields"):
|
|
||||||
new_model.Meta.model_fields = model_fields
|
|
||||||
if not hasattr(new_model.Meta, "abstract"):
|
|
||||||
new_model.Meta.abstract = False
|
|
||||||
|
|
||||||
|
|
||||||
def add_cached_properties(new_model: Type["Model"]) -> None:
|
def add_cached_properties(new_model: Type["Model"]) -> None:
|
||||||
"""
|
"""
|
||||||
Sets cached properties for both pydantic and ormar models.
|
Sets cached properties for both pydantic and ormar models.
|
||||||
@ -510,24 +206,12 @@ def register_signals(new_model: Type["Model"]) -> None: # noqa: CCR001
|
|||||||
new_model.Meta.signals = signals
|
new_model.Meta.signals = signals
|
||||||
|
|
||||||
|
|
||||||
def get_potential_fields(attrs: Dict) -> Dict:
|
|
||||||
"""
|
|
||||||
Gets all the fields in current class namespace that are Fields.
|
|
||||||
|
|
||||||
:param attrs: current class namespace
|
|
||||||
:type attrs: Dict
|
|
||||||
:return: extracted fields that are ormar Fields
|
|
||||||
:rtype: Dict
|
|
||||||
"""
|
|
||||||
return {k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)}
|
|
||||||
|
|
||||||
|
|
||||||
def check_conflicting_fields(
|
def check_conflicting_fields(
|
||||||
new_fields: Set,
|
new_fields: Set,
|
||||||
attrs: Dict,
|
attrs: Dict,
|
||||||
base_class: type,
|
base_class: type,
|
||||||
curr_class: type,
|
curr_class: type,
|
||||||
previous_fields: Set = None,
|
previous_fields: Set = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
You cannot redefine fields with same names in inherited classes.
|
You cannot redefine fields with same names in inherited classes.
|
||||||
@ -557,11 +241,11 @@ def check_conflicting_fields(
|
|||||||
|
|
||||||
|
|
||||||
def update_attrs_and_fields(
|
def update_attrs_and_fields(
|
||||||
attrs: Dict,
|
attrs: Dict,
|
||||||
new_attrs: Dict,
|
new_attrs: Dict,
|
||||||
model_fields: Dict,
|
model_fields: Dict,
|
||||||
new_model_fields: Dict,
|
new_model_fields: Dict,
|
||||||
new_fields: Set,
|
new_fields: Set,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Updates __annotations__, values of model fields (so pydantic FieldInfos)
|
Updates __annotations__, values of model fields (so pydantic FieldInfos)
|
||||||
@ -585,7 +269,7 @@ def update_attrs_and_fields(
|
|||||||
|
|
||||||
|
|
||||||
def update_attrs_from_base_meta( # noqa: CCR001
|
def update_attrs_from_base_meta( # noqa: CCR001
|
||||||
base_class: "Model", attrs: Dict,
|
base_class: "Model", attrs: Dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Updates Meta parameters in child from parent if needed.
|
Updates Meta parameters in child from parent if needed.
|
||||||
@ -612,13 +296,85 @@ def update_attrs_from_base_meta( # noqa: CCR001
|
|||||||
setattr(attrs["Meta"], param, parent_value)
|
setattr(attrs["Meta"], param, parent_value)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_data_from_parent_model( # noqa: CCR001
|
||||||
|
base_class: Type["Model"],
|
||||||
|
curr_class: type,
|
||||||
|
attrs: Dict,
|
||||||
|
model_fields: Dict[
|
||||||
|
str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]]
|
||||||
|
],
|
||||||
|
) -> Tuple[Dict, Dict]:
|
||||||
|
"""
|
||||||
|
Copy the key parameters [databse, metadata, property_fields and constraints]
|
||||||
|
and fields from parent models. Overwrites them if needed.
|
||||||
|
|
||||||
|
Only abstract classes can be subclassed.
|
||||||
|
|
||||||
|
Since relation fields requires different related_name for different children
|
||||||
|
|
||||||
|
|
||||||
|
:raises: ModelDefinitionError if non abstract model is subclassed
|
||||||
|
:param base_class: one of the parent classes
|
||||||
|
:type base_class: Model or model parent class
|
||||||
|
:param curr_class: current constructed class
|
||||||
|
:type curr_class: Model or model parent class
|
||||||
|
:param attrs: new namespace for class being constructed
|
||||||
|
:type attrs: Dict
|
||||||
|
:param model_fields: ormar fields in defined in current class
|
||||||
|
:type model_fields: Dict[str, BaseField]
|
||||||
|
:return: updated attrs and model_fields
|
||||||
|
:rtype: Tuple[Dict, Dict]
|
||||||
|
"""
|
||||||
|
if attrs.get("Meta"):
|
||||||
|
new_fields = set(base_class.Meta.model_fields.keys()) # type: ignore
|
||||||
|
previous_fields = set({k for k, v in attrs.items() if isinstance(v, FieldInfo)})
|
||||||
|
check_conflicting_fields(
|
||||||
|
new_fields=new_fields,
|
||||||
|
attrs=attrs,
|
||||||
|
base_class=base_class,
|
||||||
|
curr_class=curr_class,
|
||||||
|
previous_fields=previous_fields,
|
||||||
|
)
|
||||||
|
if previous_fields and not base_class.Meta.abstract: # type: ignore
|
||||||
|
raise ModelDefinitionError(
|
||||||
|
f"{curr_class.__name__} cannot inherit "
|
||||||
|
f"from non abstract class {base_class.__name__}"
|
||||||
|
)
|
||||||
|
update_attrs_from_base_meta(
|
||||||
|
base_class=base_class, # type: ignore
|
||||||
|
attrs=attrs,
|
||||||
|
)
|
||||||
|
parent_fields = dict()
|
||||||
|
meta = attrs.get("Meta")
|
||||||
|
if not meta: # pragma: no cover
|
||||||
|
raise ModelDefinitionError(
|
||||||
|
f"Model {curr_class.__name__} declared without Meta"
|
||||||
|
)
|
||||||
|
table_name = (
|
||||||
|
meta.tablename
|
||||||
|
if hasattr(meta, "tablename") and meta.tablename
|
||||||
|
else attrs.get("__name__", "").lower() + "s"
|
||||||
|
)
|
||||||
|
for field_name, field in base_class.Meta.model_fields.items():
|
||||||
|
if issubclass(field, ForeignKeyField) and field.related_name:
|
||||||
|
copy_field = type(field.__name__, (field,), dict(field.__dict__))
|
||||||
|
related_name = field.related_name + "_" + table_name
|
||||||
|
copy_field.related_name = related_name # type: ignore
|
||||||
|
parent_fields[field_name] = copy_field
|
||||||
|
else:
|
||||||
|
parent_fields[field_name] = field
|
||||||
|
|
||||||
|
model_fields.update(parent_fields) # type: ignore
|
||||||
|
return attrs, model_fields
|
||||||
|
|
||||||
|
|
||||||
def extract_from_parents_definition( # noqa: CCR001
|
def extract_from_parents_definition( # noqa: CCR001
|
||||||
base_class: type,
|
base_class: type,
|
||||||
curr_class: type,
|
curr_class: type,
|
||||||
attrs: Dict,
|
attrs: Dict,
|
||||||
model_fields: Dict[
|
model_fields: Dict[
|
||||||
str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]]
|
str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]]
|
||||||
],
|
],
|
||||||
) -> Tuple[Dict, Dict]:
|
) -> Tuple[Dict, Dict]:
|
||||||
"""
|
"""
|
||||||
Extracts fields from base classes if they have valid oramr fields.
|
Extracts fields from base classes if they have valid oramr fields.
|
||||||
@ -644,40 +400,13 @@ def extract_from_parents_definition( # noqa: CCR001
|
|||||||
:rtype: Tuple[Dict, Dict]
|
:rtype: Tuple[Dict, Dict]
|
||||||
"""
|
"""
|
||||||
if hasattr(base_class, "Meta"):
|
if hasattr(base_class, "Meta"):
|
||||||
if attrs.get("Meta"):
|
base_class = cast(Type["Model"], base_class)
|
||||||
new_fields = set(base_class.Meta.model_fields.keys()) # type: ignore
|
return copy_data_from_parent_model(
|
||||||
previous_fields = set(
|
base_class=base_class,
|
||||||
{k for k, v in attrs.items() if isinstance(v, FieldInfo)}
|
curr_class=curr_class,
|
||||||
)
|
attrs=attrs,
|
||||||
check_conflicting_fields(
|
model_fields=model_fields,
|
||||||
new_fields=new_fields,
|
)
|
||||||
attrs=attrs,
|
|
||||||
base_class=base_class,
|
|
||||||
curr_class=curr_class,
|
|
||||||
previous_fields=previous_fields,
|
|
||||||
)
|
|
||||||
if previous_fields and not base_class.Meta.abstract: # type: ignore
|
|
||||||
raise ModelDefinitionError(
|
|
||||||
f"{curr_class.__name__} cannot inherit "
|
|
||||||
f"from non abstract class {base_class.__name__}"
|
|
||||||
)
|
|
||||||
update_attrs_from_base_meta(
|
|
||||||
base_class=base_class, # type: ignore
|
|
||||||
attrs=attrs,
|
|
||||||
)
|
|
||||||
parent_fields = dict()
|
|
||||||
table_name = attrs.get("Meta").tablename if hasattr(attrs.get("Meta"), "tablename") else attrs.get(
|
|
||||||
'__name__').lower() + 's'
|
|
||||||
for field_name, field in base_class.Meta.model_fields.items():
|
|
||||||
if issubclass(field, ForeignKeyField) and field.related_name:
|
|
||||||
copy_field = type(field.__name__, (field,), dict(field.__dict__))
|
|
||||||
copy_field.related_name = field.related_name + '_' + table_name
|
|
||||||
parent_fields[field_name] = copy_field
|
|
||||||
else:
|
|
||||||
parent_fields[field_name] = field
|
|
||||||
|
|
||||||
model_fields.update(parent_fields) # type: ignore
|
|
||||||
return attrs, model_fields
|
|
||||||
|
|
||||||
key = "__annotations__"
|
key = "__annotations__"
|
||||||
if hasattr(base_class, PARSED_FIELDS_KEY):
|
if hasattr(base_class, PARSED_FIELDS_KEY):
|
||||||
@ -734,8 +463,37 @@ def extract_from_parents_definition( # noqa: CCR001
|
|||||||
|
|
||||||
class ModelMetaclass(pydantic.main.ModelMetaclass):
|
class ModelMetaclass(pydantic.main.ModelMetaclass):
|
||||||
def __new__( # type: ignore # noqa: CCR001
|
def __new__( # type: ignore # noqa: CCR001
|
||||||
mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict
|
mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict
|
||||||
) -> "ModelMetaclass":
|
) -> "ModelMetaclass":
|
||||||
|
"""
|
||||||
|
Metaclass used by ormar Models that performs configuration
|
||||||
|
and build of ormar Models.
|
||||||
|
|
||||||
|
|
||||||
|
Sets pydantic configuration.
|
||||||
|
Extract model_fields and convert them to pydantic FieldInfo,
|
||||||
|
updates class namespace.
|
||||||
|
|
||||||
|
Extracts settings and fields from parent classes.
|
||||||
|
Fetches methods decorated with @property_field decorator
|
||||||
|
to expose them later in dict().
|
||||||
|
|
||||||
|
Construct parent pydantic Metaclass/ Model.
|
||||||
|
|
||||||
|
If class has Meta class declared (so actual ormar Models) it also:
|
||||||
|
* populate sqlalchemy columns, pkname and tables from model_fields
|
||||||
|
* register reverse relationships on related models
|
||||||
|
* registers all relations in alias manager that populates table_prefixes
|
||||||
|
* exposes alias manager on each Model
|
||||||
|
* creates QuerySet for each model and exposes it on a class
|
||||||
|
|
||||||
|
:param name: name of current class
|
||||||
|
:type name: str
|
||||||
|
:param bases: base classes
|
||||||
|
:type bases: Tuple
|
||||||
|
:param attrs: class namespace
|
||||||
|
:type attrs: Dict
|
||||||
|
"""
|
||||||
attrs["Config"] = get_pydantic_base_orm_config()
|
attrs["Config"] = get_pydantic_base_orm_config()
|
||||||
attrs["__name__"] = name
|
attrs["__name__"] = name
|
||||||
attrs, model_fields = extract_annotations_and_default_vals(attrs)
|
attrs, model_fields = extract_annotations_and_default_vals(attrs)
|
||||||
@ -760,7 +518,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
|
|||||||
new_model = populate_meta_sqlalchemy_table_if_required(new_model)
|
new_model = populate_meta_sqlalchemy_table_if_required(new_model)
|
||||||
expand_reverse_relationships(new_model)
|
expand_reverse_relationships(new_model)
|
||||||
for field_name, field in new_model.Meta.model_fields.items():
|
for field_name, field in new_model.Meta.model_fields.items():
|
||||||
register_relation_in_alias_manager_new(new_model, field, field_name)
|
register_relation_in_alias_manager(new_model, field, field_name)
|
||||||
|
|
||||||
if new_model.Meta.pkname not in attrs["__annotations__"]:
|
if new_model.Meta.pkname not in attrs["__annotations__"]:
|
||||||
field_name = new_model.Meta.pkname
|
field_name = new_model.Meta.pkname
|
||||||
|
|||||||
@ -22,6 +22,20 @@ from ormar.models.metaclass import ModelMeta
|
|||||||
|
|
||||||
|
|
||||||
def group_related_list(list_: List) -> Dict:
|
def group_related_list(list_: List) -> Dict:
|
||||||
|
"""
|
||||||
|
Translates the list of related strings into a dictionary.
|
||||||
|
That way nested models are grouped to traverse them in a right order
|
||||||
|
and to avoid repetition.
|
||||||
|
|
||||||
|
Sample: ["people__houses", "people__cars__models", "people__cars__colors"]
|
||||||
|
will become:
|
||||||
|
{'people': {'houses': [], 'cars': ['models', 'colors']}}
|
||||||
|
|
||||||
|
:param list_: list of related models used in select related
|
||||||
|
:type list_: List[str]
|
||||||
|
:return: list converted to dictionary to avoid repetition and group nested models
|
||||||
|
:rtype: Dict[str, List]
|
||||||
|
"""
|
||||||
test_dict: Dict[str, Any] = dict()
|
test_dict: Dict[str, Any] = dict()
|
||||||
grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0])
|
grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0])
|
||||||
for key, group in grouped:
|
for key, group in grouped:
|
||||||
@ -63,7 +77,38 @@ class Model(NewBaseModel):
|
|||||||
fields: Optional[Union[Dict, Set]] = None,
|
fields: Optional[Union[Dict, Set]] = None,
|
||||||
exclude_fields: Optional[Union[Dict, Set]] = None,
|
exclude_fields: Optional[Union[Dict, Set]] = None,
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
|
"""
|
||||||
|
Model method to convert raw sql row from database into ormar.Model instance.
|
||||||
|
Traverses nested models if they were specified in select_related for query.
|
||||||
|
|
||||||
|
Called recurrently and returns model instance if it's present in the row.
|
||||||
|
Note that it's processing one row at a time, so if there are duplicates of
|
||||||
|
parent row that needs to be joined/combined
|
||||||
|
(like parent row in sql join with 2+ child rows)
|
||||||
|
instances populated in this method are later combined in the QuerySet.
|
||||||
|
Other method working directly on raw database results is in prefetch_query,
|
||||||
|
where rows are populated in a different way as they do not have
|
||||||
|
nested models in result.
|
||||||
|
|
||||||
|
:param row: raw result row from the database
|
||||||
|
:type row: sqlalchemy.engine.result.ResultProxy
|
||||||
|
:param select_related: list of names of related models fetched from database
|
||||||
|
:type select_related: List
|
||||||
|
:param related_models: list or dict of related models
|
||||||
|
:type related_models: Union[List, Dict]
|
||||||
|
:param previous_model: internal param for nested models to specify table_prefix
|
||||||
|
:type previous_model: Model class
|
||||||
|
:param related_name: internal parameter - name of current nested model
|
||||||
|
:type related_name: str
|
||||||
|
:param fields: fields and related model fields to include
|
||||||
|
if provided only those are included
|
||||||
|
:type fields: Optional[Union[Dict, Set]]
|
||||||
|
:param exclude_fields: fields and related model fields to exclude
|
||||||
|
excludes the fields even if they are provided in fields
|
||||||
|
:type exclude_fields: Optional[Union[Dict, Set]]
|
||||||
|
:return: returns model if model is populated from database
|
||||||
|
:rtype: Optional[Model]
|
||||||
|
"""
|
||||||
item: Dict[str, Any] = {}
|
item: Dict[str, Any] = {}
|
||||||
select_related = select_related or []
|
select_related = select_related or []
|
||||||
related_models = related_models or []
|
related_models = related_models or []
|
||||||
@ -86,7 +131,7 @@ class Model(NewBaseModel):
|
|||||||
previous_model = through_field.through # type: ignore
|
previous_model = through_field.through # type: ignore
|
||||||
|
|
||||||
if previous_model and rel_name2:
|
if previous_model and rel_name2:
|
||||||
table_prefix = cls.Meta.alias_manager.resolve_relation_join_new(
|
table_prefix = cls.Meta.alias_manager.resolve_relation_join(
|
||||||
previous_model, rel_name2
|
previous_model, rel_name2
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -127,6 +172,32 @@ class Model(NewBaseModel):
|
|||||||
fields: Optional[Union[Dict, Set]] = None,
|
fields: Optional[Union[Dict, Set]] = None,
|
||||||
exclude_fields: Optional[Union[Dict, Set]] = None,
|
exclude_fields: Optional[Union[Dict, Set]] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Traverses structure of related models and populates the nested models
|
||||||
|
from the database row.
|
||||||
|
Related models can be a list if only directly related models are to be
|
||||||
|
populated, converted to dict if related models also have their own related
|
||||||
|
models to be populated.
|
||||||
|
|
||||||
|
Recurrently calls from_row method on nested instances and create nested
|
||||||
|
instances. In the end those instances are added to the final model dictionary.
|
||||||
|
|
||||||
|
:param item: dictionary of already populated nested models, otherwise empty dict
|
||||||
|
:type item: Dict
|
||||||
|
:param row: raw result row from the database
|
||||||
|
:type row: sqlalchemy.engine.result.ResultProxy
|
||||||
|
:param related_models: list or dict of related models
|
||||||
|
:type related_models: Union[Dict, List]
|
||||||
|
:param fields: fields and related model fields to include -
|
||||||
|
if provided only those are included
|
||||||
|
:type fields: Optional[Union[Dict, Set]]
|
||||||
|
:param exclude_fields: fields and related model fields to exclude
|
||||||
|
excludes the fields even if they are provided in fields
|
||||||
|
:type exclude_fields: Optional[Union[Dict, Set]]
|
||||||
|
:return: dictionary with keys corresponding to model fields names
|
||||||
|
and values are database values
|
||||||
|
:rtype: Dict
|
||||||
|
"""
|
||||||
for related in related_models:
|
for related in related_models:
|
||||||
if isinstance(related_models, dict) and related_models[related]:
|
if isinstance(related_models, dict) and related_models[related]:
|
||||||
first_part, remainder = related, related_models[related]
|
first_part, remainder = related, related_models[related]
|
||||||
@ -176,22 +247,26 @@ class Model(NewBaseModel):
|
|||||||
All joined tables have prefixes to allow duplicate column names,
|
All joined tables have prefixes to allow duplicate column names,
|
||||||
as well as duplicated joins to the same table from multiple different tables.
|
as well as duplicated joins to the same table from multiple different tables.
|
||||||
|
|
||||||
Extracted fields populates the item dict that is later used to construct a Model.
|
Extracted fields populates the item dict later used to construct a Model.
|
||||||
|
|
||||||
|
Used in Model.from_row and PrefetchQuery._populate_rows methods.
|
||||||
|
|
||||||
:param item: dictionary of already populated nested models, otherwise empty dict
|
:param item: dictionary of already populated nested models, otherwise empty dict
|
||||||
:type item: Dict
|
:type item: Dict
|
||||||
:param row: raw result row from the database
|
:param row: raw result row from the database
|
||||||
:type row: sqlalchemy.engine.result.ResultProxy
|
:type row: sqlalchemy.engine.result.ResultProxy
|
||||||
:param table_prefix: prefix of the table from AliasManager
|
:param table_prefix: prefix of the table from AliasManager
|
||||||
each pair of tables have own prefix (two of them depending on direction) - used in joins
|
each pair of tables have own prefix (two of them depending on direction) -
|
||||||
to allow multiple joins to the same table.
|
used in joins to allow multiple joins to the same table.
|
||||||
:type table_prefix: str
|
:type table_prefix: str
|
||||||
:param fields: fields and related model fields to include - if provided only those are included
|
:param fields: fields and related model fields to include -
|
||||||
|
if provided only those are included
|
||||||
:type fields: Optional[Union[Dict, Set]]
|
:type fields: Optional[Union[Dict, Set]]
|
||||||
:param exclude_fields: fields and related model fields to exclude
|
:param exclude_fields: fields and related model fields to exclude
|
||||||
excludes the fields even if they are provided in fields
|
excludes the fields even if they are provided in fields
|
||||||
:type exclude_fields: Optional[Union[Dict, Set]]
|
:type exclude_fields: Optional[Union[Dict, Set]]
|
||||||
:return: dictionary with keys corresponding to model fields names and values are database values
|
:return: dictionary with keys corresponding to model fields names
|
||||||
|
and values are database values
|
||||||
:rtype: Dict
|
:rtype: Dict
|
||||||
"""
|
"""
|
||||||
# databases does not keep aliases in Record for postgres, change to raw row
|
# databases does not keep aliases in Record for postgres, change to raw row
|
||||||
@ -216,7 +291,7 @@ class Model(NewBaseModel):
|
|||||||
|
|
||||||
async def upsert(self: T, **kwargs: Any) -> T:
|
async def upsert(self: T, **kwargs: Any) -> T:
|
||||||
"""
|
"""
|
||||||
Performs either a save or an update depending on the presence of the primary key.
|
Performs either a save or an update depending on the presence of the pk.
|
||||||
If the pk field is filled it's an update, otherwise the save is performed.
|
If the pk field is filled it's an update, otherwise the save is performed.
|
||||||
For save kwargs are ignored, used only in update if provided.
|
For save kwargs are ignored, used only in update if provided.
|
||||||
|
|
||||||
@ -237,11 +312,13 @@ class Model(NewBaseModel):
|
|||||||
Related models are saved by pk number, reverse relation and many to many fields
|
Related models are saved by pk number, reverse relation and many to many fields
|
||||||
are not saved - use corresponding relations methods.
|
are not saved - use corresponding relations methods.
|
||||||
|
|
||||||
If there are fields with server_default set and those fields are not already filled
|
If there are fields with server_default set and those fields
|
||||||
save will trigger also a second query to refreshed the fields populated server side.
|
are not already filled save will trigger also a second query
|
||||||
|
to refreshed the fields populated server side.
|
||||||
|
|
||||||
Does not recognize if model was previously saved. If you want to perform update or
|
Does not recognize if model was previously saved.
|
||||||
insert depending on the pk fields presence use upsert.
|
If you want to perform update or insert depending on the pk
|
||||||
|
fields presence use upsert.
|
||||||
|
|
||||||
Sends pre_save and post_save signals.
|
Sends pre_save and post_save signals.
|
||||||
|
|
||||||
@ -289,7 +366,8 @@ class Model(NewBaseModel):
|
|||||||
self, follow: bool = False, visited: Set = None, update_count: int = 0
|
self, follow: bool = False, visited: Set = None, update_count: int = 0
|
||||||
) -> int: # noqa: CCR001
|
) -> int: # noqa: CCR001
|
||||||
"""
|
"""
|
||||||
Triggers a upsert method on all related models if the instances are not already saved.
|
Triggers a upsert method on all related models
|
||||||
|
if the instances are not already saved.
|
||||||
By default saves only the directly related ones.
|
By default saves only the directly related ones.
|
||||||
|
|
||||||
If follow=True is set it saves also related models of related models.
|
If follow=True is set it saves also related models of related models.
|
||||||
@ -299,15 +377,17 @@ class Model(NewBaseModel):
|
|||||||
|
|
||||||
That way already visited models that are nested are saved, but the save do not
|
That way already visited models that are nested are saved, but the save do not
|
||||||
follow them inside. So Model A -> Model B -> Model A -> Model C will save second
|
follow them inside. So Model A -> Model B -> Model A -> Model C will save second
|
||||||
Model A but will never follow into Model C. Nested relations of those kind need to
|
Model A but will never follow into Model C.
|
||||||
be persisted manually.
|
Nested relations of those kind need to be persisted manually.
|
||||||
|
|
||||||
:param follow: flag to trigger deep save - by default only directly related models are saved
|
:param follow: flag to trigger deep save -
|
||||||
|
by default only directly related models are saved
|
||||||
with follow=True also related models of related models are saved
|
with follow=True also related models of related models are saved
|
||||||
:type follow: bool
|
:type follow: bool
|
||||||
:param visited: internal parameter for recursive calls - already visited models
|
:param visited: internal parameter for recursive calls - already visited models
|
||||||
:type visited: Set
|
:type visited: Set
|
||||||
:param update_count: internal parameter for recursive calls - no uf updated instances
|
:param update_count: internal parameter for recursive calls -
|
||||||
|
number of updated instances
|
||||||
:type update_count: int
|
:type update_count: int
|
||||||
:return: number of updated/saved models
|
:return: number of updated/saved models
|
||||||
:rtype: int
|
:rtype: int
|
||||||
@ -348,12 +428,14 @@ class Model(NewBaseModel):
|
|||||||
|
|
||||||
:param rel: Model to follow
|
:param rel: Model to follow
|
||||||
:type rel: Model
|
:type rel: Model
|
||||||
:param follow: flag to trigger deep save - by default only directly related models are saved
|
:param follow: flag to trigger deep save -
|
||||||
|
by default only directly related models are saved
|
||||||
with follow=True also related models of related models are saved
|
with follow=True also related models of related models are saved
|
||||||
:type follow: bool
|
:type follow: bool
|
||||||
:param visited: internal parameter for recursive calls - already visited models
|
:param visited: internal parameter for recursive calls - already visited models
|
||||||
:type visited: Set
|
:type visited: Set
|
||||||
:param update_count: internal parameter for recursive calls - no uf updated instances
|
:param update_count: internal parameter for recursive calls -
|
||||||
|
number of updated instances
|
||||||
:type update_count: int
|
:type update_count: int
|
||||||
:return: tuple of update count and visited
|
:return: tuple of update count and visited
|
||||||
:rtype: Tuple[int, Set]
|
:rtype: Tuple[int, Set]
|
||||||
@ -429,10 +511,10 @@ class Model(NewBaseModel):
|
|||||||
async def load(self: T) -> T:
|
async def load(self: T) -> T:
|
||||||
"""
|
"""
|
||||||
Allow to refresh existing Models fields from database.
|
Allow to refresh existing Models fields from database.
|
||||||
Be careful as the related models can be overwritten by pk_only models during load.
|
Be careful as the related models can be overwritten by pk_only models in load.
|
||||||
Does NOT refresh the related models fields if they were loaded before.
|
Does NOT refresh the related models fields if they were loaded before.
|
||||||
|
|
||||||
:raises: If given primary key is not found in database the NoMatch exception is raised.
|
:raises: If given pk is not found in database the NoMatch exception is raised.
|
||||||
|
|
||||||
:return: reloaded Model
|
:return: reloaded Model
|
||||||
:rtype: Model
|
:rtype: Model
|
||||||
|
|||||||
@ -141,7 +141,7 @@ class QueryClause:
|
|||||||
through_field.through, through_field.to, explicit_multi=True
|
through_field.through, through_field.to, explicit_multi=True
|
||||||
)
|
)
|
||||||
manager = model_cls.Meta.alias_manager
|
manager = model_cls.Meta.alias_manager
|
||||||
table_prefix = manager.resolve_relation_join_new(previous_model, part2)
|
table_prefix = manager.resolve_relation_join(previous_model, part2)
|
||||||
model_cls = model_cls.Meta.model_fields[part].to
|
model_cls = model_cls.Meta.model_fields[part].to
|
||||||
previous_model = model_cls
|
previous_model = model_cls
|
||||||
return select_related, table_prefix, model_cls
|
return select_related, table_prefix, model_cls
|
||||||
|
|||||||
@ -135,7 +135,7 @@ class SqlJoin:
|
|||||||
model_cls = join_params.model_cls.Meta.model_fields[part].to
|
model_cls = join_params.model_cls.Meta.model_fields[part].to
|
||||||
to_table = model_cls.Meta.table.name
|
to_table = model_cls.Meta.table.name
|
||||||
|
|
||||||
alias = model_cls.Meta.alias_manager.resolve_relation_join_new(
|
alias = model_cls.Meta.alias_manager.resolve_relation_join(
|
||||||
join_params.prev_model, part
|
join_params.prev_model, part
|
||||||
)
|
)
|
||||||
if alias not in self.used_aliases:
|
if alias not in self.used_aliases:
|
||||||
|
|||||||
@ -328,7 +328,7 @@ class PrefetchQuery:
|
|||||||
if issubclass(target_field, ManyToManyField):
|
if issubclass(target_field, ManyToManyField):
|
||||||
query_target = target_field.through
|
query_target = target_field.through
|
||||||
select_related = [target_name]
|
select_related = [target_name]
|
||||||
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_join_new(
|
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_join(
|
||||||
query_target, target_name
|
query_target, target_name
|
||||||
)
|
)
|
||||||
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
|
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class AliasManager:
|
|||||||
def prefixed_table_name(alias: str, name: str) -> text:
|
def prefixed_table_name(alias: str, name: str) -> text:
|
||||||
return text(f"{name} {alias}_{name}")
|
return text(f"{name} {alias}_{name}")
|
||||||
|
|
||||||
def add_relation_type_new(
|
def add_relation_type(
|
||||||
self, source_model: Type["Model"], relation_name: str, is_multi: bool = False
|
self, source_model: Type["Model"], relation_name: str, is_multi: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
parent_key = f"{source_model.get_name()}_{relation_name}"
|
parent_key = f"{source_model.get_name()}_{relation_name}"
|
||||||
@ -56,7 +56,7 @@ class AliasManager:
|
|||||||
if child_key not in self._aliases_new:
|
if child_key not in self._aliases_new:
|
||||||
self._aliases_new[child_key] = get_table_alias()
|
self._aliases_new[child_key] = get_table_alias()
|
||||||
|
|
||||||
def resolve_relation_join_new(
|
def resolve_relation_join(
|
||||||
self, from_model: Type["Model"], relation_name: str
|
self, from_model: Type["Model"], relation_name: str
|
||||||
) -> str:
|
) -> str:
|
||||||
alias = self._aliases_new.get(f"{from_model.get_name()}_{relation_name}", "")
|
alias = self._aliases_new.get(f"{from_model.get_name()}_{relation_name}", "")
|
||||||
|
|||||||
@ -100,7 +100,7 @@ class Car(ormar.Model):
|
|||||||
id: int = ormar.Integer(primary_key=True)
|
id: int = ormar.Integer(primary_key=True)
|
||||||
name: str = ormar.String(max_length=50)
|
name: str = ormar.String(max_length=50)
|
||||||
owner: Person = ormar.ForeignKey(Person)
|
owner: Person = ormar.ForeignKey(Person)
|
||||||
co_owner: Person = ormar.ForeignKey(Person, related_name='coowned')
|
co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
|
||||||
|
|
||||||
|
|
||||||
class Truck(Car):
|
class Truck(Car):
|
||||||
@ -113,7 +113,7 @@ class Truck(Car):
|
|||||||
|
|
||||||
class Bus(Car):
|
class Bus(Car):
|
||||||
class Meta:
|
class Meta:
|
||||||
tablename = 'buses'
|
tablename = "buses"
|
||||||
metadata = metadata
|
metadata = metadata
|
||||||
database = db
|
database = db
|
||||||
|
|
||||||
@ -134,6 +134,7 @@ def test_init_of_abstract_model():
|
|||||||
|
|
||||||
def test_field_redefining_raises_error():
|
def test_field_redefining_raises_error():
|
||||||
with pytest.raises(ModelDefinitionError):
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
class WrongField(DateFieldsModel): # pragma: no cover
|
class WrongField(DateFieldsModel): # pragma: no cover
|
||||||
class Meta(ormar.ModelMeta):
|
class Meta(ormar.ModelMeta):
|
||||||
tablename = "wrongs"
|
tablename = "wrongs"
|
||||||
@ -146,6 +147,7 @@ def test_field_redefining_raises_error():
|
|||||||
|
|
||||||
def test_model_subclassing_non_abstract_raises_error():
|
def test_model_subclassing_non_abstract_raises_error():
|
||||||
with pytest.raises(ModelDefinitionError):
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover
|
class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover
|
||||||
class Meta(ormar.ModelMeta):
|
class Meta(ormar.ModelMeta):
|
||||||
tablename = "wrongs"
|
tablename = "wrongs"
|
||||||
@ -163,7 +165,7 @@ def test_params_are_inherited():
|
|||||||
|
|
||||||
|
|
||||||
def round_date_to_seconds(
|
def round_date_to_seconds(
|
||||||
date: datetime.datetime,
|
date: datetime.datetime,
|
||||||
) -> datetime.datetime: # pragma: no cover
|
) -> datetime.datetime: # pragma: no cover
|
||||||
if date.microsecond >= 500000:
|
if date.microsecond >= 500000:
|
||||||
date = date + datetime.timedelta(seconds=1)
|
date = date + datetime.timedelta(seconds=1)
|
||||||
@ -206,9 +208,9 @@ async def test_fields_inherited_from_mixin():
|
|||||||
|
|
||||||
sub2 = (
|
sub2 = (
|
||||||
await Subject.objects.select_related("category")
|
await Subject.objects.select_related("category")
|
||||||
.order_by("-created_date")
|
.order_by("-created_date")
|
||||||
.exclude_fields("updated_date")
|
.exclude_fields("updated_date")
|
||||||
.get()
|
.get()
|
||||||
)
|
)
|
||||||
assert round_date_to_seconds(sub2.created_date) == round_date_to_seconds(
|
assert round_date_to_seconds(sub2.created_date) == round_date_to_seconds(
|
||||||
sub.created_date
|
sub.created_date
|
||||||
@ -223,9 +225,9 @@ async def test_fields_inherited_from_mixin():
|
|||||||
|
|
||||||
sub3 = (
|
sub3 = (
|
||||||
await Subject.objects.prefetch_related("category")
|
await Subject.objects.prefetch_related("category")
|
||||||
.order_by("-created_date")
|
.order_by("-created_date")
|
||||||
.exclude_fields({"updated_date": ..., "category": {"updated_date"}})
|
.exclude_fields({"updated_date": ..., "category": {"updated_date"}})
|
||||||
.get()
|
.get()
|
||||||
)
|
)
|
||||||
assert round_date_to_seconds(sub3.created_date) == round_date_to_seconds(
|
assert round_date_to_seconds(sub3.created_date) == round_date_to_seconds(
|
||||||
sub.created_date
|
sub.created_date
|
||||||
@ -243,15 +245,19 @@ async def test_fields_inherited_from_mixin():
|
|||||||
async def test_inheritance_with_relation():
|
async def test_inheritance_with_relation():
|
||||||
async with db:
|
async with db:
|
||||||
async with db.transaction(force_rollback=True):
|
async with db.transaction(force_rollback=True):
|
||||||
sam = await Person(name='Sam').save()
|
sam = await Person(name="Sam").save()
|
||||||
joe = await Person(name='Joe').save()
|
joe = await Person(name="Joe").save()
|
||||||
await Truck(name='Shelby wanna be', max_capacity=1400, owner=sam, co_owner=joe).save()
|
await Truck(
|
||||||
|
name="Shelby wanna be", max_capacity=1400, owner=sam, co_owner=joe
|
||||||
|
).save()
|
||||||
|
|
||||||
shelby = await Truck.objects.select_related(['owner', 'co_owner']).get()
|
shelby = await Truck.objects.select_related(["owner", "co_owner"]).get()
|
||||||
assert shelby.name == 'Shelby wanna be'
|
assert shelby.name == "Shelby wanna be"
|
||||||
assert shelby.owner.name == 'Sam'
|
assert shelby.owner.name == "Sam"
|
||||||
assert shelby.co_owner.name == 'Joe'
|
assert shelby.co_owner.name == "Joe"
|
||||||
|
|
||||||
joe_check = await Person.objects.select_related('coowned_trucks').get(name='Joe')
|
joe_check = await Person.objects.select_related("coowned_trucks").get(
|
||||||
|
name="Joe"
|
||||||
|
)
|
||||||
assert joe_check.pk == joe.pk
|
assert joe_check.pk == joe.pk
|
||||||
assert joe_check.coowned_trucks[0] == shelby
|
assert joe_check.coowned_trucks[0] == shelby
|
||||||
|
|||||||
Reference in New Issue
Block a user