diff --git a/.gitignore b/.gitignore index 2b9985d..26114bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ p38venv alembic alembic.ini +build .idea .pytest_cache .mypy_cache diff --git a/docs/api/fields/base-field.md b/docs/api/fields/base-field.md index 82b99d3..b30d498 100644 --- a/docs/api/fields/base-field.md +++ b/docs/api/fields/base-field.md @@ -20,8 +20,7 @@ to pydantic field types like ConstrainedStr #### is\_valid\_uni\_relation ```python - | @classmethod - | is_valid_uni_relation(cls) -> bool + | is_valid_uni_relation() -> bool ``` Checks if field is a relation definition but only for ForeignKey relation, @@ -40,8 +39,7 @@ Model columns only. #### get\_alias ```python - | @classmethod - | get_alias(cls) -> str + | get_alias() -> str ``` Used to translate Model column names to database column names during db queries. @@ -51,75 +49,26 @@ Used to translate Model column names to database column names during db queries. `(str)`: returns custom database column name if defined by user, otherwise field name in ormar/pydantic - -#### is\_valid\_field\_info\_field + +#### get\_pydantic\_default ```python - | @classmethod - | is_valid_field_info_field(cls, field_name: str) -> bool -``` - -Checks if field belongs to pydantic FieldInfo -- used during setting default pydantic values. -Excludes defaults and alias as they are populated separately -(defaults) or not at all (alias) - -**Arguments**: - -- `field_name (str)`: field name of BaseFIeld - -**Returns**: - -`(bool)`: True if field is present on pydantic.FieldInfo - - -#### get\_base\_pydantic\_field\_info - -```python - | @classmethod - | get_base_pydantic_field_info(cls, allow_null: bool) -> FieldInfo + | get_pydantic_default() -> Dict ``` Generates base pydantic.FieldInfo with only default and optionally required to fix pydantic Json field being set to required=False. Used in an ormar Model Metaclass. -**Arguments**: - -- `allow_null (bool)`: flag if the default value can be None -or if it should be populated by pydantic Undefined - **Returns**: `(pydantic.FieldInfo)`: instance of base pydantic.FieldInfo - -#### convert\_to\_pydantic\_field\_info - -```python - | @classmethod - | convert_to_pydantic_field_info(cls, allow_null: bool = False) -> FieldInfo -``` - -Converts a BaseField into pydantic.FieldInfo -that is later easily processed by pydantic. -Used in an ormar Model Metaclass. - -**Arguments**: - -- `allow_null (bool)`: flag if the default value can be None -or if it should be populated by pydantic Undefined - -**Returns**: - -`(pydantic.FieldInfo)`: actual instance of pydantic.FieldInfo with all needed fields populated - #### default\_value ```python - | @classmethod - | default_value(cls, use_server: bool = False) -> Optional[FieldInfo] + | default_value(use_server: bool = False) -> Optional[Dict] ``` Returns a FieldInfo instance with populated default @@ -145,8 +94,7 @@ which is returning a FieldInfo instance #### get\_default ```python - | @classmethod - | get_default(cls, use_server: bool = False) -> Any + | get_default(use_server: bool = False) -> Any ``` Return default value for a field. @@ -166,8 +114,7 @@ treated as default value, default False #### has\_default ```python - | @classmethod - | has_default(cls, use_server: bool = True) -> bool + | has_default(use_server: bool = True) -> bool ``` Checks if the field has default value set. @@ -185,8 +132,7 @@ treated as default value, default False #### is\_auto\_primary\_key ```python - | @classmethod - | is_auto_primary_key(cls) -> bool + | is_auto_primary_key() -> bool ``` Checks if field is first a primary key and if it, @@ -201,8 +147,7 @@ Autoincrement primary_key is nullable/optional. #### construct\_constraints ```python - | @classmethod - | construct_constraints(cls) -> List + | construct_constraints() -> List ``` Converts list of ormar constraints into sqlalchemy ForeignKeys. @@ -217,8 +162,7 @@ And we need a new ForeignKey for subclasses of current model #### get\_column ```python - | @classmethod - | get_column(cls, name: str) -> sqlalchemy.Column + | get_column(name: str) -> sqlalchemy.Column ``` Returns definition of sqlalchemy.Column used in creation of sqlalchemy.Table. @@ -233,12 +177,28 @@ primary_key, index, unique, nullable, default and server_default. `(sqlalchemy.Column)`: actual definition of the database column as sqlalchemy requires. + +#### \_get\_encrypted\_column + +```python + | _get_encrypted_column(name: str) -> sqlalchemy.Column +``` + +Returns EncryptedString column type instead of actual column. + +**Arguments**: + +- `name (str)`: column name + +**Returns**: + +`(sqlalchemy.Column)`: newly defined column + #### expand\_relationship ```python - | @classmethod - | expand_relationship(cls, value: Any, child: Union["Model", "NewBaseModel"], to_register: bool = True) -> Any + | expand_relationship(value: Any, child: Union["Model", "NewBaseModel"], to_register: bool = True) -> Any ``` Function overwritten for relations, in basic field the value is returned as is. @@ -261,8 +221,7 @@ dict (from Model) or actual instance/list of a "Model". #### set\_self\_reference\_flag ```python - | @classmethod - | set_self_reference_flag(cls) -> None + | set_self_reference_flag() -> None ``` Sets `self_reference` to True if field to and owner are same model. @@ -275,8 +234,7 @@ Sets `self_reference` to True if field to and owner are same model. #### has\_unresolved\_forward\_refs ```python - | @classmethod - | has_unresolved_forward_refs(cls) -> bool + | has_unresolved_forward_refs() -> bool ``` Verifies if the filed has any ForwardRefs that require updating before the @@ -290,8 +248,7 @@ model can be used. #### evaluate\_forward\_ref ```python - | @classmethod - | evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None + | evaluate_forward_ref(globalns: Any, localns: Any) -> None ``` Evaluates the ForwardRef to actual Field based on global and local namespaces @@ -309,8 +266,7 @@ Evaluates the ForwardRef to actual Field based on global and local namespaces #### get\_related\_name ```python - | @classmethod - | get_related_name(cls) -> str + | get_related_name() -> str ``` Returns name to use for reverse relation. diff --git a/docs/api/fields/foreign-key.md b/docs/api/fields/foreign-key.md index 055b661..c8ae997 100644 --- a/docs/api/fields/foreign-key.md +++ b/docs/api/fields/foreign-key.md @@ -5,7 +5,7 @@ #### create\_dummy\_instance ```python -create_dummy_instance(fk: Type["Model"], pk: Any = None) -> "Model" +create_dummy_instance(fk: Type["T"], pk: Any = None) -> "T" ``` Ormar never returns you a raw data. @@ -31,7 +31,7 @@ If the nested related Models are required they are set with -1 as pk value. #### create\_dummy\_model ```python -create_dummy_model(base_model: Type["Model"], pk_field: Type[Union[BaseField, "ForeignKeyField", "ManyToManyField"]]) -> Type["BaseModel"] +create_dummy_model(base_model: Type["T"], pk_field: Union[BaseField, "ForeignKeyField", "ManyToManyField"]) -> Type["BaseModel"] ``` Used to construct a dummy pydantic model for type hints and pydantic validation. @@ -40,7 +40,7 @@ Populates only pk field and set it to desired type. **Arguments**: - `base_model (Model class)`: class of target dummy model -- `pk_field (Type[Union[BaseField, "ForeignKeyField", "ManyToManyField"]])`: ormar Field to be set on pydantic Model +- `pk_field (Union[BaseField, "ForeignKeyField", "ManyToManyField"])`: ormar Field to be set on pydantic Model **Returns**: @@ -50,7 +50,7 @@ Populates only pk field and set it to desired type. #### populate\_fk\_params\_based\_on\_to\_model ```python -populate_fk_params_based_on_to_model(to: Type["Model"], nullable: bool, onupdate: str = None, ondelete: str = None) -> Tuple[Any, List, Any] +populate_fk_params_based_on_to_model(to: Type["T"], nullable: bool, onupdate: str = None, ondelete: str = None) -> Tuple[Any, List, Any] ``` Based on target to model to which relation leads to populates the type of the @@ -69,6 +69,25 @@ How to treat child rows on delete of parent (the one where FK is defined) model. `(Tuple[Any, List, Any])`: tuple with target pydantic type, list of fk constraints and target col type + +#### validate\_not\_allowed\_fields + +```python +validate_not_allowed_fields(kwargs: Dict) -> None +``` + +Verifies if not allowed parameters are set on relation models. +Usually they are omitted later anyway but this way it's explicitly +notify the user that it's not allowed/ supported. + +**Raises**: + +- `ModelDefinitionError`: if any forbidden field is set + +**Arguments**: + +- `kwargs (Dict)`: dict of kwargs to verify passed to relation field + ## UniqueColumns Objects @@ -94,7 +113,7 @@ to produce sqlalchemy.ForeignKeys #### ForeignKey ```python -ForeignKey(to: "ToType", *, name: str = None, unique: bool = False, nullable: bool = True, related_name: str = None, virtual: bool = False, onupdate: str = None, ondelete: str = None, **kwargs: Any, ,) -> Any +ForeignKey(to: "ToType", *, name: str = None, unique: bool = False, nullable: bool = True, related_name: str = None, virtual: bool = False, onupdate: str = None, ondelete: str = None, **kwargs: Any, ,) -> "T" ``` Despite a name it's a function that returns constructed ForeignKeyField. @@ -134,8 +153,7 @@ Actual class returned from ForeignKey function call and stored in model_fields. #### get\_source\_related\_name ```python - | @classmethod - | get_source_related_name(cls) -> str + | get_source_related_name() -> str ``` Returns name to use for source relation name. @@ -150,8 +168,7 @@ It's either set as `related_name` or by default it's owner model. get_name + 's' #### get\_related\_name ```python - | @classmethod - | get_related_name(cls) -> str + | get_related_name() -> str ``` Returns name to use for reverse relation. @@ -161,12 +178,37 @@ It's either set as `related_name` or by default it's owner model. get_name + 's' `(str)`: name of the related_name or default related name. + +#### default\_target\_field\_name + +```python + | default_target_field_name() -> str +``` + +Returns default target model name on through model. + +**Returns**: + +`(str)`: name of the field + + +#### default\_source\_field\_name + +```python + | default_source_field_name() -> str +``` + +Returns default target model name on through model. + +**Returns**: + +`(str)`: name of the field + #### evaluate\_forward\_ref ```python - | @classmethod - | evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None + | evaluate_forward_ref(globalns: Any, localns: Any) -> None ``` Evaluates the ForwardRef to actual Field based on global and local namespaces @@ -184,8 +226,7 @@ Evaluates the ForwardRef to actual Field based on global and local namespaces #### \_extract\_model\_from\_sequence ```python - | @classmethod - | _extract_model_from_sequence(cls, value: List, child: "Model", to_register: bool) -> List["Model"] + | _extract_model_from_sequence(value: List, child: "Model", to_register: bool) -> List["Model"] ``` Takes a list of Models and registers them on parent. @@ -207,8 +248,7 @@ Used in reverse FK relations. #### \_register\_existing\_model ```python - | @classmethod - | _register_existing_model(cls, value: "Model", child: "Model", to_register: bool) -> "Model" + | _register_existing_model(value: "Model", child: "Model", to_register: bool) -> "Model" ``` Takes already created instance and registers it for parent. @@ -230,8 +270,7 @@ Used in reverse FK relations and normal FK for single models. #### \_construct\_model\_from\_dict ```python - | @classmethod - | _construct_model_from_dict(cls, value: dict, child: "Model", to_register: bool) -> "Model" + | _construct_model_from_dict(value: dict, child: "Model", to_register: bool) -> "Model" ``` Takes a dictionary, creates a instance and registers it for parent. @@ -254,8 +293,7 @@ Used in normal FK for dictionaries. #### \_construct\_model\_from\_pk ```python - | @classmethod - | _construct_model_from_pk(cls, value: Any, child: "Model", to_register: bool) -> "Model" + | _construct_model_from_pk(value: Any, child: "Model", to_register: bool) -> "Model" ``` Takes a pk value, creates a dummy instance and registers it for parent. @@ -277,8 +315,7 @@ Used in normal FK for dictionaries. #### register\_relation ```python - | @classmethod - | register_relation(cls, model: "Model", child: "Model") -> None + | register_relation(model: "Model", child: "Model") -> None ``` Registers relation between parent and child in relation manager. @@ -296,8 +333,7 @@ Used in Metaclass and sometimes some relations are missing #### has\_unresolved\_forward\_refs ```python - | @classmethod - | has_unresolved_forward_refs(cls) -> bool + | has_unresolved_forward_refs() -> bool ``` Verifies if the filed has any ForwardRefs that require updating before the @@ -311,8 +347,7 @@ model can be used. #### expand\_relationship ```python - | @classmethod - | expand_relationship(cls, value: Any, child: Union["Model", "NewBaseModel"], to_register: bool = True) -> Optional[Union["Model", List["Model"]]] + | expand_relationship(value: Any, child: Union["Model", "NewBaseModel"], to_register: bool = True) -> Optional[Union["Model", List["Model"]]] ``` For relations the child model is first constructed (if needed), @@ -336,8 +371,7 @@ Selects the appropriate constructor based on a passed value. #### get\_relation\_name ```python - | @classmethod - | get_relation_name(cls) -> str + | get_relation_name() -> str ``` Returns name of the relation, which can be a own name or through model @@ -351,8 +385,7 @@ names for m2m models #### get\_source\_model ```python - | @classmethod - | get_source_model(cls) -> Type["Model"] + | get_source_model() -> Type["Model"] ``` Returns model from which the relation comes -> either owner or through model diff --git a/docs/api/fields/many-to-many.md b/docs/api/fields/many-to-many.md index 89570aa..de80987 100644 --- a/docs/api/fields/many-to-many.md +++ b/docs/api/fields/many-to-many.md @@ -1,6 +1,19 @@ # fields.many\_to\_many + +#### forbid\_through\_relations + +```python +forbid_through_relations(through: Type["Model"]) -> None +``` + +Verifies if the through model does not have relations. + +**Arguments**: + +- `through (Type['Model])`: through Model to be checked + #### populate\_m2m\_params\_based\_on\_to\_model @@ -24,7 +37,7 @@ pydantic field to use and type of the target column field. #### ManyToMany ```python -ManyToMany(to: "ToType", through: Optional["ToType"] = None, *, name: str = None, unique: bool = False, virtual: bool = False, **kwargs: Any, ,) -> Any +ManyToMany(to: "ToType", through: Optional["ToType"] = None, *, name: str = None, unique: bool = False, virtual: bool = False, **kwargs: Any, ,) -> "RelationProxy[T]" ``` Despite a name it's a function that returns constructed ManyToManyField. @@ -60,8 +73,7 @@ Actual class returned from ManyToMany function call and stored in model_fields. #### get\_source\_related\_name ```python - | @classmethod - | get_source_related_name(cls) -> str + | get_source_related_name() -> str ``` Returns name to use for source relation name. @@ -72,40 +84,11 @@ It's either set as `related_name` or by default it's field name. `(str)`: name of the related_name or default related name. - -#### default\_target\_field\_name - -```python - | @classmethod - | default_target_field_name(cls) -> str -``` - -Returns default target model name on through model. - -**Returns**: - -`(str)`: name of the field - - -#### default\_source\_field\_name - -```python - | @classmethod - | default_source_field_name(cls) -> str -``` - -Returns default target model name on through model. - -**Returns**: - -`(str)`: name of the field - #### has\_unresolved\_forward\_refs ```python - | @classmethod - | has_unresolved_forward_refs(cls) -> bool + | has_unresolved_forward_refs() -> bool ``` Verifies if the filed has any ForwardRefs that require updating before the @@ -119,8 +102,7 @@ model can be used. #### evaluate\_forward\_ref ```python - | @classmethod - | evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None + | evaluate_forward_ref(globalns: Any, localns: Any) -> None ``` Evaluates the ForwardRef to actual Field based on global and local namespaces @@ -138,8 +120,7 @@ Evaluates the ForwardRef to actual Field based on global and local namespaces #### get\_relation\_name ```python - | @classmethod - | get_relation_name(cls) -> str + | get_relation_name() -> str ``` Returns name of the relation, which can be a own name or through model @@ -153,8 +134,7 @@ names for m2m models #### get\_source\_model ```python - | @classmethod - | get_source_model(cls) -> Type["Model"] + | get_source_model() -> Type["Model"] ``` Returns model from which the relation comes -> either owner or through model @@ -167,8 +147,7 @@ Returns model from which the relation comes -> either owner or through model #### create\_default\_through\_model ```python - | @classmethod - | create_default_through_model(cls) -> None + | create_default_through_model() -> None ``` Creates default empty through model if no additional fields are required. diff --git a/docs/api/models/helpers/models.md b/docs/api/models/helpers/models.md index 2537a70..328a95c 100644 --- a/docs/api/models/helpers/models.md +++ b/docs/api/models/helpers/models.md @@ -5,7 +5,7 @@ #### is\_field\_an\_forward\_ref ```python -is_field_an_forward_ref(field: Type["BaseField"]) -> bool +is_field_an_forward_ref(field: "BaseField") -> bool ``` Checks if field is a relation field and whether any of the referenced models @@ -91,7 +91,7 @@ extraction of ormar model_fields. #### group\_related\_list ```python -group_related_list(list_: List) -> Dict +group_related_list(list_: List) -> collections.OrderedDict ``` Translates the list of related strings into a dictionary. diff --git a/docs/api/models/helpers/pydantic.md b/docs/api/models/helpers/pydantic.md index 49e44a5..2788e90 100644 --- a/docs/api/models/helpers/pydantic.md +++ b/docs/api/models/helpers/pydantic.md @@ -5,7 +5,7 @@ #### create\_pydantic\_field ```python -create_pydantic_field(field_name: str, model: Type["Model"], model_field: Type["ManyToManyField"]) -> None +create_pydantic_field(field_name: str, model: Type["Model"], model_field: "ManyToManyField") -> None ``` Registers pydantic field on through model that leads to passed model @@ -38,32 +38,6 @@ field_name. Returns a pydantic field with type of field_name field type. `(pydantic.ModelField)`: newly created pydantic field - -#### populate\_default\_pydantic\_field\_value - -```python -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. - -**Arguments**: - -- `ormar_field (ormar Field)`: field to convert -- `field_name (str)`: field to convert name -- `attrs (Dict)`: current class namespace - -**Returns**: - -`(Dict)`: updated namespace dict - #### populate\_pydantic\_default\_values @@ -76,7 +50,7 @@ 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. +parameters saved. Overwrites the annotations of ormar fields to corresponding types declared on ormar fields (constructed dynamically for relations). diff --git a/docs/api/models/helpers/relations.md b/docs/api/models/helpers/relations.md index 8da7561..025356b 100644 --- a/docs/api/models/helpers/relations.md +++ b/docs/api/models/helpers/relations.md @@ -5,7 +5,7 @@ #### register\_relation\_on\_build ```python -register_relation_on_build(field: Type["ForeignKeyField"]) -> None +register_relation_on_build(field: "ForeignKeyField") -> None ``` Registers ForeignKey relation in alias_manager to set a table_prefix. @@ -23,7 +23,7 @@ aliases for proper sql joins. #### register\_many\_to\_many\_relation\_on\_build ```python -register_many_to_many_relation_on_build(field: Type["ManyToManyField"]) -> None +register_many_to_many_relation_on_build(field: "ManyToManyField") -> None ``` Registers connection between through model and both sides of the m2m relation. @@ -43,7 +43,7 @@ By default relation name is a model.name.lower(). #### expand\_reverse\_relationship ```python -expand_reverse_relationship(model_field: Type["ForeignKeyField"]) -> None +expand_reverse_relationship(model_field: "ForeignKeyField") -> None ``` If the reverse relation has not been set before it's set here. @@ -76,7 +76,7 @@ If the reverse relation has not been set before it's set here. #### register\_reverse\_model\_fields ```python -register_reverse_model_fields(model_field: Type["ForeignKeyField"]) -> None +register_reverse_model_fields(model_field: "ForeignKeyField") -> None ``` Registers reverse ForeignKey field on related model. @@ -93,7 +93,7 @@ Autogenerated reverse fields also set related_name to the original field name. #### register\_through\_shortcut\_fields ```python -register_through_shortcut_fields(model_field: Type["ManyToManyField"]) -> None +register_through_shortcut_fields(model_field: "ManyToManyField") -> None ``` Registers m2m relation through shortcut on both ends of the relation. @@ -106,7 +106,7 @@ Registers m2m relation through shortcut on both ends of the relation. #### register\_relation\_in\_alias\_manager ```python -register_relation_in_alias_manager(field: Type["ForeignKeyField"]) -> None +register_relation_in_alias_manager(field: "ForeignKeyField") -> None ``` Registers the relation (and reverse relation) in alias manager. @@ -125,7 +125,7 @@ fk - register_relation_on_build #### verify\_related\_name\_dont\_duplicate ```python -verify_related_name_dont_duplicate(related_name: str, model_field: Type["ForeignKeyField"]) -> None +verify_related_name_dont_duplicate(related_name: str, model_field: "ForeignKeyField") -> None ``` Verifies whether the used related_name (regardless of the fact if user defined or @@ -150,7 +150,7 @@ model #### reverse\_field\_not\_already\_registered ```python -reverse_field_not_already_registered(model_field: Type["ForeignKeyField"]) -> bool +reverse_field_not_already_registered(model_field: "ForeignKeyField") -> bool ``` Checks if child is already registered in parents pydantic fields. diff --git a/docs/api/models/helpers/sqlalchemy.md b/docs/api/models/helpers/sqlalchemy.md index 473b599..8738404 100644 --- a/docs/api/models/helpers/sqlalchemy.md +++ b/docs/api/models/helpers/sqlalchemy.md @@ -5,7 +5,7 @@ #### adjust\_through\_many\_to\_many\_model ```python -adjust_through_many_to_many_model(model_field: Type["ManyToManyField"]) -> None +adjust_through_many_to_many_model(model_field: "ManyToManyField") -> None ``` Registers m2m relation on through model. @@ -21,7 +21,7 @@ Sets pydantic fields with child and parent model types. #### create\_and\_append\_m2m\_fk ```python -create_and_append_m2m_fk(model: Type["Model"], model_field: Type["ManyToManyField"], field_name: str) -> None +create_and_append_m2m_fk(model: Type["Model"], model_field: "ManyToManyField", field_name: str) -> None ``` Registers sqlalchemy Column with sqlalchemy.ForeignKey leading to the model. @@ -98,6 +98,72 @@ or pkname validation fails. `(Tuple[Optional[str], List[sqlalchemy.Column]])`: pkname, list of sqlalchemy columns + +#### \_process\_fields + +```python +_process_fields(model_fields: Dict, new_model: Type["Model"]) -> Tuple[Optional[str], List[sqlalchemy.Column]] +``` + +Helper method. + +Populates pkname and columns. +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. + +Sets `owner` on each model_field as reference to newly created Model. + +**Raises**: + +- `ModelDefinitionError`: if validation of related_names fail, +or pkname validation fails. + +**Arguments**: + +- `model_fields (Dict[str, ormar.Field])`: dictionary of declared ormar model fields +- `new_model (Model class)`: + +**Returns**: + +`(Tuple[Optional[str], List[sqlalchemy.Column]])`: pkname, list of sqlalchemy columns + + +#### \_is\_through\_model\_not\_set + +```python +_is_through_model_not_set(field: "BaseField") -> bool +``` + +Alias to if check that verifies if through model was created. + +**Arguments**: + +- `field ("BaseField")`: field to check + +**Returns**: + +`(bool)`: result of the check + + +#### \_is\_db\_field + +```python +_is_db_field(field: "BaseField") -> bool +``` + +Alias to if check that verifies if field should be included in database. + +**Arguments**: + +- `field ("BaseField")`: field to check + +**Returns**: + +`(bool)`: result of the check + #### populate\_meta\_tablename\_columns\_and\_pk @@ -165,7 +231,7 @@ It populates name, metadata, columns and constraints. #### update\_column\_definition ```python -update_column_definition(model: Union[Type["Model"], Type["NewBaseModel"]], field: Type["ForeignKeyField"]) -> None +update_column_definition(model: Union[Type["Model"], Type["NewBaseModel"]], field: "ForeignKeyField") -> None ``` Updates a column with a new type column based on updated parameters in FK fields. @@ -173,7 +239,7 @@ Updates a column with a new type column based on updated parameters in FK fields **Arguments**: - `model (Type["Model"])`: model on which columns needs to be updated -- `field (Type[ForeignKeyField])`: field with column definition that requires update +- `field (ForeignKeyField)`: field with column definition that requires update **Returns**: diff --git a/docs/api/models/helpers/validation.md b/docs/api/models/helpers/validation.md index 9c2717b..5c38665 100644 --- a/docs/api/models/helpers/validation.md +++ b/docs/api/models/helpers/validation.md @@ -5,7 +5,7 @@ #### check\_if\_field\_has\_choices ```python -check_if_field_has_choices(field: Type[BaseField]) -> bool +check_if_field_has_choices(field: BaseField) -> bool ``` Checks if given field has choices populated. @@ -23,7 +23,7 @@ A if it has one, a validator for this field needs to be attached. #### convert\_choices\_if\_needed ```python -convert_choices_if_needed(field: Type["BaseField"], value: Any) -> Tuple[Any, List] +convert_choices_if_needed(field: "BaseField", value: Any) -> Tuple[Any, List] ``` Converts dates to isoformat as fastapi can check this condition in routes @@ -37,7 +37,7 @@ Converts decimal to float with given scale. **Arguments**: -- `field (Type[BaseField])`: ormar field to check with choices +- `field (BaseField)`: ormar field to check with choices - `values (Dict)`: current values of the model to verify **Returns**: @@ -48,7 +48,7 @@ Converts decimal to float with given scale. #### validate\_choices ```python -validate_choices(field: Type["BaseField"], value: Any) -> None +validate_choices(field: "BaseField", value: Any) -> None ``` Validates if given value is in provided choices. @@ -59,7 +59,7 @@ Validates if given value is in provided choices. **Arguments**: -- `field (Type[BaseField])`: field to validate +- `field (BaseField)`: field to validate - `value (Any)`: value of the field diff --git a/docs/api/models/mixins/excludable-mixin.md b/docs/api/models/mixins/excludable-mixin.md index b2ad2f6..7731a62 100644 --- a/docs/api/models/mixins/excludable-mixin.md +++ b/docs/api/models/mixins/excludable-mixin.md @@ -78,12 +78,12 @@ Primary key field is always added and cannot be excluded (will be added anyway). `(List[str])`: list of column field names or aliases - -#### \_update\_excluded\_with\_related\_not\_required + +#### \_update\_excluded\_with\_related ```python | @classmethod - | _update_excluded_with_related_not_required(cls, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None], nested: bool = False) -> Union[Set, Dict] + | _update_excluded_with_related(cls, exclude: Union[Set, Dict, None]) -> Set ``` Used during generation of the dict(). @@ -96,7 +96,6 @@ exclusion, for nested models all related models are excluded. **Arguments**: - `exclude (Union[Set, Dict, None])`: set/dict with fields to exclude -- `nested (bool)`: flag setting nested models (child of previous one, not main one) **Returns**: diff --git a/docs/api/models/mixins/merge-model-mixin.md b/docs/api/models/mixins/merge-model-mixin.md index a7ccbd9..d6e28f9 100644 --- a/docs/api/models/mixins/merge-model-mixin.md +++ b/docs/api/models/mixins/merge-model-mixin.md @@ -19,7 +19,7 @@ in the end all parent (main) models should be unique. ```python | @classmethod - | merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"] + | merge_instances_list(cls, result_rows: List["Model"]) -> List["Model"] ``` Merges a list of models into list of unique models. @@ -41,7 +41,7 @@ populated, each instance is one row in db and some models can duplicate ```python | @classmethod - | merge_two_instances(cls, one: "Model", other: "Model") -> "Model" + | merge_two_instances(cls, one: "Model", other: "Model", relation_map: Dict = None) -> "Model" ``` Merges current (other) Model and previous one (one) and returns the current @@ -51,6 +51,7 @@ If needed it's calling itself recurrently and merges also children models. **Arguments**: +- `relation_map (Dict)`: map of models relations to follow - `one (Model)`: previous model instance - `other (Model)`: current model instance @@ -58,3 +59,30 @@ If needed it's calling itself recurrently and merges also children models. `(Model)`: current Model instance with data merged from previous one. + +#### \_merge\_items\_lists + +```python + | @classmethod + | _merge_items_lists(cls, field_name: str, current_field: List, other_value: List, relation_map: Optional[Dict]) -> List +``` + +Takes two list of nested models and process them going deeper +according with the map. + +If model from one's list is in other -> they are merged with relations +to follow passed from map. + +If one's model is not in other it's simply appended to the list. + +**Arguments**: + +- `field_name (str)`: name of the current relation field +- `current_field (List[Model])`: list of nested models from one model +- `other_value (List[Model])`: list of nested models from other model +- `relation_map (Dict)`: map of relations to follow + +**Returns**: + +`(List[Model])`: merged list of models + diff --git a/docs/api/models/mixins/prefetch-query-mixin.md b/docs/api/models/mixins/prefetch-query-mixin.md index b5eb0f7..d4eadc2 100644 --- a/docs/api/models/mixins/prefetch-query-mixin.md +++ b/docs/api/models/mixins/prefetch-query-mixin.md @@ -59,7 +59,7 @@ or field name specified by related parameter. ```python | @classmethod - | get_related_field_name(cls, target_field: Type["ForeignKeyField"]) -> str + | get_related_field_name(cls, target_field: "ForeignKeyField") -> str ``` Returns name of the relation field that should be used in prefetch query. diff --git a/docs/api/models/mixins/relation-mixin.md b/docs/api/models/mixins/relation-mixin.md index 50ccb79..9859f90 100644 --- a/docs/api/models/mixins/relation-mixin.md +++ b/docs/api/models/mixins/relation-mixin.md @@ -30,7 +30,7 @@ related fields. ```python | @classmethod - | extract_related_fields(cls) -> List + | extract_related_fields(cls) -> List["ForeignKeyField"] ``` Returns List of ormar Fields for all relations declared on a model. @@ -45,7 +45,7 @@ List is cached in cls._related_fields for quicker access. ```python | @classmethod - | extract_through_names(cls) -> Set + | extract_through_names(cls) -> Set[str] ``` Extracts related fields through names which are shortcuts to through models. @@ -84,43 +84,35 @@ related fields that are not stored as foreign keys on given model. `(Set)`: set of model fields with non fk relation fields excluded - -#### \_exclude\_related\_names\_not\_required - -```python - | @classmethod - | _exclude_related_names_not_required(cls, nested: bool = False) -> Set -``` - -Returns a set of non mandatory related models field names. - -For a main model (not nested) only nullable related field names are returned, -for nested models all related models are returned. - -**Arguments**: - -- `nested (bool)`: flag setting nested models (child of previous one, not main one) - -**Returns**: - -`(Set)`: set of non mandatory related fields - #### \_iterate\_related\_models ```python | @classmethod - | _iterate_related_models(cls, visited: Set[Union[Type["Model"], Type["RelationMixin"]]] = None, source_relation: str = None, source_model: Union[Type["Model"], Type["RelationMixin"]] = None) -> List[str] + | _iterate_related_models(cls, node_list: NodeList = None, source_relation: str = None) -> List[str] ``` Iterates related models recursively to extract relation strings of nested not visited models. +**Returns**: + +`(List[str])`: list of relation strings to be passed to select_related + + +#### \_get\_final\_relations + +```python + | @staticmethod + | _get_final_relations(processed_relations: List, source_relation: Optional[str]) -> List[str] +``` + +Helper method to prefix nested relation strings with current source relation + **Arguments**: -- `visited (Set[str])`: set of already visited models +- `processed_relations (List[str])`: list of already processed relation str - `source_relation (str)`: name of the current relation -- `source_model (Type["Model"])`: model from which relation comes in nested relations **Returns**: diff --git a/docs/api/models/mixins/save-prepare-mixin.md b/docs/api/models/mixins/save-prepare-mixin.md index a3f14cb..c03daf3 100644 --- a/docs/api/models/mixins/save-prepare-mixin.md +++ b/docs/api/models/mixins/save-prepare-mixin.md @@ -33,6 +33,25 @@ Translate columns into aliases (db names). `(Dict[str, str])`: dictionary of model that is about to be saved + +#### \_remove\_not\_ormar\_fields + +```python + | @classmethod + | _remove_not_ormar_fields(cls, new_kwargs: dict) -> dict +``` + +Removes primary key for if it's nullable or autoincrement pk field, +and it's set to None. + +**Arguments**: + +- `new_kwargs (Dict[str, str])`: dictionary of model that is about to be saved + +**Returns**: + +`(Dict[str, str])`: dictionary of model that is about to be saved + #### \_remove\_pk\_from\_kwargs @@ -52,6 +71,25 @@ and it's set to None. `(Dict[str, str])`: dictionary of model that is about to be saved + +#### parse\_non\_db\_fields + +```python + | @classmethod + | parse_non_db_fields(cls, model_dict: Dict) -> Dict +``` + +Receives dictionary of model that is about to be saved and changes uuid fields +to strings in bulk_update. + +**Arguments**: + +- `model_dict (Dict)`: dictionary of model that is about to be saved + +**Returns**: + +`(Dict)`: dictionary of model that is about to be saved + #### substitute\_models\_with\_pks @@ -110,3 +148,73 @@ fields with choices set to see if the value is allowed. `(Dict)`: dictionary of model that is about to be saved + +#### \_upsert\_model + +```python + | @staticmethod + | async _upsert_model(instance: "Model", save_all: bool, previous_model: Optional["Model"], relation_field: Optional["ForeignKeyField"], update_count: int) -> int +``` + +Method updates given instance if: + +* instance is not saved or +* instance have no pk or +* save_all=True flag is set + +and instance is not __pk_only__. + +If relation leading to instance is a ManyToMany also the through model is saved + +**Arguments**: + +- `instance (Model)`: current model to upsert +- `save_all (bool)`: flag if all models should be saved or only not saved ones +- `relation_field (Optional[ForeignKeyField])`: field with relation +- `previous_model (Model)`: previous model from which method came +- `update_count (int)`: no of updated models + +**Returns**: + +`(int)`: no of updated models + + +#### \_upsert\_through\_model + +```python + | @staticmethod + | async _upsert_through_model(instance: "Model", previous_model: "Model", relation_field: "ForeignKeyField") -> None +``` + +Upsert through model for m2m relation. + +**Arguments**: + +- `instance (Model)`: current model to upsert +- `relation_field (Optional[ForeignKeyField])`: field with relation +- `previous_model (Model)`: previous model from which method came + + +#### \_update\_relation\_list + +```python + | async _update_relation_list(fields_list: Collection["ForeignKeyField"], follow: bool, save_all: bool, relation_map: Dict, update_count: int) -> int +``` + +Internal method used in save_related to follow deeper from +related models and update numbers of updated related instances. + +**Arguments**: + +- `fields_list (Collection["ForeignKeyField"])`: list of ormar fields to follow and save +- `relation_map (Dict)`: map of relations to follow +- `follow (bool)`: flag to trigger deep save - +by default only directly related models are saved +with follow=True also related models of related models are saved +- `update_count (int)`: internal parameter for recursive calls - +number of updated instances + +**Returns**: + +`(int)`: tuple of update count and visited + diff --git a/docs/api/models/model-metaclass.md b/docs/api/models/model-metaclass.md index 957a9f7..ab65c2a 100644 --- a/docs/api/models/model-metaclass.md +++ b/docs/api/models/model-metaclass.md @@ -102,7 +102,7 @@ Updates Meta parameters in child from parent if needed. #### copy\_and\_replace\_m2m\_through\_model ```python -copy_and_replace_m2m_through_model(field: Type[ManyToManyField], field_name: str, table_name: str, parent_fields: Dict, attrs: Dict, meta: ModelMeta, base_class: Type["Model"]) -> None +copy_and_replace_m2m_through_model(field: ManyToManyField, field_name: str, table_name: str, parent_fields: Dict, attrs: Dict, meta: ModelMeta, base_class: Type["Model"]) -> None ``` Clones class with Through model for m2m relations, appends child name to the name @@ -119,7 +119,7 @@ Removes the original sqlalchemy table from metadata if it was not removed. **Arguments**: - `base_class (Type["Model"])`: base class model -- `field (Type[ManyToManyField])`: field with relations definition +- `field (ManyToManyField)`: field with relations definition - `field_name (str)`: name of the relation field - `table_name (str)`: name of the table - `parent_fields (Dict)`: dictionary of fields to copy to new models from parent @@ -130,9 +130,7 @@ Removes the original sqlalchemy table from metadata if it was not removed. #### copy\_data\_from\_parent\_model ```python -copy_data_from_parent_model(base_class: Type["Model"], curr_class: type, attrs: Dict, model_fields: Dict[ - str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]] - ]) -> Tuple[Dict, Dict] +copy_data_from_parent_model(base_class: Type["Model"], curr_class: type, attrs: Dict, model_fields: Dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]]) -> Tuple[Dict, Dict] ``` Copy the key parameters [databse, metadata, property_fields and constraints] @@ -162,9 +160,7 @@ Since relation fields requires different related_name for different children #### extract\_from\_parents\_definition ```python -extract_from_parents_definition(base_class: type, curr_class: type, attrs: Dict, model_fields: Dict[ - str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]] - ]) -> Tuple[Dict, Dict] +extract_from_parents_definition(base_class: type, curr_class: type, attrs: Dict, model_fields: Dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]]) -> Tuple[Dict, Dict] ``` Extracts fields from base classes if they have valid oramr fields. diff --git a/docs/api/models/model-row.md b/docs/api/models/model-row.md index 60f0b3a..3483d50 100644 --- a/docs/api/models/model-row.md +++ b/docs/api/models/model-row.md @@ -13,7 +13,7 @@ class ModelRow(NewBaseModel) ```python | @classmethod - | from_row(cls, row: sqlalchemy.engine.ResultProxy, source_model: Type["Model"], select_related: List = None, related_models: Any = None, related_field: Type["ForeignKeyField"] = None, excludable: ExcludableItems = None, current_relation_str: str = "", proxy_source_model: Optional[Type["Model"]] = None) -> Optional["Model"] + | from_row(cls, row: sqlalchemy.engine.ResultProxy, source_model: Type["Model"], select_related: List = None, related_models: Any = None, related_field: "ForeignKeyField" = None, excludable: ExcludableItems = None, current_relation_str: str = "", proxy_source_model: Optional[Type["Model"]] = None, used_prefixes: List[str] = None) -> Optional["Model"] ``` Model method to convert raw sql row from database into ormar.Model instance. @@ -30,6 +30,7 @@ nested models in result. **Arguments**: +- `used_prefixes (List[str])`: list of already extracted prefixes - `proxy_source_model (Optional[Type["ModelRow"]])`: source model from which querysetproxy is constructed - `excludable (ExcludableItems)`: structure of fields to include and exclude - `current_relation_str (str)`: name of the relation field @@ -37,18 +38,37 @@ nested models in result. - `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database - `select_related (List)`: list of names of related models fetched from database - `related_models (Union[List, Dict])`: list or dict of related models -- `related_field (Type[ForeignKeyField])`: field with relation declaration +- `related_field (ForeignKeyField)`: field with relation declaration **Returns**: `(Optional[Model])`: returns model if model is populated from database + +#### \_process\_table\_prefix + +```python + | @classmethod + | _process_table_prefix(cls, source_model: Type["Model"], current_relation_str: str, related_field: "ForeignKeyField", used_prefixes: List[str]) -> str +``` + +**Arguments**: + +- `source_model (Type[Model])`: model on which relation was defined +- `current_relation_str (str)`: current relation string +- `related_field ("ForeignKeyField")`: field with relation declaration +- `used_prefixes (List[str])`: list of already extracted prefixes + +**Returns**: + +`(str)`: table_prefix to use + #### \_populate\_nested\_models\_from\_row ```python | @classmethod - | _populate_nested_models_from_row(cls, item: dict, row: sqlalchemy.engine.ResultProxy, source_model: Type["Model"], related_models: Any, excludable: ExcludableItems, table_prefix: str, current_relation_str: str = None, proxy_source_model: Type["Model"] = None) -> dict + | _populate_nested_models_from_row(cls, item: dict, row: sqlalchemy.engine.ResultProxy, source_model: Type["Model"], related_models: Any, excludable: ExcludableItems, table_prefix: str, used_prefixes: List[str], current_relation_str: str = None, proxy_source_model: Type["Model"] = None) -> dict ``` Traverses structure of related models and populates the nested models @@ -75,12 +95,48 @@ instances. In the end those instances are added to the final model dictionary. `(Dict)`: dictionary with keys corresponding to model fields names and values are database values - -#### populate\_through\_instance + +#### \_process\_remainder\_and\_relation\_string + +```python + | @staticmethod + | _process_remainder_and_relation_string(related_models: Union[Dict, List], current_relation_str: Optional[str], related: str) -> Tuple[str, Optional[Union[Dict, List]]] +``` + +Process remainder models and relation string + +**Arguments**: + +- `related_models (Union[Dict, List])`: list or dict of related models +- `current_relation_str (Optional[str])`: current relation string +- `related (str)`: name of the relation + + +#### \_populate\_through\_instance ```python | @classmethod - | populate_through_instance(cls, row: sqlalchemy.engine.ResultProxy, through_name: str, related: str, excludable: ExcludableItems) -> "ModelRow" + | _populate_through_instance(cls, row: sqlalchemy.engine.ResultProxy, item: Dict, related: str, excludable: ExcludableItems, child: "Model", proxy_source_model: Optional[Type["Model"]]) -> None +``` + +Populates the through model on reverse side of current query. +Normally it's child class, unless the query is from queryset. + +**Arguments**: + +- `row (sqlalchemy.engine.ResultProxy)`: row from db result +- `item (Dict)`: parent item dict +- `related (str)`: current relation name +- `excludable (ExcludableItems)`: structure of fields to include and exclude +- `child ("Model")`: child item of parent +- `proxy_source_model (Type["Model"])`: source model from which querysetproxy is constructed + + +#### \_create\_through\_instance + +```python + | @classmethod + | _create_through_instance(cls, row: sqlalchemy.engine.ResultProxy, through_name: str, related: str, excludable: ExcludableItems) -> "ModelRow" ``` Initialize the through model from db row. diff --git a/docs/api/models/model.md b/docs/api/models/model.md index facb8f4..a086dc4 100644 --- a/docs/api/models/model.md +++ b/docs/api/models/model.md @@ -12,7 +12,7 @@ class Model(ModelRow) #### upsert ```python - | async upsert(**kwargs: Any) -> "Model" + | async upsert(**kwargs: Any) -> T ``` Performs either a save or an update depending on the presence of the pk. @@ -31,7 +31,7 @@ For save kwargs are ignored, used only in update if provided. #### save ```python - | async save() -> "Model" + | async save() -> T ``` Performs a save of given Model instance. @@ -60,7 +60,7 @@ Sets model save status to True. #### save\_related ```python - | async save_related(follow: bool = False, visited: Set = None, update_count: int = 0) -> int + | async save_related(follow: bool = False, save_all: bool = False, relation_map: Dict = None, exclude: Union[Set, Dict] = None, update_count: int = 0, previous_model: "Model" = None, relation_field: Optional["ForeignKeyField"] = None) -> int ``` Triggers a upsert method on all related models @@ -79,10 +79,14 @@ Nested relations of those kind need to be persisted manually. **Arguments**: +- `relation_field (Optional[ForeignKeyField])`: field with relation leading to this model +- `previous_model (Model)`: previous model from which method came +- `exclude (Union[Set, Dict])`: items to exclude during saving of relations +- `relation_map (Dict)`: map of relations to follow +- `save_all (bool)`: flag if all models should be saved or only not saved ones - `follow (bool)`: flag to trigger deep save - by default only directly related models are saved with follow=True also related models of related models are saved -- `visited (Set)`: internal parameter for recursive calls - already visited models - `update_count (int)`: internal parameter for recursive calls - number of updated instances @@ -90,36 +94,11 @@ number of updated instances `(int)`: number of updated/saved models - -#### \_update\_and\_follow - -```python - | @staticmethod - | async _update_and_follow(rel: "Model", follow: bool, visited: Set, update_count: int) -> Tuple[int, Set] -``` - -Internal method used in save_related to follow related models and update numbers -of updated related instances. - -**Arguments**: - -- `rel (Model)`: Model to follow -- `follow (bool)`: flag to trigger deep save - -by default only directly related models are saved -with follow=True also related models of related models are saved -- `visited (Set)`: internal parameter for recursive calls - already visited models -- `update_count (int)`: internal parameter for recursive calls - -number of updated instances - -**Returns**: - -`(Tuple[int, Set])`: tuple of update count and visited - #### update ```python - | async update(**kwargs: Any) -> "Model" + | async update(_columns: List[str] = None, **kwargs: Any) -> T ``` Performs update of Model instance in the database. @@ -129,14 +108,15 @@ Sends pre_update and post_update signals. Sets model save status to True. +**Arguments**: + +- `_columns (List)`: list of columns to update, if None all are updated +- `kwargs (Any)`: list of fields to update as field=value pairs + **Raises**: - `ModelPersistenceError`: If the pk column is not set -**Arguments**: - -- `kwargs (Any)`: list of fields to update as field=value pairs - **Returns**: `(Model)`: updated Model @@ -166,7 +146,7 @@ or update and the Model will be saved in database again. #### load ```python - | async load() -> "Model" + | async load() -> T ``` Allow to refresh existing Models fields from database. @@ -185,7 +165,7 @@ Does NOT refresh the related models fields if they were loaded before. #### load\_all ```python - | async load_all(follow: bool = False, exclude: Union[List, str, Set, Dict] = None) -> "Model" + | async load_all(follow: bool = False, exclude: Union[List, str, Set, Dict] = None, order_by: Union[List, str] = None) -> T ``` Allow to refresh existing Models fields from database. @@ -203,17 +183,18 @@ follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X will load second Model A but will never follow into Model X. Nested relations of those kind need to be loaded manually. -**Raises**: - -- `NoMatch`: If given pk is not found in database. - **Arguments**: -- `exclude ()`: +- `order_by (Union[List, str])`: columns by which models should be sorted +- `exclude (Union[List, str, Set, Dict])`: related models to exclude - `follow (bool)`: flag to trigger deep save - by default only directly related models are saved with follow=True also related models of related models are saved +**Raises**: + +- `NoMatch`: If given pk is not found in database. + **Returns**: `(Model)`: reloaded Model diff --git a/docs/api/models/new-basemodel.md b/docs/api/models/new-basemodel.md index 88b4ed7..3c5cdb1 100644 --- a/docs/api/models/new-basemodel.md +++ b/docs/api/models/new-basemodel.md @@ -364,7 +364,7 @@ Returns related field names applying on them include and exclude set. ```python | @staticmethod - | _extract_nested_models_from_list(models: MutableSequence, include: Union[Set, Dict, None], exclude: Union[Set, Dict, None]) -> List + | _extract_nested_models_from_list(relation_map: Dict, models: MutableSequence, include: Union[Set, Dict, None], exclude: Union[Set, Dict, None]) -> List ``` Converts list of models into list of dictionaries. @@ -383,7 +383,7 @@ Converts list of models into list of dictionaries. #### \_skip\_ellipsis ```python - | _skip_ellipsis(items: Union[Set, Dict, None], key: str) -> Union[Set, Dict, None] + | _skip_ellipsis(items: Union[Set, Dict, None], key: str, default_return: Any = None) -> Union[Set, Dict, None] ``` Helper to traverse the include/exclude dictionaries. @@ -399,11 +399,25 @@ and not the actual set/dict with fields names. `(Union[Set, Dict, None])`: nested value of the items + +#### \_convert\_all + +```python + | _convert_all(items: Union[Set, Dict, None]) -> Union[Set, Dict, None] +``` + +Helper to convert __all__ pydantic special index to ormar which does not +support index based exclusions. + +**Arguments**: + +- `items (Union[Set, Dict, None])`: current include/exclude value + #### \_extract\_nested\_models ```python - | _extract_nested_models(nested: bool, dict_instance: Dict, include: Optional[Dict], exclude: Optional[Dict]) -> Dict + | _extract_nested_models(relation_map: Dict, dict_instance: Dict, include: Optional[Dict], exclude: Optional[Dict]) -> Dict ``` Traverse nested models and converts them into dictionaries. @@ -424,7 +438,7 @@ Calls itself recursively if needed. #### dict ```python - | dict(*, include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None, by_alias: bool = False, skip_defaults: bool = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, nested: bool = False) -> "DictStrAny" + | dict(*, include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None, by_alias: bool = False, skip_defaults: bool = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, relation_map: Dict = None) -> "DictStrAny" ``` Generate a dictionary representation of the model, @@ -443,7 +457,7 @@ Additionally fields decorated with @property_field are also added. - `exclude_unset (bool)`: flag to exclude not set values - passed to pydantic - `exclude_defaults (bool)`: flag to exclude default values - passed to pydantic - `exclude_none (bool)`: flag to exclude None values - passed to pydantic -- `nested (bool)`: flag if the current model is nested +- `relation_map (Dict)`: map of the relations to follow to avoid circural deps **Returns**: @@ -536,14 +550,14 @@ That includes own non-relational fields ang foreign key fields. #### get\_relation\_model\_id ```python - | get_relation_model_id(target_field: Type["BaseField"]) -> Optional[int] + | get_relation_model_id(target_field: "BaseField") -> Optional[int] ``` Returns an id of the relation side model to use in prefetch query. **Arguments**: -- `target_field (Type["BaseField"])`: field with relation definition +- `target_field ("BaseField")`: field with relation definition **Returns**: diff --git a/docs/api/models/traversible.md b/docs/api/models/traversible.md new file mode 100644 index 0000000..f83c91f --- /dev/null +++ b/docs/api/models/traversible.md @@ -0,0 +1,78 @@ + +# models.traversible + + +## NodeList Objects + +```python +class NodeList() +``` + +Helper class that helps with iterating nested models + + +#### add + +```python + | add(node_class: Type["RelationMixin"], relation_name: str = None, parent_node: "Node" = None) -> "Node" +``` + +Adds new Node or returns the existing one + +**Arguments**: + +- `node_class (ormar.models.metaclass.ModelMetaclass)`: Model in current node +- `relation_name (str)`: name of the current relation +- `parent_node (Optional[Node])`: parent node + +**Returns**: + +`(Node)`: returns new or already existing node + + +#### find + +```python + | find(node_class: Type["RelationMixin"], relation_name: Optional[str] = None, parent_node: "Node" = None) -> Optional["Node"] +``` + +Searches for existing node with given parameters + +**Arguments**: + +- `node_class (ormar.models.metaclass.ModelMetaclass)`: Model in current node +- `relation_name (str)`: name of the current relation +- `parent_node (Optional[Node])`: parent node + +**Returns**: + +`(Optional[Node])`: returns already existing node or None + + +## Node Objects + +```python +class Node() +``` + + +#### visited + +```python + | visited(relation_name: str) -> bool +``` + +Checks if given relation was already visited. + +Relation was visited if it's name is in current node children. + +Relation was visited if one of the parent node had the same Model class + +**Arguments**: + +- `relation_name (str)`: name of relation + +**Returns**: + +`(bool)`: result of the check + diff --git a/docs/api/query-set/clause.md b/docs/api/query-set/clause.md index 53bd55f..0efe18b 100644 --- a/docs/api/query-set/clause.md +++ b/docs/api/query-set/clause.md @@ -1,6 +1,115 @@ # queryset.clause + +## FilterGroup Objects + +```python +class FilterGroup() +``` + +Filter groups are used in complex queries condition to group and and or +clauses in where condition + + +#### resolve + +```python + | resolve(model_cls: Type["Model"], select_related: List = None, filter_clauses: List = None) -> Tuple[List[FilterAction], List[str]] +``` + +Resolves the FilterGroups actions to use proper target model, replace +complex relation prefixes if needed and nested groups also resolved. + +**Arguments**: + +- `model_cls (Type["Model"])`: model from which the query is run +- `select_related (List[str])`: list of models to join +- `filter_clauses (List[FilterAction])`: list of filter conditions + +**Returns**: + +`(Tuple[List[FilterAction], List[str]])`: list of filter conditions and select_related list + + +#### \_iter + +```python + | _iter() -> Generator +``` + +Iterates all actions in a tree + +**Returns**: + +`(Generator)`: generator yielding from own actions and nested groups + + +#### \_get\_text\_clauses + +```python + | _get_text_clauses() -> List[sqlalchemy.sql.expression.TextClause] +``` + +Helper to return list of text queries from actions and nested groups + +**Returns**: + +`(List[sqlalchemy.sql.elements.TextClause])`: list of text queries from actions and nested groups + + +#### get\_text\_clause + +```python + | get_text_clause() -> sqlalchemy.sql.expression.TextClause +``` + +Returns all own actions and nested groups conditions compiled and joined +inside parentheses. +Escapes characters if it's required. +Substitutes values of the models if value is a ormar Model with its pk value. +Compiles the clause. + +**Returns**: + +`(sqlalchemy.sql.elements.TextClause)`: complied and escaped clause + + +#### or\_ + +```python +or_(*args: FilterGroup, **kwargs: Any) -> FilterGroup +``` + +Construct or filter from nested groups and keyword arguments + +**Arguments**: + +- `args (Tuple[FilterGroup])`: nested filter groups +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup ready to be resolved + + +#### and\_ + +```python +and_(*args: FilterGroup, **kwargs: Any) -> FilterGroup +``` + +Construct and filter from nested groups and keyword arguments + +**Arguments**: + +- `args (Tuple[FilterGroup])`: nested filter groups +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup ready to be resolved + ## QueryClause Objects @@ -14,7 +123,7 @@ Constructs FilterActions from strings passed as arguments #### prepare\_filter ```python - | prepare_filter(**kwargs: Any) -> Tuple[List[FilterAction], List[str]] + | prepare_filter(_own_only: bool = False, **kwargs: Any) -> Tuple[List[FilterAction], List[str]] ``` Main external access point that processes the clauses into sqlalchemy text @@ -23,6 +132,7 @@ mentioned in select_related strings but not included in select_related. **Arguments**: +- `_own_only ()`: - `kwargs (Any)`: key, value pair with column names and values **Returns**: @@ -33,7 +143,7 @@ mentioned in select_related strings but not included in select_related. #### \_populate\_filter\_clauses ```python - | _populate_filter_clauses(**kwargs: Any) -> Tuple[List[FilterAction], List[str]] + | _populate_filter_clauses(_own_only: bool, **kwargs: Any) -> Tuple[List[FilterAction], List[str]] ``` Iterates all clauses and extracts used operator and field from related @@ -104,3 +214,16 @@ present in alias_manager. `(List[FilterAction])`: list of actions with aliases changed if needed + +#### \_verify\_prefix\_and\_switch + +```python + | _verify_prefix_and_switch(action: "FilterAction") -> None +``` + +Helper to switch prefix to complex relation one if required + +**Arguments**: + +- `action (ormar.queryset.actions.filter_action.FilterAction)`: action to switch prefix in + diff --git a/docs/api/query-set/field-accessor.md b/docs/api/query-set/field-accessor.md new file mode 100644 index 0000000..8ef5e14 --- /dev/null +++ b/docs/api/query-set/field-accessor.md @@ -0,0 +1,359 @@ + +# queryset.field\_accessor + + +## FieldAccessor Objects + +```python +class FieldAccessor() +``` + +Helper to access ormar fields directly from Model class also for nested +models attributes. + + +#### \_\_bool\_\_ + +```python + | __bool__() -> bool +``` + +Hack to avoid pydantic name check from parent model, returns false + +**Returns**: + +`(bool)`: False + + +#### \_\_getattr\_\_ + +```python + | __getattr__(item: str) -> Any +``` + +Accessor return new accessor for each field and nested models. +Thanks to that operator overload is possible to use in filter. + +**Arguments**: + +- `item (str)`: attribute name + +**Returns**: + +`(ormar.queryset.field_accessor.FieldAccessor)`: FieldAccessor for field or nested model + + +#### \_\_eq\_\_ + +```python + | __eq__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column = ` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### \_\_ge\_\_ + +```python + | __ge__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column >= ` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### \_\_gt\_\_ + +```python + | __gt__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column > ` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### \_\_le\_\_ + +```python + | __le__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column <= ` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### \_\_lt\_\_ + +```python + | __lt__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column < ` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### \_\_mod\_\_ + +```python + | __mod__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column LIKE '%%'` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### \_\_lshift\_\_ + +```python + | __lshift__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column IN (, ,...)` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### \_\_rshift\_\_ + +```python + | __rshift__(other: Any) -> FilterGroup +``` + +overloaded to work as sql `column IS NULL` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### in\_ + +```python + | in_(other: Any) -> FilterGroup +``` + +works as sql `column IN (, ,...)` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### iexact + +```python + | iexact(other: Any) -> FilterGroup +``` + +works as sql `column = ` case-insensitive + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### contains + +```python + | contains(other: Any) -> FilterGroup +``` + +works as sql `column LIKE '%%'` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### icontains + +```python + | icontains(other: Any) -> FilterGroup +``` + +works as sql `column LIKE '%%'` case-insensitive + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### startswith + +```python + | startswith(other: Any) -> FilterGroup +``` + +works as sql `column LIKE '%'` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### istartswith + +```python + | istartswith(other: Any) -> FilterGroup +``` + +works as sql `column LIKE '%'` case-insensitive + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### endswith + +```python + | endswith(other: Any) -> FilterGroup +``` + +works as sql `column LIKE '%'` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### iendswith + +```python + | iendswith(other: Any) -> FilterGroup +``` + +works as sql `column LIKE '%'` case-insensitive + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### isnull + +```python + | isnull(other: Any) -> FilterGroup +``` + +works as sql `column IS NULL` or `IS NOT NULL` + +**Arguments**: + +- `other (str)`: value to check agains operator + +**Returns**: + +`(ormar.queryset.clause.FilterGroup)`: FilterGroup for operator + + +#### asc + +```python + | asc() -> OrderAction +``` + +works as sql `column asc` + +**Returns**: + +`(ormar.queryset.actions.OrderGroup)`: OrderGroup for operator + + +#### desc + +```python + | desc() -> OrderAction +``` + +works as sql `column desc` + +**Returns**: + +`(ormar.queryset.actions.OrderGroup)`: OrderGroup for operator + diff --git a/docs/api/query-set/join.md b/docs/api/query-set/join.md index 9a8f898..d6e7ce4 100644 --- a/docs/api/query-set/join.md +++ b/docs/api/query-set/join.md @@ -27,7 +27,7 @@ Shortcut for ormar's model AliasManager stored on Meta. ```python | @property - | to_table() -> str + | to_table() -> sqlalchemy.Table ``` Shortcut to table name of the next model @@ -172,6 +172,36 @@ Updates the used aliases list directly. Process order_by causes for non m2m relations. + +#### \_verify\_allowed\_order\_field + +```python + | _verify_allowed_order_field(order_by: str) -> None +``` + +Verifies if proper field string is used. + +**Arguments**: + +- `order_by (str)`: string with order by definition + + +#### \_get\_alias\_and\_model + +```python + | _get_alias_and_model(order_by: str) -> Tuple[str, Type["Model"]] +``` + +Returns proper model and alias to be applied in the clause. + +**Arguments**: + +- `order_by (str)`: string with order by definition + +**Returns**: + +`(Tuple[str, Type["Model"]])`: alias and model to be used in clause + #### \_get\_order\_bys diff --git a/docs/api/query-set/prefetch-query.md b/docs/api/query-set/prefetch-query.md index ff0a64c..0ab7ce2 100644 --- a/docs/api/query-set/prefetch-query.md +++ b/docs/api/query-set/prefetch-query.md @@ -241,7 +241,7 @@ Calls itself recurrently to extract deeper nested relations of related model. #### \_run\_prefetch\_query ```python - | async _run_prefetch_query(target_field: Type["BaseField"], excludable: "ExcludableItems", filter_clauses: List, related_field_name: str) -> Tuple[str, str, List] + | async _run_prefetch_query(target_field: "BaseField", excludable: "ExcludableItems", filter_clauses: List, related_field_name: str) -> Tuple[str, str, List] ``` Actually runs the queries against the database and populates the raw response @@ -252,7 +252,7 @@ models. **Arguments**: -- `target_field (Type["BaseField"])`: ormar field with relation definition +- `target_field ("BaseField")`: ormar field with relation definition - `filter_clauses (List[sqlalchemy.sql.elements.TextClause])`: list of clauses, actually one clause with ids of relation **Returns**: @@ -283,14 +283,14 @@ deeper on related model and already loaded in select related query. #### \_update\_already\_loaded\_rows ```python - | _update_already_loaded_rows(target_field: Type["BaseField"], prefetch_dict: Dict, orders_by: Dict) -> None + | _update_already_loaded_rows(target_field: "BaseField", prefetch_dict: Dict, orders_by: Dict) -> None ``` Updates models that are already loaded, usually children of children. **Arguments**: -- `target_field (Type["BaseField"])`: ormar field with relation definition +- `target_field ("BaseField")`: ormar field with relation definition - `prefetch_dict (Dict)`: dictionaries of related models to prefetch - `orders_by (Dict)`: dictionary of order by clauses by model @@ -298,7 +298,7 @@ Updates models that are already loaded, usually children of children. #### \_populate\_rows ```python - | _populate_rows(rows: List, target_field: Type["ForeignKeyField"], parent_model: Type["Model"], table_prefix: str, exclude_prefix: str, excludable: "ExcludableItems", prefetch_dict: Dict, orders_by: Dict) -> None + | _populate_rows(rows: List, target_field: "ForeignKeyField", parent_model: Type["Model"], table_prefix: str, exclude_prefix: str, excludable: "ExcludableItems", prefetch_dict: Dict, orders_by: Dict) -> None ``` Instantiates children models extracted from given relation. @@ -314,7 +314,7 @@ and set on the parent model after sorting if needed. - `excludable (ExcludableItems)`: structure of fields to include and exclude - `rows (List[sqlalchemy.engine.result.RowProxy])`: raw sql response from the prefetch query -- `target_field (Type["BaseField"])`: field with relation definition from parent model +- `target_field ("BaseField")`: field with relation definition from parent model - `parent_model (Type[Model])`: model with relation definition - `table_prefix (str)`: prefix of the target table from current relation - `prefetch_dict (Dict)`: dictionaries of related models to prefetch diff --git a/docs/api/query-set/query-set.md b/docs/api/query-set/query-set.md index 34f0945..d259202 100644 --- a/docs/api/query-set/query-set.md +++ b/docs/api/query-set/query-set.md @@ -5,7 +5,7 @@ ## QuerySet Objects ```python -class QuerySet() +class QuerySet(Generic[T]) ``` Main class to perform database queries, exposed on each model as objects attribute. @@ -29,7 +29,7 @@ Shortcut to model class Meta set on QuerySet model. ```python | @property - | model() -> Type["Model"] + | model() -> Type["T"] ``` Shortcut to model class set on QuerySet. @@ -52,7 +52,7 @@ all not passed params are taken from current values. #### \_prefetch\_related\_models ```python - | async _prefetch_related_models(models: Sequence[Optional["Model"]], rows: List) -> Sequence[Optional["Model"]] + | async _prefetch_related_models(models: List[Optional["T"]], rows: List) -> List[Optional["T"]] ``` Performs prefetch query for selected models names. @@ -70,7 +70,7 @@ Performs prefetch query for selected models names. #### \_process\_query\_result\_rows ```python - | _process_query_result_rows(rows: List) -> Sequence[Optional["Model"]] + | _process_query_result_rows(rows: List) -> List[Optional["T"]] ``` Process database rows and initialize ormar Model from each of the rows. @@ -83,12 +83,29 @@ Process database rows and initialize ormar Model from each of the rows. `(List[Model])`: list of models + +#### \_resolve\_filter\_groups + +```python + | _resolve_filter_groups(groups: Any) -> Tuple[List[FilterGroup], List[str]] +``` + +Resolves filter groups to populate FilterAction params in group tree. + +**Arguments**: + +- `groups (Any)`: tuple of FilterGroups + +**Returns**: + +`(Tuple[List[FilterGroup], List[str]])`: list of resolver groups + #### check\_single\_result\_rows\_count ```python | @staticmethod - | check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None + | check_single_result_rows_count(rows: Sequence[Optional["T"]]) -> None ``` Verifies if the result has one and only one row. @@ -149,7 +166,7 @@ If any of the params is not passed the QuerySet own value is used. #### filter ```python - | filter(_exclude: bool = False, **kwargs: Any) -> "QuerySet" + | filter(*args: Any, *, _exclude: bool = False, **kwargs: Any) -> "QuerySet[T]" ``` Allows you to filter by any `Model` attribute/field @@ -162,6 +179,8 @@ You can use special filter suffix to change the filter operands: * contains - like `album__name__contains='Mal'` (sql like) * icontains - like `album__name__icontains='mal'` (sql like case insensitive) * in - like `album__name__in=['Malibu', 'Barclay']` (sql in) +* isnull - like `album__name__isnull=True` (sql is null) +(isnotnull `album__name__isnull=False` (sql is not null)) * gt - like `position__gt=3` (sql >) * gte - like `position__gte=3` (sql >=) * lt - like `position__lt=3` (sql <) @@ -184,7 +203,7 @@ You can use special filter suffix to change the filter operands: #### exclude ```python - | exclude(**kwargs: Any) -> "QuerySet" + | exclude(*args: Any, **kwargs: Any) -> "QuerySet[T]" ``` Works exactly the same as filter and all modifiers (suffixes) are the same, @@ -211,7 +230,7 @@ becomes a union of conditions. #### select\_related ```python - | select_related(related: Union[List, str]) -> "QuerySet" + | select_related(related: Union[List, str]) -> "QuerySet[T]" ``` Allows to prefetch related models during the same query. @@ -232,11 +251,40 @@ To chain related `Models` relation use double underscores between names. `(QuerySet)`: QuerySet + +#### select\_all + +```python + | select_all(follow: bool = False) -> "QuerySet[T]" +``` + +By default adds only directly related models. + +If follow=True is set it adds also related models of related models. + +To not get stuck in an infinite loop as related models also keep a relation +to parent model visited models set is kept. + +That way already visited models that are nested are loaded, but the load do not +follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X +will load second Model A but will never follow into Model X. +Nested relations of those kind need to be loaded manually. + +**Arguments**: + +- `follow (bool)`: flag to trigger deep save - +by default only directly related models are saved +with follow=True also related models of related models are saved + +**Returns**: + +`(Model)`: reloaded Model + #### prefetch\_related ```python - | prefetch_related(related: Union[List, str]) -> "QuerySet" + | prefetch_related(related: Union[List, str]) -> "QuerySet[T]" ``` Allows to prefetch related models during query - but opposite to @@ -262,7 +310,7 @@ To chain related `Models` relation use double underscores between names. #### fields ```python - | fields(columns: Union[List, str, Set, Dict], _is_exclude: bool = False) -> "QuerySet" + | fields(columns: Union[List, str, Set, Dict], _is_exclude: bool = False) -> "QuerySet[T]" ``` With `fields()` you can select subset of model columns to limit the data load. @@ -314,7 +362,7 @@ To include whole nested model specify model related field name and ellipsis. #### exclude\_fields ```python - | exclude_fields(columns: Union[List, str, Set, Dict]) -> "QuerySet" + | exclude_fields(columns: Union[List, str, Set, Dict]) -> "QuerySet[T]" ``` With `exclude_fields()` you can select subset of model columns that will @@ -349,7 +397,7 @@ if explicitly excluded. #### order\_by ```python - | order_by(columns: Union[List, str]) -> "QuerySet" + | order_by(columns: Union[List, str, OrderAction]) -> "QuerySet[T]" ``` With `order_by()` you can order the results from database based on your @@ -413,6 +461,62 @@ Returns number of rows matching the given criteria `(int)`: number of rows + +#### max + +```python + | async max(columns: Union[str, List[str]]) -> Any +``` + +Returns max value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(Any)`: max value of column(s) + + +#### min + +```python + | async min(columns: Union[str, List[str]]) -> Any +``` + +Returns min value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(Any)`: min value of column(s) + + +#### sum + +```python + | async sum(columns: Union[str, List[str]]) -> Any +``` + +Returns sum value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(int)`: sum value of columns + + +#### avg + +```python + | async avg(columns: Union[str, List[str]]) -> Any +``` + +Returns avg value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(Union[int, float, List])`: avg value of columns + #### update @@ -438,7 +542,7 @@ each=True flag to affect whole table. #### delete ```python - | async delete(each: bool = False, **kwargs: Any) -> int + | async delete(*args: Any, *, each: bool = False, **kwargs: Any) -> int ``` Deletes from the model table after applying the filters from kwargs. @@ -459,7 +563,7 @@ each=True flag to affect whole table. #### paginate ```python - | paginate(page: int, page_size: int = 20) -> "QuerySet" + | paginate(page: int, page_size: int = 20) -> "QuerySet[T]" ``` You can paginate the result which is a combination of offset and limit clauses. @@ -478,7 +582,7 @@ Limit is set to page size and offset is set to (page-1) * page_size. #### limit ```python - | limit(limit_count: int, limit_raw_sql: bool = None) -> "QuerySet" + | limit(limit_count: int, limit_raw_sql: bool = None) -> "QuerySet[T]" ``` You can limit the results to desired number of parent models. @@ -499,7 +603,7 @@ models use the `limit_raw_sql` parameter flag, and set it to `True`. #### offset ```python - | offset(offset: int, limit_raw_sql: bool = None) -> "QuerySet" + | offset(offset: int, limit_raw_sql: bool = None) -> "QuerySet[T]" ``` You can also offset the results by desired number of main models. @@ -520,7 +624,7 @@ models use the `limit_raw_sql` parameter flag, and set it to `True`. #### first ```python - | async first(**kwargs: Any) -> "Model" + | async first(*args: Any, **kwargs: Any) -> "T" ``` Gets the first row from the db ordered by primary key column ascending. @@ -538,11 +642,34 @@ Gets the first row from the db ordered by primary key column ascending. `(Model)`: returned model + +#### get\_or\_none + +```python + | async get_or_none(*args: Any, **kwargs: Any) -> Optional["T"] +``` + +Get's the first row from the db meeting the criteria set by kwargs. + +If no criteria set it will return the last row in db sorted by pk. + +Passing a criteria is actually calling filter(**kwargs) method described below. + +If not match is found None will be returned. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: returned model + #### get ```python - | async get(**kwargs: Any) -> "Model" + | async get(*args: Any, **kwargs: Any) -> "T" ``` Get's the first row from the db meeting the criteria set by kwargs. @@ -568,7 +695,7 @@ Passing a criteria is actually calling filter(**kwargs) method described below. #### get\_or\_create ```python - | async get_or_create(**kwargs: Any) -> "Model" + | async get_or_create(*args: Any, **kwargs: Any) -> "T" ``` Combination of create and get methods. @@ -589,7 +716,7 @@ it creates a new one with given kwargs. #### update\_or\_create ```python - | async update_or_create(**kwargs: Any) -> "Model" + | async update_or_create(**kwargs: Any) -> "T" ``` Updates the model, or in case there is no match in database creates a new one. @@ -606,7 +733,7 @@ Updates the model, or in case there is no match in database creates a new one. #### all ```python - | async all(**kwargs: Any) -> Sequence[Optional["Model"]] + | async all(*args: Any, **kwargs: Any) -> List[Optional["T"]] ``` Returns all rows from a database for given model for set filter options. @@ -627,7 +754,7 @@ If there are no rows meeting the criteria an empty list is returned. #### create ```python - | async create(**kwargs: Any) -> "Model" + | async create(**kwargs: Any) -> "T" ``` Creates the model instance, saves it in a database and returns the updates model @@ -647,7 +774,7 @@ The allowed kwargs are `Model` fields names and proper value types. #### bulk\_create ```python - | async bulk_create(objects: List["Model"]) -> None + | async bulk_create(objects: List["T"]) -> None ``` Performs a bulk update in one database session to speed up the process. @@ -666,7 +793,7 @@ Bulk operations do not send signals. #### bulk\_update ```python - | async bulk_update(objects: List["Model"], columns: List[str] = None) -> None + | async bulk_update(objects: List["T"], columns: List[str] = None) -> None ``` Performs bulk update in one database session to speed up the process. diff --git a/docs/api/query-set/query.md b/docs/api/query-set/query.md index dc3314d..fc086c9 100644 --- a/docs/api/query-set/query.md +++ b/docs/api/query-set/query.md @@ -28,6 +28,16 @@ Applies order_by queries on main model when it's used as a subquery. That way the subquery with limit and offset only on main model has proper sorting applied and correct models are fetched. + +#### \_apply\_default\_model\_sorting + +```python + | _apply_default_model_sorting() -> None +``` + +Applies orders_by from model Meta class (if provided), if it was not provided +it was filled by metaclass so it's always there and falls back to pk column + #### \_pagination\_query\_required @@ -62,11 +72,13 @@ Returns ready to run query with all joins and clauses. `(sqlalchemy.sql.selectable.Select)`: ready to run query with all joins and clauses. - -#### \_build\_pagination\_subquery + +#### \_build\_pagination\_condition ```python - | _build_pagination_subquery() -> sqlalchemy.sql.select + | _build_pagination_condition() -> Tuple[ + | sqlalchemy.sql.expression.TextClause, sqlalchemy.sql.expression.TextClause + | ] ``` In order to apply limit and offset on main table in join only @@ -78,9 +90,8 @@ Needed only if limit or offset are set, the flag limit_sql_raw is not set and query has select_related applied. Otherwise we can limit/offset normally at the end of whole query. -**Returns**: - -`(sqlalchemy.sql.select)`: constructed subquery on main table with limit, offset and order applied +The condition is added to filters to filter out desired number of main model +primary key values. Whole query is used to determine the values. #### \_apply\_expression\_modifiers diff --git a/docs/api/query-set/utils.md b/docs/api/query-set/utils.md index f42e340..2268086 100644 --- a/docs/api/query-set/utils.md +++ b/docs/api/query-set/utils.md @@ -5,7 +5,7 @@ #### check\_node\_not\_dict\_or\_not\_last\_node ```python -check_node_not_dict_or_not_last_node(part: str, parts: List, current_level: Any) -> bool +check_node_not_dict_or_not_last_node(part: str, is_last: bool, current_level: Any) -> bool ``` Checks if given name is not present in the current level of the structure. @@ -86,6 +86,27 @@ only other values are overwritten. `(Dict)`: combination of both dicts + +#### subtract\_dict + +```python +subtract_dict(current_dict: Any, updating_dict: Any) -> Dict +``` + +Update one dict with another but with regard for nested keys. + +That way nested sets are unionised, dicts updated and +only other values are overwritten. + +**Arguments**: + +- `current_dict (Dict[str, ellipsis])`: dict to update +- `updating_dict (Dict)`: dict with values to update + +**Returns**: + +`(Dict)`: combination of both dicts + #### update\_dict\_from\_list @@ -169,3 +190,24 @@ constructed, extracts alias based on last relation leading to target model. `(Tuple[str, Type["Model"], str])`: table prefix, target model and relation string + +#### \_process\_through\_field + +```python +_process_through_field(related_parts: List, relation: Optional[str], related_field: "BaseField", previous_model: Type["Model"], previous_models: List[Type["Model"]]) -> Tuple[Type["Model"], Optional[str], bool] +``` + +Helper processing through models as they need to be treated differently. + +**Arguments**: + +- `related_parts (List[str])`: split relation string +- `relation (str)`: relation name +- `related_field ("ForeignKeyField")`: field with relation declaration +- `previous_model (Type["Model"])`: model from which relation is coming +- `previous_models (List[Type["Model"]])`: list of already visited models in relation chain + +**Returns**: + +`(Tuple[Type["Model"], str, bool])`: previous_model, relation, is_through + diff --git a/docs/api/relations/alias-manager.md b/docs/api/relations/alias-manager.md index d563cf8..57c4885 100644 --- a/docs/api/relations/alias-manager.md +++ b/docs/api/relations/alias-manager.md @@ -56,7 +56,7 @@ List has to have sqlalchemy names of columns (ormar aliases) not the ormar ones. ```python | @staticmethod - | prefixed_table_name(alias: str, name: str) -> text + | prefixed_table_name(alias: str, table: sqlalchemy.Table) -> text ``` Creates text clause with table name with aliased name. @@ -64,7 +64,7 @@ Creates text clause with table name with aliased name. **Arguments**: - `alias (str)`: alias of given table -- `name (str)`: table name +- `table (sqlalchemy.Table)`: table **Returns**: @@ -138,7 +138,7 @@ Given model and relation name returns the alias for this relation. #### resolve\_relation\_alias\_after\_complex ```python - | resolve_relation_alias_after_complex(source_model: Union[Type["Model"], Type["ModelRow"]], relation_str: str, relation_field: Type["ForeignKeyField"]) -> str + | resolve_relation_alias_after_complex(source_model: Union[Type["Model"], Type["ModelRow"]], relation_str: str, relation_field: "ForeignKeyField") -> str ``` Given source model and relation string returns the alias for this complex @@ -147,7 +147,7 @@ field definition. **Arguments**: -- `relation_field (Type["ForeignKeyField"])`: field with direct relation definition +- `relation_field ("ForeignKeyField")`: field with direct relation definition - `source_model (source Model)`: model with query starts - `relation_str (str)`: string with relation joins defined diff --git a/docs/api/relations/queryset-proxy.md b/docs/api/relations/queryset-proxy.md index 1eb9637..3b46759 100644 --- a/docs/api/relations/queryset-proxy.md +++ b/docs/api/relations/queryset-proxy.md @@ -5,7 +5,7 @@ ## QuerysetProxy Objects ```python -class QuerysetProxy() +class QuerysetProxy(Generic[T]) ``` Exposes QuerySet methods on relations, but also handles creating and removing @@ -16,7 +16,7 @@ of through Models for m2m relations. ```python | @property - | queryset() -> "QuerySet" + | queryset() -> "QuerySet[T]" ``` Returns queryset if it's set, AttributeError otherwise. @@ -43,7 +43,7 @@ Set's the queryset. Initialized in RelationProxy. #### \_assign\_child\_to\_parent ```python - | _assign_child_to_parent(child: Optional["Model"]) -> None + | _assign_child_to_parent(child: Optional["T"]) -> None ``` Registers child in parents RelationManager. @@ -56,7 +56,7 @@ Registers child in parents RelationManager. #### \_register\_related ```python - | _register_related(child: Union["Model", Sequence[Optional["Model"]]]) -> None + | _register_related(child: Union["T", Sequence[Optional["T"]]]) -> None ``` Registers child/ children in parents RelationManager. @@ -78,7 +78,7 @@ Cleans the current list of the related models. #### create\_through\_instance ```python - | async create_through_instance(child: "Model", **kwargs: Any) -> None + | async create_through_instance(child: "T", **kwargs: Any) -> None ``` Crete a through model instance in the database for m2m relations. @@ -92,7 +92,7 @@ Crete a through model instance in the database for m2m relations. #### update\_through\_instance ```python - | async update_through_instance(child: "Model", **kwargs: Any) -> None + | async update_through_instance(child: "T", **kwargs: Any) -> None ``` Updates a through model instance in the database for m2m relations. @@ -102,11 +102,26 @@ Updates a through model instance in the database for m2m relations. - `kwargs (Any)`: dict of additional keyword arguments for through instance - `child (Model)`: child model instance + +#### upsert\_through\_instance + +```python + | async upsert_through_instance(child: "T", **kwargs: Any) -> None +``` + +Updates a through model instance in the database for m2m relations if +it already exists, else creates one. + +**Arguments**: + +- `kwargs (Any)`: dict of additional keyword arguments for through instance +- `child (Model)`: child model instance + #### delete\_through\_instance ```python - | async delete_through_instance(child: "Model") -> None + | async delete_through_instance(child: "T") -> None ``` Removes through model instance from the database for m2m relations. @@ -147,6 +162,62 @@ Actual call delegated to QuerySet. `(int)`: number of rows + +#### max + +```python + | async max(columns: Union[str, List[str]]) -> Any +``` + +Returns max value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(Any)`: max value of column(s) + + +#### min + +```python + | async min(columns: Union[str, List[str]]) -> Any +``` + +Returns min value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(Any)`: min value of column(s) + + +#### sum + +```python + | async sum(columns: Union[str, List[str]]) -> Any +``` + +Returns sum value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(int)`: sum value of columns + + +#### avg + +```python + | async avg(columns: Union[str, List[str]]) -> Any +``` + +Returns avg value of columns for rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(Union[int, float, List])`: avg value of columns + #### clear @@ -175,7 +246,7 @@ or not, keep_reversed=False deletes them from database. #### first ```python - | async first(**kwargs: Any) -> "Model" + | async first(*args: Any, **kwargs: Any) -> "T" ``` Gets the first row from the db ordered by primary key column ascending. @@ -192,11 +263,34 @@ List of related models is cleared before the call. `(_asyncio.Future)`: + +#### get\_or\_none + +```python + | async get_or_none(*args: Any, **kwargs: Any) -> Optional["T"] +``` + +Get's the first row from the db meeting the criteria set by kwargs. + +If no criteria set it will return the last row in db sorted by pk. + +Passing a criteria is actually calling filter(**kwargs) method described below. + +If not match is found None will be returned. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: returned model + #### get ```python - | async get(**kwargs: Any) -> "Model" + | async get(*args: Any, **kwargs: Any) -> "T" ``` Get's the first row from the db meeting the criteria set by kwargs. @@ -226,7 +320,7 @@ List of related models is cleared before the call. #### all ```python - | async all(**kwargs: Any) -> Sequence[Optional["Model"]] + | async all(*args: Any, **kwargs: Any) -> List[Optional["T"]] ``` Returns all rows from a database for given model for set filter options. @@ -251,7 +345,7 @@ List of related models is cleared before the call. #### create ```python - | async create(**kwargs: Any) -> "Model" + | async create(**kwargs: Any) -> "T" ``` Creates the model instance, saves it in a database and returns the updates model @@ -296,7 +390,7 @@ each=True flag to affect whole table. #### get\_or\_create ```python - | async get_or_create(**kwargs: Any) -> "Model" + | async get_or_create(*args: Any, **kwargs: Any) -> "T" ``` Combination of create and get methods. @@ -317,7 +411,7 @@ it creates a new one with given kwargs. #### update\_or\_create ```python - | async update_or_create(**kwargs: Any) -> "Model" + | async update_or_create(**kwargs: Any) -> "T" ``` Updates the model, or in case there is no match in database creates a new one. @@ -336,7 +430,7 @@ Actual call delegated to QuerySet. #### filter ```python - | filter(**kwargs: Any) -> "QuerysetProxy" + | filter(*args: Any, **kwargs: Any) -> "QuerysetProxy[T]" ``` Allows you to filter by any `Model` attribute/field @@ -349,6 +443,8 @@ You can use special filter suffix to change the filter operands: * contains - like `album__name__contains='Mal'` (sql like) * icontains - like `album__name__icontains='mal'` (sql like case insensitive) * in - like `album__name__in=['Malibu', 'Barclay']` (sql in) +* isnull - like `album__name__isnull=True` (sql is null) +(isnotnull `album__name__isnull=False` (sql is not null)) * gt - like `position__gt=3` (sql >) * gte - like `position__gte=3` (sql >=) * lt - like `position__lt=3` (sql <) @@ -372,7 +468,7 @@ Actual call delegated to QuerySet. #### exclude ```python - | exclude(**kwargs: Any) -> "QuerysetProxy" + | exclude(*args: Any, **kwargs: Any) -> "QuerysetProxy[T]" ``` Works exactly the same as filter and all modifiers (suffixes) are the same, @@ -397,11 +493,40 @@ Actual call delegated to QuerySet. `(QuerysetProxy)`: filtered QuerysetProxy + +#### select\_all + +```python + | select_all(follow: bool = False) -> "QuerysetProxy[T]" +``` + +By default adds only directly related models. + +If follow=True is set it adds also related models of related models. + +To not get stuck in an infinite loop as related models also keep a relation +to parent model visited models set is kept. + +That way already visited models that are nested are loaded, but the load do not +follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X +will load second Model A but will never follow into Model X. +Nested relations of those kind need to be loaded manually. + +**Arguments**: + +- `follow (bool)`: flag to trigger deep save - +by default only directly related models are saved +with follow=True also related models of related models are saved + +**Returns**: + +`(Model)`: reloaded Model + #### select\_related ```python - | select_related(related: Union[List, str]) -> "QuerysetProxy" + | select_related(related: Union[List, str]) -> "QuerysetProxy[T]" ``` Allows to prefetch related models during the same query. @@ -428,7 +553,7 @@ Actual call delegated to QuerySet. #### prefetch\_related ```python - | prefetch_related(related: Union[List, str]) -> "QuerysetProxy" + | prefetch_related(related: Union[List, str]) -> "QuerysetProxy[T]" ``` Allows to prefetch related models during query - but opposite to @@ -456,7 +581,7 @@ Actual call delegated to QuerySet. #### paginate ```python - | paginate(page: int, page_size: int = 20) -> "QuerysetProxy" + | paginate(page: int, page_size: int = 20) -> "QuerysetProxy[T]" ``` You can paginate the result which is a combination of offset and limit clauses. @@ -477,7 +602,7 @@ Actual call delegated to QuerySet. #### limit ```python - | limit(limit_count: int) -> "QuerysetProxy" + | limit(limit_count: int) -> "QuerysetProxy[T]" ``` You can limit the results to desired number of parent models. @@ -496,7 +621,7 @@ Actual call delegated to QuerySet. #### offset ```python - | offset(offset: int) -> "QuerysetProxy" + | offset(offset: int) -> "QuerysetProxy[T]" ``` You can also offset the results by desired number of main models. @@ -515,7 +640,7 @@ Actual call delegated to QuerySet. #### fields ```python - | fields(columns: Union[List, str, Set, Dict]) -> "QuerysetProxy" + | fields(columns: Union[List, str, Set, Dict]) -> "QuerysetProxy[T]" ``` With `fields()` you can select subset of model columns to limit the data load. @@ -568,7 +693,7 @@ Actual call delegated to QuerySet. #### exclude\_fields ```python - | exclude_fields(columns: Union[List, str, Set, Dict]) -> "QuerysetProxy" + | exclude_fields(columns: Union[List, str, Set, Dict]) -> "QuerysetProxy[T]" ``` With `exclude_fields()` you can select subset of model columns that will @@ -605,7 +730,7 @@ Actual call delegated to QuerySet. #### order\_by ```python - | order_by(columns: Union[List, str]) -> "QuerysetProxy" + | order_by(columns: Union[List, str, "OrderAction"]) -> "QuerysetProxy[T]" ``` With `order_by()` you can order the results from database based on your diff --git a/docs/api/relations/relation-manager.md b/docs/api/relations/relation-manager.md index d83febe..3b6c0e8 100644 --- a/docs/api/relations/relation-manager.md +++ b/docs/api/relations/relation-manager.md @@ -50,7 +50,7 @@ Actual call is delegated to Relation instance registered under relation name. ```python | @staticmethod - | add(parent: "Model", child: "Model", field: Type["ForeignKeyField"]) -> None + | add(parent: "Model", child: "Model", field: "ForeignKeyField") -> None ``` Adds relation on both sides -> meaning on both child and parent models. @@ -121,14 +121,14 @@ Returns the actual relation and not the related model(s). #### \_get\_relation\_type ```python - | _get_relation_type(field: Type["BaseField"]) -> RelationType + | _get_relation_type(field: "BaseField") -> RelationType ``` Returns type of the relation declared on a field. **Arguments**: -- `field (Type[BaseField])`: field with relation declaration +- `field (BaseField)`: field with relation declaration **Returns**: @@ -138,7 +138,7 @@ Returns type of the relation declared on a field. #### \_add\_relation ```python - | _add_relation(field: Type["BaseField"]) -> None + | _add_relation(field: "BaseField") -> None ``` Registers relation in the manager. @@ -146,5 +146,5 @@ Adds Relation instance under field.name. **Arguments**: -- `field (Type[BaseField])`: field with relation declaration +- `field (BaseField)`: field with relation declaration diff --git a/docs/api/relations/relation-proxy.md b/docs/api/relations/relation-proxy.md index 1d122a7..a896df6 100644 --- a/docs/api/relations/relation-proxy.md +++ b/docs/api/relations/relation-proxy.md @@ -5,7 +5,7 @@ ## RelationProxy Objects ```python -class RelationProxy(list) +class RelationProxy(Generic[T], list) ``` Proxy of the Relation that is a list with special methods. @@ -96,7 +96,7 @@ Otherwise QuerySetProxy cannot filter by parent primary key. #### \_set\_queryset ```python - | _set_queryset() -> "QuerySet" + | _set_queryset() -> "QuerySet[T]" ``` Creates new QuerySet with relation model and pre filters it with currents @@ -111,7 +111,7 @@ to the parent model only, without need for user to filter them. #### remove ```python - | async remove(item: "Model", keep_reversed: bool = True) -> None + | async remove(item: "T", keep_reversed: bool = True) -> None ``` Removes the related from relation with parent. @@ -131,7 +131,7 @@ will be deleted, and not only removed from relation). #### add ```python - | async add(item: "Model", **kwargs: Any) -> None + | async add(item: "T", **kwargs: Any) -> None ``` Adds child model to relation. diff --git a/docs/api/relations/relation.md b/docs/api/relations/relation.md index 29e8ab7..a20016b 100644 --- a/docs/api/relations/relation.md +++ b/docs/api/relations/relation.md @@ -18,7 +18,7 @@ Different types of relations supported by ormar: ## Relation Objects ```python -class Relation() +class Relation(Generic[T]) ``` Keeps related Models and handles adding/removing of the children. @@ -27,7 +27,7 @@ Keeps related Models and handles adding/removing of the children. #### \_\_init\_\_ ```python - | __init__(manager: "RelationsManager", type_: RelationType, field_name: str, to: Type["Model"], through: Type["Model"] = None) -> None + | __init__(manager: "RelationsManager", type_: RelationType, field_name: str, to: Type["T"], through: Type["Model"] = None) -> None ``` Initialize the Relation and keep the related models either as instances of diff --git a/docs/api/relations/utils.md b/docs/api/relations/utils.md index a771d31..8711b3d 100644 --- a/docs/api/relations/utils.md +++ b/docs/api/relations/utils.md @@ -5,7 +5,7 @@ #### get\_relations\_sides\_and\_names ```python -get_relations_sides_and_names(to_field: Type[ForeignKeyField], parent: "Model", child: "Model") -> Tuple["Model", "Model", str, str] +get_relations_sides_and_names(to_field: ForeignKeyField, parent: "Model", child: "Model") -> Tuple["Model", "Model", str, str] ``` Determines the names of child and parent relations names, as well as diff --git a/docs/api/signals/decorators.md b/docs/api/signals/decorators.md index de7fe8b..7fcbc0a 100644 --- a/docs/api/signals/decorators.md +++ b/docs/api/signals/decorators.md @@ -128,3 +128,75 @@ that should have the signal receiver registered `(Callable)`: returns the original function untouched + +#### pre\_relation\_add + +```python +pre_relation_add(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for pre_relation_add signal. + +**Arguments**: + +- `senders (Union[Type["Model"], List[Type["Model"]]])`: one or a list of "Model" classes +that should have the signal receiver registered + +**Returns**: + +`(Callable)`: returns the original function untouched + + +#### post\_relation\_add + +```python +post_relation_add(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for post_relation_add signal. + +**Arguments**: + +- `senders (Union[Type["Model"], List[Type["Model"]]])`: one or a list of "Model" classes +that should have the signal receiver registered + +**Returns**: + +`(Callable)`: returns the original function untouched + + +#### pre\_relation\_remove + +```python +pre_relation_remove(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for pre_relation_remove signal. + +**Arguments**: + +- `senders (Union[Type["Model"], List[Type["Model"]]])`: one or a list of "Model" classes +that should have the signal receiver registered + +**Returns**: + +`(Callable)`: returns the original function untouched + + +#### post\_relation\_remove + +```python +post_relation_remove(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for post_relation_remove signal. + +**Arguments**: + +- `senders (Union[Type["Model"], List[Type["Model"]]])`: one or a list of "Model" classes +that should have the signal receiver registered + +**Returns**: + +`(Callable)`: returns the original function untouched + diff --git a/docs/queries/filter-and-sort.md b/docs/queries/filter-and-sort.md index 4045f10..7f4f7d3 100644 --- a/docs/queries/filter-and-sort.md +++ b/docs/queries/filter-and-sort.md @@ -69,34 +69,34 @@ tracks = Track.objects.filter(album__name="Fantasies").all() You can use special filter suffix to change the filter operands: -* exact - exact match to value, sql `column = ` - * can be written as`album__name__exact='Malibu'` -* iexact - exact match sql `column = ` (case insensitive) - * can be written as`album__name__iexact='malibu'` -* contains - sql `column LIKE '%%'` - * can be written as`album__name__contains='Mal'` -* icontains - sql `column LIKE '%%'` (case insensitive) - * can be written as`album__name__icontains='mal'` -* in - sql ` column IN (, , ...)` - * can be written as`album__name__in=['Malibu', 'Barclay']` -* isnull - sql `column IS NULL` (and sql `column IS NOT NULL`) - * can be written as`album__name__isnull=True` (isnotnull `album__name__isnull=False`) -* gt - sql `column > ` (greater than) - * can be written as`position__gt=3` -* gte - sql `column >= ` (greater or equal than) - * can be written as`position__gte=3` -* lt - sql `column < ` (lower than) - * can be written as`position__lt=3` -* lte - sql `column <= ` (lower equal than) - * can be written as`position__lte=3` -* startswith - sql `column LIKE '%'` (exact start match) - * can be written as`album__name__startswith='Mal'` -* istartswith - sql `column LIKE '%'` (case insensitive) - * can be written as`album__name__istartswith='mal'` -* endswith - sql `column LIKE '%'` (exact end match) - * can be written as`album__name__endswith='ibu'` -* iendswith - sql `column LIKE '%'` (case insensitive) - * can be written as`album__name__iendswith='IBU'` +* **exact** - exact match to value, sql `column = ` + * can be written as`album__name__exact='Malibu'` +* **iexact** - exact match sql `column = ` (case insensitive) + * can be written as`album__name__iexact='malibu'` +* **contains** - sql `column LIKE '%%'` + * can be written as`album__name__contains='Mal'` +* **icontains** - sql `column LIKE '%%'` (case insensitive) + * can be written as`album__name__icontains='mal'` +* **in** - sql ` column IN (, , ...)` + * can be written as`album__name__in=['Malibu', 'Barclay']` +* **isnull** - sql `column IS NULL` (and sql `column IS NOT NULL`) + * can be written as`album__name__isnull=True` (isnotnull `album__name__isnull=False`) +* **gt** - sql `column > ` (greater than) + * can be written as`position__gt=3` +* **gte** - sql `column >= ` (greater or equal than) + * can be written as`position__gte=3` +* **lt** - sql `column < ` (lower than) + * can be written as`position__lt=3` +* **lte** - sql `column <= ` (lower equal than) + * can be written as`position__lte=3` +* **startswith** - sql `column LIKE '%'` (exact start match) + * can be written as`album__name__startswith='Mal'` +* **istartswith** - sql `column LIKE '%'` (case insensitive) + * can be written as`album__name__istartswith='mal'` +* **endswith** - sql `column LIKE '%'` (exact end match) + * can be written as`album__name__endswith='ibu'` +* **iendswith** - sql `column LIKE '%'` (case insensitive) + * can be written as`album__name__iendswith='IBU'` Some samples: @@ -116,40 +116,40 @@ Product.objects.filter( ### Python style filters -* exact - exact match to value, sql `column = ` - * can be written as `Track.album.name == 'Malibu` -* iexact - exact match sql `column = ` (case insensitive) - * can be written as `Track.album.name.iexact('malibu')` -* contains - sql `column LIKE '%%'` - * can be written as `Track.album.name % 'Mal')` - * can be written as `Track.album.name.contains('Mal')` -* icontains - sql `column LIKE '%%'` (case insensitive) - * can be written as `Track.album.name.icontains('mal')` -* in - sql ` column IN (, , ...)` - * can be written as `Track.album.name << ['Malibu', 'Barclay']` - * can be written as `Track.album.name.in_(['Malibu', 'Barclay'])` -* isnull - sql `column IS NULL` (and sql `column IS NOT NULL`) - * can be written as `Track.album.name >> None` - * can be written as `Track.album.name.is_null(True)` - * not null can be written as `Track.album.name.is_null(False)` - * not null can be written as `~(Track.album.name >> None)` - * not null can be written as `~(Track.album.name.is_null(True))` -* gt - sql `column > ` (greater than) - * can be written as `Track.album.name > 3` -* gte - sql `column >= ` (greater or equal than) - * can be written as `Track.album.name >= 3` -* lt - sql `column < ` (lower than) - * can be written as `Track.album.name < 3` -* lte - sql `column <= ` (lower equal than) - * can be written as `Track.album.name <= 3` -* startswith - sql `column LIKE '%'` (exact start match) - * can be written as `Track.album.name.startswith('Mal')` -* istartswith - sql `column LIKE '%'` (case insensitive) - * can be written as `Track.album.name.istartswith('mal')` -* endswith - sql `column LIKE '%'` (exact end match) - * can be written as `Track.album.name.endswith('ibu')` -* iendswith - sql `column LIKE '%'` (case insensitive) - * can be written as `Track.album.name.iendswith('IBU')` +* **exact** - exact match to value, sql `column = ` + * can be written as `Track.album.name == 'Malibu` +* **iexact** - exact match sql `column = ` (case insensitive) + * can be written as `Track.album.name.iexact('malibu')` +* **contains** - sql `column LIKE '%%'` + * can be written as `Track.album.name % 'Mal')` + * can be written as `Track.album.name.contains('Mal')` +* **icontains** - sql `column LIKE '%%'` (case insensitive) + * can be written as `Track.album.name.icontains('mal')` +* **in** - sql ` column IN (, , ...)` + * can be written as `Track.album.name << ['Malibu', 'Barclay']` + * can be written as `Track.album.name.in_(['Malibu', 'Barclay'])` +* **isnull** - sql `column IS NULL` (and sql `column IS NOT NULL`) + * can be written as `Track.album.name >> None` + * can be written as `Track.album.name.isnull(True)` + * not null can be written as `Track.album.name.isnull(False)` + * not null can be written as `~(Track.album.name >> None)` + * not null can be written as `~(Track.album.name.isnull(True))` +* **gt** - sql `column > ` (greater than) + * can be written as `Track.album.name > 3` +* **gte** - sql `column >= ` (greater or equal than) + * can be written as `Track.album.name >= 3` +* **lt** - sql `column < ` (lower than) + * can be written as `Track.album.name < 3` +* **lte** - sql `column <= ` (lower equal than) + * can be written as `Track.album.name <= 3` +* **startswith** - sql `column LIKE '%'` (exact start match) + * can be written as `Track.album.name.startswith('Mal')` +* **istartswith** - sql `column LIKE '%'` (case insensitive) + * can be written as `Track.album.name.istartswith('mal')` +* **endswith** - sql `column LIKE '%'` (exact end match) + * can be written as `Track.album.name.endswith('ibu')` +* **iendswith** - sql `column LIKE '%'` (case insensitive) + * can be written as `Track.album.name.iendswith('IBU')` Some samples: @@ -291,7 +291,7 @@ Let's select books of Tolkien **OR** books written after 1970 sql: `WHERE ( authors.name = 'J.R.R. Tolkien' OR books.year > 1970 )` -### Django style +#### Django style ```python books = ( await Book.objects.select_related("author") @@ -301,7 +301,7 @@ books = ( assert len(books) == 5 ``` -### Python style +#### Python style ```python books = ( await Book.objects.select_related("author") @@ -316,7 +316,7 @@ Now let's select books written after 1960 or before 1940 which were written by T sql: `WHERE ( books.year > 1960 OR books.year < 1940 ) AND authors.name = 'J.R.R. Tolkien'` -### Django style +#### Django style ```python # OPTION 1 - split and into separate call books = ( @@ -344,7 +344,7 @@ assert books[0].title == "The Hobbit" assert books[1].title == "The Silmarillion" ``` -### Python style +#### Python style ```python books = ( await Book.objects.select_related("author") @@ -375,7 +375,7 @@ Books of Sapkowski from before 2000 or books of Tolkien written after 1960 sql: `WHERE ( ( books.year > 1960 AND authors.name = 'J.R.R. Tolkien' ) OR ( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) ) ` -### Django style +#### Django style ```python books = ( await Book.objects.select_related("author") @@ -390,7 +390,7 @@ books = ( assert len(books) == 2 ``` -### Python style +#### Python style ```python books = ( await Book.objects.select_related("author") @@ -411,7 +411,7 @@ sql: ( books.year < 2000 AND os0cec_authors.name = 'Andrzej Sapkowski' ) OR books.title LIKE '%hobbit%' )` -### Django style +#### Django style ```python books = ( await Book.objects.select_related("author") @@ -426,7 +426,7 @@ books = ( ) ``` -### Python style +#### Python style ```python books = ( await Book.objects.select_related("author") @@ -451,7 +451,7 @@ AND authors.name = 'J.R.R. Tolkien' ) OR You can construct a query as follows: -### Django style +#### Django style ```python books = ( await Book.objects.select_related("author") @@ -472,16 +472,21 @@ assert books[1].title == "The Silmarillion" assert books[2].title == "The Witcher" ``` +#### Python style ```python books = ( await Book.objects.select_related("author") - .filter( - ormar.or_( - ormar.and_( - ormar.or_(year__gt=1960, year__lt=1940), - author__name="J.R.R. Tolkien", - ), - ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"), + .filter( + ( + ( + (Book.year > 1960) | + (Book.year < 1940) + ) & + (Book.author.name == "J.R.R. Tolkien") + ) | + ( + (Book.year < 2000) & + (Book.author.name == "Andrzej Sapkowski") ) ) .all() @@ -512,7 +517,7 @@ assert books[0].title == "The Witcher" Same applies to python style chaining and nesting. -### Django style +#### Django style Note that with django style you cannot provide the same keyword argument several times so queries like `filter(ormar.or_(name='Jack', name='John'))` are not allowed. If you want to check the same column for several values simply use `in` operator: `filter(name__in=['Jack','John'])`. @@ -559,7 +564,7 @@ books = ( assert len(books) == 5 ``` -### Python style +#### Python style Note that with python style you can perfectly use the same fields as many times as you want. @@ -721,7 +726,7 @@ Given sample Models like following: To order by main model field just provide a field name -### Django style +#### Django style ```python toys = await Toy.objects.select_related("owner").order_by("name").all() assert [x.name.replace("Toy ", "") for x in toys] == [ @@ -731,7 +736,7 @@ assert toys[0].owner == zeus assert toys[1].owner == aphrodite ``` -### Python style +#### Python style ```python toys = await Toy.objects.select_related("owner").order_by(Toy.name.asc()).all() assert [x.name.replace("Toy ", "") for x in toys] == [ @@ -747,7 +752,7 @@ To sort on nested models separate field names with dunder '__'. You can sort this way across all relation types -> `ForeignKey`, reverse virtual FK and `ManyToMany` fields. -### Django style +#### Django style ```python toys = await Toy.objects.select_related("owner").order_by("owner__name").all() assert toys[0].owner.name == toys[1].owner.name == "Aphrodite" @@ -755,7 +760,7 @@ assert toys[2].owner.name == toys[3].owner.name == "Hermes" assert toys[4].owner.name == toys[5].owner.name == "Zeus" ``` -### Python style +#### Python style ```python toys = await Toy.objects.select_related("owner").order_by(Toy.owner.name.asc()).all() assert toys[0].owner.name == toys[1].owner.name == "Aphrodite" @@ -765,7 +770,7 @@ assert toys[4].owner.name == toys[5].owner.name == "Zeus" To sort in descending order provide a hyphen in front of the field name -### Django style +#### Django style ```python owner = ( await Owner.objects.select_related("toys") @@ -777,7 +782,7 @@ assert owner.toys[0].name == "Toy 4" assert owner.toys[1].name == "Toy 1" ``` -### Python style +#### Python style ```python owner = ( await Owner.objects.select_related("toys") diff --git a/docs/releases.md b/docs/releases.md index 6b9a4a5..ae63e5e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -25,10 +25,10 @@ * isnull - sql `column IS NULL` (and sql `column IS NOT NULL`) * OLD: `album__name__isnull=True` (isnotnull `album__name__isnull=False`) * NEW: can be also written as `Track.album.name >> None` - * NEW: can be also written as `Track.album.name.is_null(True)` - * NEW: not null can be also written as `Track.album.name.is_null(False)` + * NEW: can be also written as `Track.album.name.isnull(True)` + * NEW: not null can be also written as `Track.album.name.isnull(False)` * NEW: not null can be also written as `~(Track.album.name >> None)` - * NEW: not null can be also written as `~(Track.album.name.is_null(True))` + * NEW: not null can be also written as `~(Track.album.name.isnull(True))` * gt - sql `column > ` (greater than) * OLD: `position__gt=3` * NEW: can be also written as `Track.album.name > 3` diff --git a/mkdocs.yml b/mkdocs.yml index 65d5215..c32dbcc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,6 +77,7 @@ nav: - Order Query: api/query-set/order-query.md - Limit Query: api/query-set/limit-query.md - Offset Query: api/query-set/offset-query.md + - Field Accessor: api/query-set/field-accessor.md - api/query-set/utils.md - Relations: - Relation Manager: api/relations/relation-manager.md diff --git a/ormar/queryset/field_accessor.py b/ormar/queryset/field_accessor.py index 2701454..f0a7e2d 100644 --- a/ormar/queryset/field_accessor.py +++ b/ormar/queryset/field_accessor.py @@ -9,6 +9,11 @@ if TYPE_CHECKING: # pragma: no cover class FieldAccessor: + """ + Helper to access ormar fields directly from Model class also for nested + models attributes. + """ + def __init__( self, source_model: Type["Model"], @@ -22,10 +27,24 @@ class FieldAccessor: self._access_chain = access_chain def __bool__(self) -> bool: - # hack to avoid pydantic name check from parent model + """ + Hack to avoid pydantic name check from parent model, returns false + + :return: False + :rtype: bool + """ return False def __getattr__(self, item: str) -> Any: + """ + Accessor return new accessor for each field and nested models. + Thanks to that operator overload is possible to use in filter. + + :param item: attribute name + :type item: str + :return: FieldAccessor for field or nested model + :rtype: ormar.queryset.field_accessor.FieldAccessor + """ if self._field and item == self._field.name: return self._field @@ -57,60 +76,208 @@ class FieldAccessor: return FilterGroup(**filter_kwg) def __eq__(self, other: Any) -> FilterGroup: # type: ignore + """ + overloaded to work as sql `column = ` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="__eq__", other=other) def __ge__(self, other: Any) -> FilterGroup: + """ + overloaded to work as sql `column >= ` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="__ge__", other=other) def __gt__(self, other: Any) -> FilterGroup: + """ + overloaded to work as sql `column > ` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="__gt__", other=other) def __le__(self, other: Any) -> FilterGroup: + """ + overloaded to work as sql `column <= ` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="__le__", other=other) def __lt__(self, other: Any) -> FilterGroup: + """ + overloaded to work as sql `column < ` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="__lt__", other=other) def __mod__(self, other: Any) -> FilterGroup: + """ + overloaded to work as sql `column LIKE '%%'` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="__mod__", other=other) def __lshift__(self, other: Any) -> FilterGroup: + """ + overloaded to work as sql `column IN (, ,...)` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="in", other=other) def __rshift__(self, other: Any) -> FilterGroup: + """ + overloaded to work as sql `column IS NULL` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="isnull", other=True) def in_(self, other: Any) -> FilterGroup: + """ + works as sql `column IN (, ,...)` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="in", other=other) def iexact(self, other: Any) -> FilterGroup: + """ + works as sql `column = ` case-insensitive + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="iexact", other=other) def contains(self, other: Any) -> FilterGroup: + """ + works as sql `column LIKE '%%'` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="contains", other=other) def icontains(self, other: Any) -> FilterGroup: + """ + works as sql `column LIKE '%%'` case-insensitive + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="icontains", other=other) def startswith(self, other: Any) -> FilterGroup: + """ + works as sql `column LIKE '%'` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="startswith", other=other) def istartswith(self, other: Any) -> FilterGroup: + """ + works as sql `column LIKE '%'` case-insensitive + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="istartswith", other=other) def endswith(self, other: Any) -> FilterGroup: + """ + works as sql `column LIKE '%'` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="endswith", other=other) def iendswith(self, other: Any) -> FilterGroup: + """ + works as sql `column LIKE '%'` case-insensitive + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="iendswith", other=other) def isnull(self, other: Any) -> FilterGroup: + """ + works as sql `column IS NULL` or `IS NOT NULL` + + :param other: value to check agains operator + :type other: str + :return: FilterGroup for operator + :rtype: ormar.queryset.clause.FilterGroup + """ return self._select_operator(op="isnull", other=other) def asc(self) -> OrderAction: + """ + works as sql `column asc` + + :return: OrderGroup for operator + :rtype: ormar.queryset.actions.OrderGroup + """ return OrderAction(order_str=self._access_chain, model_cls=self._source_model) def desc(self) -> OrderAction: + """ + works as sql `column desc` + + :return: OrderGroup for operator + :rtype: ormar.queryset.actions.OrderGroup + """ return OrderAction( order_str="-" + self._access_chain, model_cls=self._source_model ) diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 7243a2a..372334c 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -122,6 +122,9 @@ renderer: - title: Offset Query contents: - queryset.offset_query.* + - title: Field accessor + contents: + - queryset.field_accessor.* - title: Utils contents: - queryset.utils.*