diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md new file mode 100644 index 0000000..e9bb2b6 --- /dev/null +++ b/docs/api/exceptions.md @@ -0,0 +1,96 @@ + +# exceptions + +Gathers all exceptions thrown by ormar. + + +## AsyncOrmException Objects + +```python +class AsyncOrmException(Exception) +``` + +Base ormar Exception + + +## ModelDefinitionError Objects + +```python +class ModelDefinitionError(AsyncOrmException) +``` + +Raised for errors related to the model definition itself: + +* setting @property_field on method with arguments other than func(self) +* defining a Field without required parameters +* defining a model with more than one primary_key +* defining a model without primary_key +* setting primary_key column as pydantic_only + + +## ModelError Objects + +```python +class ModelError(AsyncOrmException) +``` + +Raised for initialization of model with non-existing field keyword. + + +## NoMatch Objects + +```python +class NoMatch(AsyncOrmException) +``` + +Raised for database queries that has no matching result (empty result). + + +## MultipleMatches Objects + +```python +class MultipleMatches(AsyncOrmException) +``` + +Raised for database queries that should return one row (i.e. get, first etc.) +but has multiple matching results in response. + + +## QueryDefinitionError Objects + +```python +class QueryDefinitionError(AsyncOrmException) +``` + +Raised for errors in query definition: + +* using contains or icontains filter with instance of the Model +* using Queryset.update() without filter and setting each flag to True +* using Queryset.delete() without filter and setting each flag to True + + +## RelationshipInstanceError Objects + +```python +class RelationshipInstanceError(AsyncOrmException) +``` + + +## ModelPersistenceError Objects + +```python +class ModelPersistenceError(AsyncOrmException) +``` + +Raised for update of models without primary_key set (cannot retrieve from db) +or for saving a model with relation to unsaved model (cannot extract fk value). + + +## SignalDefinitionError Objects + +```python +class SignalDefinitionError(AsyncOrmException) +``` + +Raised when non callable receiver is passed as signal callback. + diff --git a/docs/api/fields/base-field.md b/docs/api/fields/base-field.md new file mode 100644 index 0000000..64fa4fb --- /dev/null +++ b/docs/api/fields/base-field.md @@ -0,0 +1,292 @@ + +# fields.base + + +## BaseField Objects + +```python +class BaseField(FieldInfo) +``` + +BaseField serves as a parent class for all basic Fields in ormar. +It keeps all common parameters available for all fields as well as +set of useful functions. + +All values are kept as class variables, ormar Fields are never instantiated. +Subclasses pydantic.FieldInfo to keep the fields related +to pydantic field types like ConstrainedStr + + +#### \_\_type\_\_ + + +#### related\_name + + +#### column\_type + + +#### constraints + + +#### name + + +#### alias + + +#### primary\_key + + +#### autoincrement + + +#### nullable + + +#### index + + +#### unique + + +#### pydantic\_only + + +#### virtual + + +#### choices + + +#### to + + +#### through + + +#### default + + +#### server\_default + + +#### is\_valid\_uni\_relation + +```python + | @classmethod + | is_valid_uni_relation(cls) -> bool +``` + +Checks if field is a relation definition but only for ForeignKey relation, +so excludes ManyToMany fields, as well as virtual ForeignKey +(second side of FK relation). + +Is used to define if a field is a db ForeignKey column that +should be saved/populated when dealing with internal/own +Model columns only. + +**Returns**: + +`(bool)`: result of the check + + +#### get\_alias + +```python + | @classmethod + | get_alias(cls) -> str +``` + +Used to translate Model column names to database column names during db queries. + +**Returns**: + +`(str)`: returns custom database column name if defined by user, +otherwise field name in ormar/pydantic + + +#### is\_valid\_field\_info\_field + +```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 + + +#### 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] +``` + +Returns a FieldInfo instance with populated default +(static) or default_factory (function). +If the field is a autoincrement primary key the default is None. +Otherwise field have to has either default, or default_factory populated. + +If all default conditions fail None is returned. + +Used in converting to pydantic FieldInfo. + +**Arguments**: + +- `use_server (bool)`: flag marking if server_default should be +treated as default value, default False + +**Returns**: + +`(Optional[pydantic.FieldInfo])`: returns a call to pydantic.Field +which is returning a FieldInfo instance + + +#### get\_default + +```python + | @classmethod + | get_default(cls, use_server: bool = False) -> Any +``` + +Return default value for a field. +If the field is Callable the function is called and actual result is returned. +Used to populate default_values for pydantic Model in ormar Model Metaclass. + +**Arguments**: + +- `use_server (bool)`: flag marking if server_default should be +treated as default value, default False + +**Returns**: + +`(Any)`: default value for the field if set, otherwise implicit None + + +#### has\_default + +```python + | @classmethod + | has_default(cls, use_server: bool = True) -> bool +``` + +Checks if the field has default value set. + +**Arguments**: + +- `use_server (bool)`: flag marking if server_default should be +treated as default value, default False + +**Returns**: + +`(bool)`: result of the check if default value is set + + +#### is\_auto\_primary\_key + +```python + | @classmethod + | is_auto_primary_key(cls) -> bool +``` + +Checks if field is first a primary key and if it, +it's than check if it's set to autoincrement. +Autoincrement primary_key is nullable/optional. + +**Returns**: + +`(bool)`: result of the check for primary key and autoincrement + + +#### construct\_constraints + +```python + | @classmethod + | construct_constraints(cls) -> List +``` + +Converts list of ormar constraints into sqlalchemy ForeignKeys. +Has to be done dynamically as sqlalchemy binds ForeignKey to the table. +And we need a new ForeignKey for subclasses of current model + +**Returns**: + +`(List[sqlalchemy.schema.ForeignKey])`: List of sqlalchemy foreign keys - by default one. + + +#### get\_column + +```python + | @classmethod + | get_column(cls, name: str) -> sqlalchemy.Column +``` + +Returns definition of sqlalchemy.Column used in creation of sqlalchemy.Table. +Populates name, column type constraints, as well as a number of parameters like +primary_key, index, unique, nullable, default and server_default. + +**Arguments**: + +- `name (str)`: name of the db column - used if alias is not set + +**Returns**: + +`(sqlalchemy.Column)`: actual definition of the database column as sqlalchemy requires. + + +#### expand\_relationship + +```python + | @classmethod + | expand_relationship(cls, value: Any, child: Union["Model", "NewBaseModel"], to_register: bool = True, relation_name: str = None) -> Any +``` + +Function overwritten for relations, in basic field the value is returned as is. +For relations the child model is first constructed (if needed), +registered in relation and returned. +For relation fields the value can be a pk value (Any type of field), +dict (from Model) or actual instance/list of a "Model". + +**Arguments**: + +- `value (Any)`: a Model field value, returned untouched for non relation fields. +- `child (Union["Model", "NewBaseModel"])`: a child Model to register +- `to_register (bool)`: flag if the relation should be set in RelationshipManager + +**Returns**: + +`(Any)`: returns untouched value for normal fields, expands only for relations + diff --git a/docs/api/fields/decorators.md b/docs/api/fields/decorators.md new file mode 100644 index 0000000..28e052c --- /dev/null +++ b/docs/api/fields/decorators.md @@ -0,0 +1,28 @@ + +# decorators.property\_field + + +#### property\_field + +```python +property_field(func: Callable) -> Union[property, Callable] +``` + +Decorator to set a property like function on Model to be exposed +as field in dict() and fastapi response. +Although you can decorate a @property field like this and this will work, +mypy validation will complain about this. +Note that "fields" exposed like this do not go through validation. + +**Raises**: + +- `ModelDefinitionError`: if method has any other argument than self. + +**Arguments**: + +- `func (Callable)`: decorated function to be exposed + +**Returns**: + +`(Union[property, Callable])`: decorated function passed in func param, with set __property_field__ = True + diff --git a/docs/api/fields/foreign-key.md b/docs/api/fields/foreign-key.md new file mode 100644 index 0000000..f274e37 --- /dev/null +++ b/docs/api/fields/foreign-key.md @@ -0,0 +1,267 @@ + +# fields.foreign\_key + + +#### create\_dummy\_instance + +```python +create_dummy_instance(fk: Type["Model"], pk: Any = None) -> "Model" +``` + +Ormar never returns you a raw data. +So if you have a related field that has a value populated +it will construct you a Model instance out of it. + +Creates a "fake" instance of passed Model from pk value. +The instantiated Model has only pk value filled. +To achieve this __pk_only__ flag has to be passed as it skips the validation. + +If the nested related Models are required they are set with -1 as pk value. + +**Arguments**: + +- `fk (Model class)`: class of the related Model to which instance should be constructed +- `pk (Any)`: value of the primary_key column + +**Returns**: + +`(Model)`: Model instance populated with only pk + + +#### create\_dummy\_model + +```python +create_dummy_model(base_model: Type["Model"], pk_field: Type[Union[BaseField, "ForeignKeyField", "ManyToManyField"]]) -> Type["BaseModel"] +``` + +Used to construct a dummy pydantic model for type hints and pydantic validation. +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 + +**Returns**: + +`(pydantic.BaseModel)`: constructed dummy model + + +## UniqueColumns Objects + +```python +class UniqueColumns(UniqueConstraint) +``` + +Subclass of sqlalchemy.UniqueConstraint. +Used to avoid importing anything from sqlalchemy by user. + + +## ForeignKeyConstraint Objects + +```python +@dataclass +class ForeignKeyConstraint() +``` + +Internal container to store ForeignKey definitions used later +to produce sqlalchemy.ForeignKeys + + +#### name + + +#### ondelete + + +#### onupdate + + +#### ForeignKey + +```python +ForeignKey(to: Type["Model"], *, 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 +``` + +Despite a name it's a function that returns constructed ForeignKeyField. +This function is actually used in model declaration (as ormar.ForeignKey(ToModel)). + +Accepts number of relation setting parameters as well as all BaseField ones. + +**Arguments**: + +- `to (Model class)`: target related ormar Model +- `name (str)`: name of the database field - later called alias +- `unique (bool)`: parameter passed to sqlalchemy.ForeignKey, unique flag +- `nullable (bool)`: marks field as optional/ required +- `related_name (str)`: name of reversed FK relation populated for you on to model +- `virtual (bool)`: marks if relation is virtual. +It is for reversed FK and auto generated FK on through model in Many2Many relations. +- `onupdate (str)`: parameter passed to sqlalchemy.ForeignKey. +How to treat child rows on update of parent (the one where FK is defined) model. +- `ondelete (str)`: parameter passed to sqlalchemy.ForeignKey. +How to treat child rows on delete of parent (the one where FK is defined) model. +- `kwargs (Any)`: all other args to be populated by BaseField + +**Returns**: + +`(ForeignKeyField)`: ormar ForeignKeyField with relation to selected model + + +## ForeignKeyField Objects + +```python +class ForeignKeyField(BaseField) +``` + +Actual class returned from ForeignKey function call and stored in model_fields. + + +#### to + + +#### name + + +#### related\_name + + +#### virtual + + +#### \_extract\_model\_from\_sequence + +```python + | @classmethod + | _extract_model_from_sequence(cls, value: List, child: "Model", to_register: bool, relation_name: str) -> List["Model"] +``` + +Takes a list of Models and registers them on parent. +Registration is mutual, so children have also reference to parent. + +Used in reverse FK relations. + +**Arguments**: + +- `value (List)`: list of Model +- `child (Model)`: child/ related Model +- `to_register (bool)`: flag if the relation should be set in RelationshipManager + +**Returns**: + +`(List["Model"])`: list (if needed) registered Models + + +#### \_register\_existing\_model + +```python + | @classmethod + | _register_existing_model(cls, value: "Model", child: "Model", to_register: bool, relation_name: str) -> "Model" +``` + +Takes already created instance and registers it for parent. +Registration is mutual, so children have also reference to parent. + +Used in reverse FK relations and normal FK for single models. + +**Arguments**: + +- `value (Model)`: already instantiated Model +- `child (Model)`: child/ related Model +- `to_register (bool)`: flag if the relation should be set in RelationshipManager + +**Returns**: + +`(Model)`: (if needed) registered Model + + +#### \_construct\_model\_from\_dict + +```python + | @classmethod + | _construct_model_from_dict(cls, value: dict, child: "Model", to_register: bool, relation_name: str) -> "Model" +``` + +Takes a dictionary, creates a instance and registers it for parent. +If dictionary contains only one field and it's a pk it is a __pk_only__ model. +Registration is mutual, so children have also reference to parent. + +Used in normal FK for dictionaries. + +**Arguments**: + +- `value (dict)`: dictionary of a Model +- `child (Model)`: child/ related Model +- `to_register (bool)`: flag if the relation should be set in RelationshipManager + +**Returns**: + +`(Model)`: (if needed) registered Model + + +#### \_construct\_model\_from\_pk + +```python + | @classmethod + | _construct_model_from_pk(cls, value: Any, child: "Model", to_register: bool, relation_name: str) -> "Model" +``` + +Takes a pk value, creates a dummy instance and registers it for parent. +Registration is mutual, so children have also reference to parent. + +Used in normal FK for dictionaries. + +**Arguments**: + +- `value (Any)`: value of a related pk / fk column +- `child (Model)`: child/ related Model +- `to_register (bool)`: flag if the relation should be set in RelationshipManager + +**Returns**: + +`(Model)`: (if needed) registered Model + + +#### register\_relation + +```python + | @classmethod + | register_relation(cls, model: "Model", child: "Model", relation_name: str) -> None +``` + +Registers relation between parent and child in relation manager. +Relation manager is kep on each model (different instance). + +Used in Metaclass and sometimes some relations are missing +(i.e. cloned Models in fastapi might miss one). + +**Arguments**: + +- `model (Model class)`: parent model (with relation definition) +- `child (Model class)`: child model + + +#### expand\_relationship + +```python + | @classmethod + | expand_relationship(cls, value: Any, child: Union["Model", "NewBaseModel"], to_register: bool = True, relation_name: str = None) -> Optional[Union["Model", List["Model"]]] +``` + +For relations the child model is first constructed (if needed), +registered in relation and returned. +For relation fields the value can be a pk value (Any type of field), +dict (from Model) or actual instance/list of a "Model". + +Selects the appropriate constructor based on a passed value. + +**Arguments**: + +- `value (Any)`: a Model field value, returned untouched for non relation fields. +- `child (Union["Model", "NewBaseModel"])`: a child Model to register +- `to_register (bool)`: flag if the relation should be set in RelationshipManager + +**Returns**: + +`(Optional[Union["Model", List["Model"]]])`: returns a Model or a list of Models + diff --git a/docs/api/fields/many-to-many.md b/docs/api/fields/many-to-many.md new file mode 100644 index 0000000..d950ee6 --- /dev/null +++ b/docs/api/fields/many-to-many.md @@ -0,0 +1,59 @@ + +# fields.many\_to\_many + + +#### REF\_PREFIX + + +#### ManyToMany + +```python +ManyToMany(to: Type["Model"], through: Type["Model"], *, name: str = None, unique: bool = False, virtual: bool = False, **kwargs: Any) -> Any +``` + +Despite a name it's a function that returns constructed ManyToManyField. +This function is actually used in model declaration +(as ormar.ManyToMany(ToModel, through=ThroughModel)). + +Accepts number of relation setting parameters as well as all BaseField ones. + +**Arguments**: + +- `to (Model class)`: target related ormar Model +- `through (Model class)`: through model for m2m relation +- `name (str)`: name of the database field - later called alias +- `unique (bool)`: parameter passed to sqlalchemy.ForeignKey, unique flag +- `virtual (bool)`: marks if relation is virtual. +It is for reversed FK and auto generated FK on through model in Many2Many relations. +- `kwargs (Any)`: all other args to be populated by BaseField + +**Returns**: + +`(ManyToManyField)`: ormar ManyToManyField with m2m relation to selected model + + +## ManyToManyField Objects + +```python +class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol) +``` + +Actual class returned from ManyToMany function call and stored in model_fields. + + +#### through + + +#### 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 + diff --git a/docs/api/fields/model-fields.md b/docs/api/fields/model-fields.md new file mode 100644 index 0000000..0744438 --- /dev/null +++ b/docs/api/fields/model-fields.md @@ -0,0 +1,514 @@ + +# fields.model\_fields + + +#### is\_field\_nullable + +```python +is_field_nullable(nullable: Optional[bool], default: Any, server_default: Any, pydantic_only: Optional[bool]) -> bool +``` + +Checks if the given field should be nullable/ optional based on parameters given. + +**Arguments**: + +- `nullable (Optional[bool])`: flag explicit setting a column as nullable +- `default (Any)`: value or function to be called as default in python +- `server_default (Any)`: function to be called as default by sql server +- `pydantic_only (Optional[bool])`: flag if fields should not be included in the sql table + +**Returns**: + +`(bool)`: result of the check + + +#### is\_auto\_primary\_key + +```python +is_auto_primary_key(primary_key: bool, autoincrement: bool) -> bool +``` + +Checks if field is an autoincrement pk -> if yes it's optional. + +**Arguments**: + +- `primary_key (bool)`: flag if field is a pk field +- `autoincrement (bool)`: flag if field should be autoincrement + +**Returns**: + +`(bool)`: result of the check + + +## ModelFieldFactory Objects + +```python +class ModelFieldFactory() +``` + +Default field factory that construct Field classes and populated their values. + + +#### \_bases + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *args: Any, **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +#### validate + +```python + | @classmethod + | validate(cls, **kwargs: Any) -> None +``` + +Used to validate if all required parameters on a given field type are set. + +**Arguments**: + +- `kwargs (Any)`: all params passed during construction + + +## String Objects + +```python +class String(ModelFieldFactory, str) +``` + +String field factory that construct Field classes and populated their values. + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *, allow_blank: bool = True, strip_whitespace: bool = False, min_length: int = None, max_length: int = None, curtail_length: int = None, regex: str = None, **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +#### validate + +```python + | @classmethod + | validate(cls, **kwargs: Any) -> None +``` + +Used to validate if all required parameters on a given field type are set. + +**Arguments**: + +- `kwargs (Any)`: all params passed during construction + + +## Integer Objects + +```python +class Integer(ModelFieldFactory, int) +``` + +Integer field factory that construct Field classes and populated their values. + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *, minimum: int = None, maximum: int = None, multiple_of: int = None, **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## Text Objects + +```python +class Text(ModelFieldFactory, str) +``` + +Text field factory that construct Field classes and populated their values. + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *, allow_blank: bool = True, strip_whitespace: bool = False, **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## Float Objects + +```python +class Float(ModelFieldFactory, float) +``` + +Float field factory that construct Field classes and populated their values. + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *, minimum: float = None, maximum: float = None, multiple_of: int = None, **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## DateTime Objects + +```python +class DateTime(ModelFieldFactory, datetime.datetime) +``` + +DateTime field factory that construct Field classes and populated their values. + + +#### \_type + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## Date Objects + +```python +class Date(ModelFieldFactory, datetime.date) +``` + +Date field factory that construct Field classes and populated their values. + + +#### \_type + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## Time Objects + +```python +class Time(ModelFieldFactory, datetime.time) +``` + +Time field factory that construct Field classes and populated their values. + + +#### \_type + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## JSON Objects + +```python +class JSON(ModelFieldFactory, pydantic.Json) +``` + +JSON field factory that construct Field classes and populated their values. + + +#### \_type + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## BigInteger Objects + +```python +class BigInteger(Integer, int) +``` + +BigInteger field factory that construct Field classes and populated their values. + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *, minimum: int = None, maximum: int = None, multiple_of: int = None, **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +## Decimal Objects + +```python +class Decimal(ModelFieldFactory, decimal.Decimal) +``` + +Decimal field factory that construct Field classes and populated their values. + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *, minimum: float = None, maximum: float = None, multiple_of: int = None, precision: int = None, scale: int = None, max_digits: int = None, decimal_places: int = None, **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + + +#### validate + +```python + | @classmethod + | validate(cls, **kwargs: Any) -> None +``` + +Used to validate if all required parameters on a given field type are set. + +**Arguments**: + +- `kwargs (Any)`: all params passed during construction + + +## UUID Objects + +```python +class UUID(ModelFieldFactory, uuid.UUID) +``` + +UUID field factory that construct Field classes and populated their values. + + +#### \_type + + +#### \_\_new\_\_ + +```python + | __new__(cls, *, uuid_format: str = "hex", **kwargs: Any) -> Type[BaseField] +``` + + +#### get\_column\_type + +```python + | @classmethod + | get_column_type(cls, **kwargs: Any) -> Any +``` + +Return proper type of db column for given field type. +Accepts required and optional parameters that each column type accepts. + +**Arguments**: + +- `kwargs (Any)`: key, value pairs of sqlalchemy options + +**Returns**: + +`(sqlalchemy Column)`: initialized column with proper options + diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..2081c16 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,16 @@ +Contains documentation of the `ormar` internal API. + +Note that this is a technical part of the documentation intended for `ormar` contributors. + +!!!note + For completeness as of now even the internal and special methods are documented and exposed in API docs. + +!!!warning + The current API docs version is a beta and not all methods are documented, + also some of redundant items are included since it was partially auto generated. + +!!!danger + Ormar is still under development, and the **internals can change at any moment**. + + You shouldn't rely even on the "public" methods if they are not documented in the + normal part of the docs. \ No newline at end of file diff --git a/docs/api/models/helpers/models.md b/docs/api/models/helpers/models.md new file mode 100644 index 0000000..ddeb85d --- /dev/null +++ b/docs/api/models/helpers/models.md @@ -0,0 +1,64 @@ + +# models.helpers.models + + +#### populate\_default\_options\_values + +```python +populate_default_options_values(new_model: Type["Model"], model_fields: Dict) -> None +``` + +Sets all optional Meta values to it's defaults +and set model_fields that were already previously extracted. + +Here should live all options that are not overwritten/set for all models. + +Current options are: +* constraints = [] +* abstract = False + +**Arguments**: + +- `new_model (Model class)`: newly constructed Model +- `model_fields (Union[Dict[str, type], Dict])`: + + +#### extract\_annotations\_and\_default\_vals + +```python +extract_annotations_and_default_vals(attrs: Dict) -> Tuple[Dict, Dict] +``` + +Extracts annotations from class namespace dict and triggers +extraction of ormar model_fields. + +**Arguments**: + +- `attrs (Dict)`: namespace of the class created + +**Returns**: + +`(Tuple[Dict, Dict])`: namespace of the class updated, dict of extracted model_fields + + +#### validate\_related\_names\_in\_relations + +```python +validate_related_names_in_relations(model_fields: Dict, new_model: Type["Model"]) -> None +``` + +Performs a validation of relation_names in relation fields. +If multiple fields are leading to the same related model +only one can have empty related_name param +(populated by default as model.name.lower()+'s'). +Also related_names have to be unique for given related model. + +**Raises**: + +- `ModelDefinitionError`: if validation of related_names fail + +**Arguments**: + +- `model_fields (Dict[str, ormar.Field])`: dictionary of declared ormar model fields +- `new_model (Model class)`: + diff --git a/docs/api/models/helpers/pydantic.md b/docs/api/models/helpers/pydantic.md new file mode 100644 index 0000000..8b38eaf --- /dev/null +++ b/docs/api/models/helpers/pydantic.md @@ -0,0 +1,122 @@ + +# models.helpers.pydantic + + +#### create\_pydantic\_field + +```python +create_pydantic_field(field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]) -> None +``` + +Registers pydantic field on through model that leads to passed model +and is registered as field_name passed. + +Through model is fetched from through attributed on passed model_field. + +**Arguments**: + +- `field_name (str)`: field name to register +- `model (Model class)`: type of field to register +- `model_field (ManyToManyField class)`: relation field from which through model is extracted + + +#### get\_pydantic\_field + +```python +get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField" +``` + +Extracts field type and if it's required from Model model_fields by passed +field_name. Returns a pydantic field with type of field_name field type. + +**Arguments**: + +- `field_name (str)`: field name to fetch from Model and name of pydantic field +- `model (Model class)`: type of field to register + +**Returns**: + +`(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 + +```python +populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict] +``` + +Extracts ormar fields from annotations (deprecated) and from namespace +dictionary of the class. Fields declared on model are all subclasses of the +BaseField class. + +Trigger conversion of ormar field into pydantic FieldInfo, which has all needed +paramaters saved. + +Overwrites the annotations of ormar fields to corresponding types declared on +ormar fields (constructed dynamically for relations). +Those annotations are later used by pydantic to construct it's own fields. + +**Arguments**: + +- `attrs (Dict)`: current class namespace + +**Returns**: + +`(Tuple[Dict, Dict])`: namespace of the class updated, dict of extracted model_fields + + +#### get\_pydantic\_base\_orm\_config + +```python +get_pydantic_base_orm_config() -> Type[BaseConfig] +``` + +Returns empty pydantic Config with orm_mode set to True. + +**Returns**: + +`(pydantic Config)`: empty default config with orm_mode set. + + +#### get\_potential\_fields + +```python +get_potential_fields(attrs: Dict) -> Dict +``` + +Gets all the fields in current class namespace that are Fields. + +**Arguments**: + +- `attrs (Dict)`: current class namespace + +**Returns**: + +`(Dict)`: extracted fields that are ormar Fields + diff --git a/docs/api/models/helpers/relations.md b/docs/api/models/helpers/relations.md new file mode 100644 index 0000000..83b30a3 --- /dev/null +++ b/docs/api/models/helpers/relations.md @@ -0,0 +1,154 @@ + +# models.helpers.relations + + +#### alias\_manager + + +#### register\_relation\_on\_build + +```python +register_relation_on_build(new_model: Type["Model"], field_name: str) -> None +``` + +Registers ForeignKey relation in alias_manager to set a table_prefix. +Registration include also reverse relation side to be able to join both sides. + +Relation is registered by model name and relation field name to allow for multiple +relations between two Models that needs to have different +aliases for proper sql joins. + +**Arguments**: + +- `new_model (Model class)`: constructed model +- `field_name (str)`: name of the related field + + +#### register\_many\_to\_many\_relation\_on\_build + +```python +register_many_to_many_relation_on_build(new_model: Type["Model"], field: Type[ManyToManyField], field_name: str) -> None +``` + +Registers connection between through model and both sides of the m2m relation. +Registration include also reverse relation side to be able to join both sides. + +Relation is registered by model name and relation field name to allow for multiple +relations between two Models that needs to have different +aliases for proper sql joins. + +By default relation name is a model.name.lower(). + +**Arguments**: + +- `field_name (str)`: name of the relation key +- `new_model (Model class)`: model on which m2m field is declared +- `field (ManyToManyField class)`: relation field + + +#### expand\_reverse\_relationships + +```python +expand_reverse_relationships(model: Type["Model"]) -> None +``` + +Iterates through model_fields of given model and verifies if all reverse +relation have been populated on related models. + +If the reverse relation has not been set before it's set here. + +**Arguments**: + +- `model (Model class)`: model on which relation should be checked and registered + + +#### register\_reverse\_model\_fields + +```python +register_reverse_model_fields(model: Type["Model"], child: Type["Model"], related_name: str, model_field: Type["ForeignKeyField"]) -> None +``` + +Registers reverse ForeignKey field on related model. +By default it's name.lower()+'s' of the model on which relation is defined. + +But if the related_model name is provided it's registered with that name. +Autogenerated reverse fields also set related_name to the original field name. + +**Arguments**: + +- `model (Model class)`: related model on which reverse field should be defined +- `child (Model class)`: parent model with relation definition +- `related_name (str)`: name by which reverse key should be registered +- `model_field (relation Field)`: original relation ForeignKey field + + +#### register\_relation\_in\_alias\_manager + +```python +register_relation_in_alias_manager(new_model: Type["Model"], field: Type[ForeignKeyField], field_name: str) -> None +``` + +Registers the relation (and reverse relation) in alias manager. +The m2m relations require registration of through model between +actual end models of the relation. + +Delegates the actual registration to: +m2m - register_many_to_many_relation_on_build +fk - register_relation_on_build + +**Arguments**: + +- `new_model (Model class)`: model on which relation field is declared +- `field (ForeignKey or ManyToManyField class)`: relation field +- `field_name (str)`: name of the relation key + + +#### verify\_related\_name\_dont\_duplicate + +```python +verify_related_name_dont_duplicate(child: Type["Model"], parent_model: Type["Model"], related_name: str) -> None +``` + +Verifies whether the used related_name (regardless of the fact if user defined or +auto generated) is already used on related model, but is connected with other model +than the one that we connect right now. + +**Raises**: + +- `ModelDefinitionError`: if name is already used but lead to different related +model + +**Arguments**: + +- `child (ormar.models.metaclass.ModelMetaclass)`: related Model class +- `parent_model (ormar.models.metaclass.ModelMetaclass)`: parent Model class +- `related_name ()`: + +**Returns**: + +`(None)`: None + + +#### reverse\_field\_not\_already\_registered + +```python +reverse_field_not_already_registered(child: Type["Model"], child_model_name: str, parent_model: Type["Model"]) -> bool +``` + +Checks if child is already registered in parents pydantic fields. + +**Raises**: + +- `ModelDefinitionError`: if related name is already used but lead to different +related model + +**Arguments**: + +- `child (ormar.models.metaclass.ModelMetaclass)`: related Model class +- `child_model_name (str)`: related_name of the child if provided +- `parent_model (ormar.models.metaclass.ModelMetaclass)`: parent Model class + +**Returns**: + +`(bool)`: result of the check + diff --git a/docs/api/models/helpers/sqlalchemy.md b/docs/api/models/helpers/sqlalchemy.md new file mode 100644 index 0000000..87b6d0e --- /dev/null +++ b/docs/api/models/helpers/sqlalchemy.md @@ -0,0 +1,145 @@ + +# models.helpers.sqlalchemy + + +#### adjust\_through\_many\_to\_many\_model + +```python +adjust_through_many_to_many_model(model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField]) -> None +``` + +Registers m2m relation on through model. +Sets ormar.ForeignKey from through model to both child and parent models. +Sets sqlalchemy.ForeignKey to both child and parent models. +Sets pydantic fields with child and parent model types. + +**Arguments**: + +- `model (Model class)`: model on which relation is declared +- `child (Model class)`: model to which m2m relation leads +- `model_field (ManyToManyField)`: relation field defined in parent model + + +#### create\_and\_append\_m2m\_fk + +```python +create_and_append_m2m_fk(model: Type["Model"], model_field: Type[ManyToManyField]) -> None +``` + +Registers sqlalchemy Column with sqlalchemy.ForeignKey leadning to the model. + +Newly created field is added to m2m relation through model Meta columns and table. + +**Arguments**: + +- `model (Model class)`: Model class to which FK should be created +- `model_field (ManyToManyField field)`: field with ManyToMany relation + + +#### check\_pk\_column\_validity + +```python +check_pk_column_validity(field_name: str, field: BaseField, pkname: Optional[str]) -> Optional[str] +``` + +Receives the field marked as primary key and verifies if the pkname +was not already set (only one allowed per model) and if field is not marked +as pydantic_only as it needs to be a database field. + +**Raises**: + +- `ModelDefintionError`: if pkname already set or field is pydantic_only + +**Arguments**: + +- `field_name (str)`: name of field +- `field (BaseField)`: ormar.Field +- `pkname (Optional[str])`: already set pkname + +**Returns**: + +`(str)`: name of the field that should be set as pkname + + +#### sqlalchemy\_columns\_from\_model\_fields + +```python +sqlalchemy_columns_from_model_fields(model_fields: Dict, new_model: Type["Model"]) -> Tuple[Optional[str], List[sqlalchemy.Column]] +``` + +Iterates over declared on Model model fields and extracts fields that +should be treated as database fields. + +If the model is empty it sets mandatory id field as primary key +(used in through models in m2m relations). + +Triggers a validation of relation_names in relation fields. If multiple fields +are leading to the same related model only one can have empty related_name param. +Also related_names have to be unique. + +Trigger validation of primary_key - only one and required pk can be set, +cannot be pydantic_only. + +Append fields to columns if it's not pydantic_only, +virtual ForeignKey or ManyToMany field. + +**Raises**: + +- `ModelDefinitionError`: if validation of related_names fail, +or pkname validation fails. + +**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 + + +#### populate\_meta\_tablename\_columns\_and\_pk + +```python +populate_meta_tablename_columns_and_pk(name: str, new_model: Type["Model"]) -> Type["Model"] +``` + +Sets Model tablename if it's not already set in Meta. +Default tablename if not present is class name lower + s (i.e. Bed becomes -> beds) + +Checks if Model's Meta have pkname and columns set. +If not calls the sqlalchemy_columns_from_model_fields to populate +columns from ormar.fields definitions. + +**Raises**: + +- `ModelDefinitionError`: if pkname is not present raises ModelDefinitionError. +Each model has to have pk. + +**Arguments**: + +- `name (str)`: name of the current Model +- `new_model (ormar.models.metaclass.ModelMetaclass)`: currently constructed Model + +**Returns**: + +`(ormar.models.metaclass.ModelMetaclass)`: Model with populated pkname and columns in Meta + + +#### populate\_meta\_sqlalchemy\_table\_if\_required + +```python +populate_meta_sqlalchemy_table_if_required(meta: "ModelMeta") -> None +``` + +Constructs sqlalchemy table out of columns and parameters set on Meta class. +It populates name, metadata, columns and constraints. + +**Arguments**: + +- `meta (Model class Meta)`: Meta class of the Model without sqlalchemy table constructed + +**Returns**: + +`(Model class)`: class with populated Meta.table + diff --git a/docs/api/models/mixins/alias-mixin.md b/docs/api/models/mixins/alias-mixin.md new file mode 100644 index 0000000..c3a48a4 --- /dev/null +++ b/docs/api/models/mixins/alias-mixin.md @@ -0,0 +1,90 @@ + +# models.mixins.alias\_mixin + + +## AliasMixin Objects + +```python +class AliasMixin() +``` + +Used to translate field names into database column names. + + +#### get\_column\_alias + +```python + | @classmethod + | get_column_alias(cls, field_name: str) -> str +``` + +Returns db alias (column name in db) for given ormar field. +For fields without alias field name is returned. + +**Arguments**: + +- `field_name (str)`: name of the field to get alias from + +**Returns**: + +`(str)`: alias (db name) if set, otherwise passed name + + +#### get\_column\_name\_from\_alias + +```python + | @classmethod + | get_column_name_from_alias(cls, alias: str) -> str +``` + +Returns ormar field name for given db alias (column name in db). +If field do not have alias it's returned as is. + +**Arguments**: + +- `alias (str)`: + +**Returns**: + +`(str)`: field name if set, otherwise passed alias (db name) + + +#### translate\_columns\_to\_aliases + +```python + | @classmethod + | translate_columns_to_aliases(cls, new_kwargs: Dict) -> Dict +``` + +Translates dictionary of model fields changing field names into aliases. +If field has no alias the field name remains intact. +Only fields present in the dictionary are translated. + +**Arguments**: + +- `new_kwargs (Dict)`: dict with fields names and their values + +**Returns**: + +`(Dict)`: dict with aliases and their values + + +#### translate\_aliases\_to\_columns + +```python + | @classmethod + | translate_aliases_to_columns(cls, new_kwargs: Dict) -> Dict +``` + +Translates dictionary of model fields changing aliases into field names. +If field has no alias the alias is already a field name. +Only fields present in the dictionary are translated. + +**Arguments**: + +- `new_kwargs (Dict)`: dict with aliases and their values + +**Returns**: + +`(Dict)`: dict with fields names and their values + diff --git a/docs/api/models/mixins/excludable-mixin.md b/docs/api/models/mixins/excludable-mixin.md new file mode 100644 index 0000000..a4d9c79 --- /dev/null +++ b/docs/api/models/mixins/excludable-mixin.md @@ -0,0 +1,206 @@ + +# models.mixins.excludable\_mixin + + +## ExcludableMixin Objects + +```python +class ExcludableMixin(RelationMixin) +``` + +Used to include/exclude given set of fields on models during load and dict() calls. + + +#### get\_child + +```python + | @staticmethod + | get_child(items: Union[Set, Dict, None], key: str = None) -> Union[Set, Dict, None] +``` + +Used to get nested dictionaries keys if they exists otherwise returns +passed items. + +**Arguments**: + +- `items (Union[Set, Dict, None])`: bag of items to include or exclude +- `key (str)`: name of the child to extract + +**Returns**: + +`(Union[Set, Dict, None])`: child extracted from items if exists + + +#### get\_excluded + +```python + | @staticmethod + | get_excluded(exclude: Union[Set, Dict, None], key: str = None) -> Union[Set, Dict, None] +``` + +Proxy to ExcludableMixin.get_child for exclusions. + +**Arguments**: + +- `exclude (Union[Set, Dict, None])`: bag of items to exclude +- `key (str)`: name of the child to extract + +**Returns**: + +`(Union[Set, Dict, None])`: child extracted from items if exists + + +#### get\_included + +```python + | @staticmethod + | get_included(include: Union[Set, Dict, None], key: str = None) -> Union[Set, Dict, None] +``` + +Proxy to ExcludableMixin.get_child for inclusions. + +**Arguments**: + +- `include (Union[Set, Dict, None])`: bag of items to include +- `key (str)`: name of the child to extract + +**Returns**: + +`(Union[Set, Dict, None])`: child extracted from items if exists + + +#### is\_excluded + +```python + | @staticmethod + | is_excluded(exclude: Union[Set, Dict, None], key: str = None) -> bool +``` + +Checks if given key should be excluded on model/ dict. + +**Arguments**: + +- `exclude (Union[Set, Dict, None])`: bag of items to exclude +- `key (str)`: name of the child to extract + +**Returns**: + +`(Union[Set, Dict, None])`: child extracted from items if exists + + +#### is\_included + +```python + | @staticmethod + | is_included(include: Union[Set, Dict, None], key: str = None) -> bool +``` + +Checks if given key should be included on model/ dict. + +**Arguments**: + +- `include (Union[Set, Dict, None])`: bag of items to include +- `key (str)`: name of the child to extract + +**Returns**: + +`(Union[Set, Dict, None])`: child extracted from items if exists + + +#### \_populate\_pk\_column + +```python + | @staticmethod + | _populate_pk_column(model: Type["Model"], columns: List[str], use_alias: bool = False) -> List[str] +``` + +Adds primary key column/alias (depends on use_alias flag) to list of +column names that are selected. + +**Arguments**: + +- `model (Type["Model"])`: model on columns are selected +- `columns (List[str])`: list of columns names +- `use_alias (bool)`: flag to set if aliases or field names should be used + +**Returns**: + +`(List[str])`: list of columns names with pk column in it + + +#### own\_table\_columns + +```python + | @classmethod + | own_table_columns(cls, model: Type["Model"], fields: Optional[Union[Set, Dict]], exclude_fields: Optional[Union[Set, Dict]], use_alias: bool = False) -> List[str] +``` + +Returns list of aliases or field names for given model. +Aliases/names switch is use_alias flag. + +If provided only fields included in fields will be returned. +If provided fields in exclude_fields will be excluded in return. + +Primary key field is always added and cannot be excluded (will be added anyway). + +**Arguments**: + +- `model (Type["Model"])`: model on columns are selected +- `fields (Optional[Union[Set, Dict]])`: set/dict of fields to include +- `exclude_fields (Optional[Union[Set, Dict]])`: set/dict of fields to exclude +- `use_alias (bool)`: flag if aliases or field names should be used + +**Returns**: + +`(List[str])`: list of column field names or aliases + + +#### \_update\_excluded\_with\_related\_not\_required + +```python + | @classmethod + | _update_excluded_with_related_not_required(cls, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None], nested: bool = False) -> Union[Set, Dict] +``` + +Used during generation of the dict(). +To avoid cyclical references and max recurrence limit nested models have to +exclude related models that are not mandatory. + +For a main model (not nested) only nullable related field names are added to +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**: + +`(Union[Set, Dict])`: set or dict with excluded fields added. + + +#### get\_names\_to\_exclude + +```python + | @classmethod + | get_names_to_exclude(cls, fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None) -> Set +``` + +Returns a set of models field names that should be explicitly excluded +during model initialization. + +Those fields will be set to None to avoid ormar/pydantic setting default +values on them. They should be returned as None in any case. + +Used in parsing data from database rows that construct Models by initializing +them with dicts constructed from those db rows. + +**Arguments**: + +- `fields (Optional[Union[Set, Dict]])`: set/dict of fields to include +- `exclude_fields (Optional[Union[Set, Dict]])`: set/dict of fields to exclude + +**Returns**: + +`(Set)`: set of field names that should be excluded + diff --git a/docs/api/models/mixins/merge-model-mixin.md b/docs/api/models/mixins/merge-model-mixin.md new file mode 100644 index 0000000..a7ccbd9 --- /dev/null +++ b/docs/api/models/mixins/merge-model-mixin.md @@ -0,0 +1,60 @@ + +# models.mixins.merge\_mixin + + +## MergeModelMixin Objects + +```python +class MergeModelMixin() +``` + +Used to merge models instances returned by database, +but already initialized to ormar Models.keys + +Models can duplicate during joins when parent model has multiple child rows, +in the end all parent (main) models should be unique. + + +#### merge\_instances\_list + +```python + | @classmethod + | merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"] +``` + +Merges a list of models into list of unique models. + +Models can duplicate during joins when parent model has multiple child rows, +in the end all parent (main) models should be unique. + +**Arguments**: + +- `result_rows (List["Model"])`: list of already initialized Models with child models +populated, each instance is one row in db and some models can duplicate + +**Returns**: + +`(List["Model"])`: list of merged models where each main model is unique + + +#### merge\_two\_instances + +```python + | @classmethod + | merge_two_instances(cls, one: "Model", other: "Model") -> "Model" +``` + +Merges current (other) Model and previous one (one) and returns the current +Model instance with data merged from previous one. + +If needed it's calling itself recurrently and merges also children models. + +**Arguments**: + +- `one (Model)`: previous model instance +- `other (Model)`: current model instance + +**Returns**: + +`(Model)`: current Model instance with data merged from previous one. + diff --git a/docs/api/models/mixins/prefetch-query-mixin.md b/docs/api/models/mixins/prefetch-query-mixin.md new file mode 100644 index 0000000..05d8b8b --- /dev/null +++ b/docs/api/models/mixins/prefetch-query-mixin.md @@ -0,0 +1,100 @@ + +# models.mixins.prefetch\_mixin + + +## PrefetchQueryMixin Objects + +```python +class PrefetchQueryMixin(RelationMixin) +``` + +Used in PrefetchQuery to extract ids and names of models to prefetch. + + +#### get\_clause\_target\_and\_filter\_column\_name + +```python + | @staticmethod + | get_clause_target_and_filter_column_name(parent_model: Type["Model"], target_model: Type["Model"], reverse: bool, related: str) -> Tuple[Type["Model"], str] +``` + +Returns Model on which query clause should be performed and name of the column. + +**Arguments**: + +- `parent_model (Type[Model])`: related model that the relation lead to +- `target_model (Type[Model])`: model on which query should be perfomed +- `reverse (bool)`: flag if the relation is reverse +- `related (str)`: name of the relation field + +**Returns**: + +`(Tuple[Type[Model], str])`: Model on which query clause should be performed and name of the column + + +#### get\_column\_name\_for\_id\_extraction + +```python + | @staticmethod + | get_column_name_for_id_extraction(parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool) -> str +``` + +Returns name of the column that should be used to extract ids from model. +Depending on the relation side it's either primary key column of parent model +or field name specified by related parameter. + +**Arguments**: + +- `parent_model (Type[Model])`: model from which id column should be extracted +- `reverse (bool)`: flag if the relation is reverse +- `related (str)`: name of the relation field +- `use_raw (bool)`: flag if aliases or field names should be used + +**Returns**: + +`()`: + + +#### get\_related\_field\_name + +```python + | @classmethod + | get_related_field_name(cls, target_field: Type["BaseField"]) -> str +``` + +Returns name of the relation field that should be used in prefetch query. +This field is later used to register relation in prefetch query, +populate relations dict, and populate nested model in prefetch query. + +**Arguments**: + +- `target_field (Type[BaseField])`: relation field that should be used in prefetch + +**Returns**: + +`(str)`: name of the field + + +#### get\_filtered\_names\_to\_extract + +```python + | @classmethod + | get_filtered_names_to_extract(cls, prefetch_dict: Dict) -> List +``` + +Returns list of related fields names that should be followed to prefetch related +models from. + +List of models is translated into dict to assure each model is extracted only +once in one query, that's why this function accepts prefetch_dict not list. + +Only relations from current model are returned. + +**Arguments**: + +- `prefetch_dict (Dict)`: dictionary of fields to extract + +**Returns**: + +`(List)`: list of fields names to extract + diff --git a/docs/api/models/mixins/relation-mixin.md b/docs/api/models/mixins/relation-mixin.md new file mode 100644 index 0000000..5e94eb9 --- /dev/null +++ b/docs/api/models/mixins/relation-mixin.md @@ -0,0 +1,93 @@ + +# models.mixins.relation\_mixin + + +## RelationMixin Objects + +```python +class RelationMixin() +``` + +Used to return relation fields/names etc. from given model + + +#### extract\_db\_own\_fields + +```python + | @classmethod + | extract_db_own_fields(cls) -> Set +``` + +Returns only fields that are stored in the own database table, exclude all +related fields. + +**Returns**: + +`(Set)`: set of model fields with relation fields excluded + + +#### extract\_related\_fields + +```python + | @classmethod + | extract_related_fields(cls) -> List +``` + +Returns List of ormar Fields for all relations declared on a model. +List is cached in cls._related_fields for quicker access. + +**Returns**: + +`(List)`: list of related fields + + +#### extract\_related\_names + +```python + | @classmethod + | extract_related_names(cls) -> Set +``` + +Returns List of fields names for all relations declared on a model. +List is cached in cls._related_names for quicker access. + +**Returns**: + +`(List)`: list of related fields names + + +#### \_extract\_db\_related\_names + +```python + | @classmethod + | _extract_db_related_names(cls) -> Set +``` + +Returns only fields that are stored in the own database table, exclude +related fields that are not stored as foreign keys on given model. + +**Returns**: + +`(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 + diff --git a/docs/api/models/mixins/save-prepare-mixin.md b/docs/api/models/mixins/save-prepare-mixin.md new file mode 100644 index 0000000..d8c7e57 --- /dev/null +++ b/docs/api/models/mixins/save-prepare-mixin.md @@ -0,0 +1,93 @@ + +# models.mixins.save\_mixin + + +## SavePrepareMixin Objects + +```python +class SavePrepareMixin(RelationMixin, AliasMixin) +``` + +Used to prepare models to be saved in database + + +#### prepare\_model\_to\_save + +```python + | @classmethod + | prepare_model_to_save(cls, new_kwargs: dict) -> dict +``` + +Combines all preparation methods before saving. +Removes primary key for if it's nullable or autoincrement pk field, +and it's set to None. +Substitute related models with their primary key values as fk column. +Populates the default values for field with default set and no value. +Translate columns into aliases (db names). + +**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 + +```python + | @classmethod + | _remove_pk_from_kwargs(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 + + +#### substitute\_models\_with\_pks + +```python + | @classmethod + | substitute_models_with_pks(cls, model_dict: Dict) -> Dict +``` + +Receives dictionary of model that is about to be saved and changes all related +models that are stored as foreign keys to their fk value. + +**Arguments**: + +- `model_dict (Dict)`: dictionary of model that is about to be saved + +**Returns**: + +`(Dict)`: dictionary of model that is about to be saved + + +#### populate\_default\_values + +```python + | @classmethod + | populate_default_values(cls, new_kwargs: Dict) -> Dict +``` + +Receives dictionary of model that is about to be saved and populates the default +value on the fields that have the default value set, but no actual value was +passed by the user. + +**Arguments**: + +- `new_kwargs (Dict)`: dictionary of model that is about to be saved + +**Returns**: + +`(Dict)`: dictionary of model that is about to be saved + diff --git a/docs/api/models/model-metaclass.md b/docs/api/models/model-metaclass.md new file mode 100644 index 0000000..b9e372a --- /dev/null +++ b/docs/api/models/model-metaclass.md @@ -0,0 +1,340 @@ + +# models.metaclass + + +#### PARSED\_FIELDS\_KEY + + +#### CONFIG\_KEY + + +## ModelMeta Objects + +```python +class ModelMeta() +``` + +Class used for type hinting. +Users can subclass this one for convenience but it's not required. +The only requirement is that ormar.Model has to have inner class with name Meta. + + +#### tablename + + +#### table + + +#### metadata + + +#### database + + +#### columns + + +#### constraints + + +#### pkname + + +#### model\_fields + + +#### alias\_manager + + +#### property\_fields + + +#### signals + + +#### abstract + + +#### check\_if\_field\_has\_choices + +```python +check_if_field_has_choices(field: Type[BaseField]) -> bool +``` + +Checks if given field has choices populated. +A if it has one, a validator for this field needs to be attached. + +**Arguments**: + +- `field (BaseField)`: ormar field to check + +**Returns**: + +`(bool)`: result of the check + + +#### choices\_validator + +```python +choices_validator(cls: Type["Model"], values: Dict[str, Any]) -> Dict[str, Any] +``` + +Validator that is attached to pydantic model pre root validators. +Validator checks if field value is in field.choices list. + +**Raises**: + +- `ValueError`: if field value is outside of allowed choices. + +**Arguments**: + +- `cls (Model class)`: constructed class +- `values (Dict[str, Any])`: dictionary of field values (pydantic side) + +**Returns**: + +`(Dict[str, Any])`: values if pass validation, otherwise exception is raised + + +#### populate\_choices\_validators + +```python +populate_choices_validators(model: Type["Model"]) -> None +``` + +Checks if Model has any fields with choices set. +If yes it adds choices validation into pre root validators. + +**Arguments**: + +- `model (Model class)`: newly constructed Model + + +#### add\_cached\_properties + +```python +add_cached_properties(new_model: Type["Model"]) -> None +``` + +Sets cached properties for both pydantic and ormar models. + +Quick access fields are fields grabbed in getattribute to skip all checks. + +Related fields and names are populated to None as they can change later. +When children models are constructed they can modify parent to register itself. + +All properties here are used as "cache" to not recalculate them constantly. + +**Arguments**: + +- `new_model (Model class)`: newly constructed Model + + +#### meta\_field\_not\_set + +```python +meta_field_not_set(model: Type["Model"], field_name: str) -> bool +``` + +Checks if field with given name is already present in model.Meta. +Then check if it's set to something truthful +(in practice meaning not None, as it's non or ormar Field only). + +**Arguments**: + +- `model (Model class)`: newly constructed model +- `field_name (str)`: name of the ormar field + +**Returns**: + +`(bool)`: result of the check + + +#### add\_property\_fields + +```python +add_property_fields(new_model: Type["Model"], attrs: Dict) -> None +``` + +Checks class namespace for properties or functions with __property_field__. +If attribute have __property_field__ it was decorated with @property_field. + +Functions like this are exposed in dict() (therefore also fastapi result). +Names of property fields are cached for quicker access / extraction. + +**Arguments**: + +- `new_model (Model class)`: newly constructed model +- `attrs (Dict[str, str])`: + + +#### register\_signals + +```python +register_signals(new_model: Type["Model"]) -> None +``` + +Registers on model's SignalEmmiter and sets pre defined signals. +Predefined signals are (pre/post) + (save/update/delete). + +Signals are emitted in both model own methods and in selected queryset ones. + +**Arguments**: + +- `new_model (Model class)`: newly constructed model + + +#### update\_attrs\_and\_fields + +```python +update_attrs_and_fields(attrs: Dict, new_attrs: Dict, model_fields: Dict, new_model_fields: Dict, new_fields: Set) -> Dict +``` + +Updates __annotations__, values of model fields (so pydantic FieldInfos) +as well as model.Meta.model_fields definitions from parents. + +**Arguments**: + +- `attrs (Dict)`: new namespace for class being constructed +- `new_attrs (Dict)`: part of the namespace extracted from parent class +- `model_fields (Dict[str, BaseField])`: ormar fields in defined in current class +- `new_model_fields (Dict[str, BaseField])`: ormar fields defined in parent classes +- `new_fields (Set[str])`: set of new fields names + + +#### verify\_constraint\_names + +```python +verify_constraint_names(base_class: "Model", model_fields: Dict, parent_value: List) -> None +``` + +Verifies if redefined fields that are overwritten in subclasses did not remove +any name of the column that is used in constraint as it will fail in sqlalchemy +Table creation. + +**Arguments**: + +- `base_class (Model or model parent class)`: one of the parent classes +- `model_fields (Dict[str, BaseField])`: ormar fields in defined in current class +- `parent_value (List)`: list of base class constraints + + +#### update\_attrs\_from\_base\_meta + +```python +update_attrs_from_base_meta(base_class: "Model", attrs: Dict, model_fields: Dict) -> None +``` + +Updates Meta parameters in child from parent if needed. + +**Arguments**: + +- `base_class (Model or model parent class)`: one of the parent classes +- `attrs (Dict)`: new namespace for class being constructed +- `model_fields (Dict[str, BaseField])`: ormar fields in defined in current class + + +#### 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 the key parameters [databse, metadata, property_fields and constraints] +and fields from parent models. Overwrites them if needed. + +Only abstract classes can be subclassed. + +Since relation fields requires different related_name for different children + + +**Raises**: + +- `ModelDefinitionError`: if non abstract model is subclassed + +**Arguments**: + +- `base_class (Model or model parent class)`: one of the parent classes +- `curr_class (Model or model parent class)`: current constructed class +- `attrs (Dict)`: new namespace for class being constructed +- `model_fields (Dict[str, BaseField])`: ormar fields in defined in current class + +**Returns**: + +`(Tuple[Dict, Dict])`: updated attrs and model_fields + + +#### 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] +``` + +Extracts fields from base classes if they have valid oramr fields. + +If model was already parsed -> fields definitions need to be removed from class +cause pydantic complains about field re-definition so after first child +we need to extract from __parsed_fields__ not the class itself. + +If the class is parsed first time annotations and field definition is parsed +from the class.__dict__. + +If the class is a ormar.Model it is skipped. + +**Arguments**: + +- `base_class (Model or model parent class)`: one of the parent classes +- `curr_class (Model or model parent class)`: current constructed class +- `attrs (Dict)`: new namespace for class being constructed +- `model_fields (Dict[str, BaseField])`: ormar fields in defined in current class + +**Returns**: + +`(Tuple[Dict, Dict])`: updated attrs and model_fields + + +## ModelMetaclass Objects + +```python +class ModelMetaclass(pydantic.main.ModelMetaclass) +``` + + +#### \_\_new\_\_ + +```python + | __new__(mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict) -> "ModelMetaclass" +``` + +Metaclass used by ormar Models that performs configuration +and build of ormar Models. + + +Sets pydantic configuration. +Extract model_fields and convert them to pydantic FieldInfo, +updates class namespace. + +Extracts settings and fields from parent classes. +Fetches methods decorated with @property_field decorator +to expose them later in dict(). + +Construct parent pydantic Metaclass/ Model. + +If class has Meta class declared (so actual ormar Models) it also: + +* populate sqlalchemy columns, pkname and tables from model_fields +* register reverse relationships on related models +* registers all relations in alias manager that populates table_prefixes +* exposes alias manager on each Model +* creates QuerySet for each model and exposes it on a class + +**Arguments**: + +- `name (str)`: name of current class +- `bases (Tuple)`: base classes +- `attrs (Dict)`: class namespace + diff --git a/docs/api/models/model-table-proxy.md b/docs/api/models/model-table-proxy.md new file mode 100644 index 0000000..eb79f52 --- /dev/null +++ b/docs/api/models/model-table-proxy.md @@ -0,0 +1,14 @@ + +# models.modelproxy + + +## ModelTableProxy Objects + +```python +class ModelTableProxy( + PrefetchQueryMixin, MergeModelMixin, SavePrepareMixin, ExcludableMixin) +``` + +Used to combine all mixins with different set of functionalities. +One of the bases of the ormar Model class. + diff --git a/docs/api/models/model.md b/docs/api/models/model.md new file mode 100644 index 0000000..b22d3d9 --- /dev/null +++ b/docs/api/models/model.md @@ -0,0 +1,323 @@ + +# models.model + + +#### group\_related\_list + +```python +group_related_list(list_: List) -> Dict +``` + +Translates the list of related strings into a dictionary. +That way nested models are grouped to traverse them in a right order +and to avoid repetition. + +Sample: ["people__houses", "people__cars__models", "people__cars__colors"] +will become: +{'people': {'houses': [], 'cars': ['models', 'colors']}} + +**Arguments**: + +- `list_ (List[str])`: list of related models used in select related + +**Returns**: + +`(Dict[str, List])`: list converted to dictionary to avoid repetition and group nested models + + +#### T + + +## Model Objects + +```python +class Model(NewBaseModel) +``` + + +#### \_\_abstract\_\_ + + +#### \_\_repr\_\_ + +```python + | __repr__() -> str +``` + + +#### from\_row + +```python + | @classmethod + | from_row(cls: Type[T], row: sqlalchemy.engine.ResultProxy, select_related: List = None, related_models: Any = None, previous_model: Type[T] = None, related_name: str = None, fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None) -> Optional[T] +``` + +Model method to convert raw sql row from database into ormar.Model instance. +Traverses nested models if they were specified in select_related for query. + +Called recurrently and returns model instance if it's present in the row. +Note that it's processing one row at a time, so if there are duplicates of +parent row that needs to be joined/combined +(like parent row in sql join with 2+ child rows) +instances populated in this method are later combined in the QuerySet. +Other method working directly on raw database results is in prefetch_query, +where rows are populated in a different way as they do not have +nested models in result. + +**Arguments**: + +- `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 +- `previous_model (Model class)`: internal param for nested models to specify table_prefix +- `related_name (str)`: internal parameter - name of current nested model +- `fields (Optional[Union[Dict, Set]])`: fields and related model fields to include +if provided only those are included +- `exclude_fields (Optional[Union[Dict, Set]])`: fields and related model fields to exclude +excludes the fields even if they are provided in fields + +**Returns**: + +`(Optional[Model])`: returns model if model is populated from database + + +#### populate\_nested\_models\_from\_row + +```python + | @classmethod + | populate_nested_models_from_row(cls, item: dict, row: sqlalchemy.engine.ResultProxy, related_models: Any, fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None) -> dict +``` + +Traverses structure of related models and populates the nested models +from the database row. +Related models can be a list if only directly related models are to be +populated, converted to dict if related models also have their own related +models to be populated. + +Recurrently calls from_row method on nested instances and create nested +instances. In the end those instances are added to the final model dictionary. + +**Arguments**: + +- `item (Dict)`: dictionary of already populated nested models, otherwise empty dict +- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database +- `related_models (Union[Dict, List])`: list or dict of related models +- `fields (Optional[Union[Dict, Set]])`: fields and related model fields to include - +if provided only those are included +- `exclude_fields (Optional[Union[Dict, Set]])`: fields and related model fields to exclude +excludes the fields even if they are provided in fields + +**Returns**: + +`(Dict)`: dictionary with keys corresponding to model fields names +and values are database values + + +#### extract\_prefixed\_table\_columns + +```python + | @classmethod + | extract_prefixed_table_columns(cls, item: dict, row: sqlalchemy.engine.result.ResultProxy, table_prefix: str, fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None) -> dict +``` + +Extracts own fields from raw sql result, using a given prefix. +Prefix changes depending on the table's position in a join. + +If the table is a main table, there is no prefix. +All joined tables have prefixes to allow duplicate column names, +as well as duplicated joins to the same table from multiple different tables. + +Extracted fields populates the item dict later used to construct a Model. + +Used in Model.from_row and PrefetchQuery._populate_rows methods. + +**Arguments**: + +- `item (Dict)`: dictionary of already populated nested models, otherwise empty dict +- `row (sqlalchemy.engine.result.ResultProxy)`: raw result row from the database +- `table_prefix (str)`: prefix of the table from AliasManager +each pair of tables have own prefix (two of them depending on direction) - +used in joins to allow multiple joins to the same table. +- `fields (Optional[Union[Dict, Set]])`: fields and related model fields to include - +if provided only those are included +- `exclude_fields (Optional[Union[Dict, Set]])`: fields and related model fields to exclude +excludes the fields even if they are provided in fields + +**Returns**: + +`(Dict)`: dictionary with keys corresponding to model fields names +and values are database values + + +#### upsert + +```python + | async upsert(**kwargs: Any) -> T +``` + +Performs either a save or an update depending on the presence of the pk. +If the pk field is filled it's an update, otherwise the save is performed. +For save kwargs are ignored, used only in update if provided. + +**Arguments**: + +- `kwargs (Any)`: list of fields to update + +**Returns**: + +`(Model)`: saved Model + + +#### save + +```python + | async save() -> T +``` + +Performs a save of given Model instance. +If primary key is already saved, db backend will throw integrity error. + +Related models are saved by pk number, reverse relation and many to many fields +are not saved - use corresponding relations methods. + +If there are fields with server_default set and those fields +are not already filled save will trigger also a second query +to refreshed the fields populated server side. + +Does not recognize if model was previously saved. +If you want to perform update or insert depending on the pk +fields presence use upsert. + +Sends pre_save and post_save signals. + +Sets model save status to True. + +**Returns**: + +`(Model)`: saved Model + + +#### save\_related + +```python + | async save_related(follow: bool = False, visited: Set = None, update_count: int = 0) -> int +``` + +Triggers a upsert method on all related models +if the instances are not already saved. +By default saves only the directly related ones. + +If follow=True is set it saves 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 saved, but the save do not +follow them inside. So Model A -> Model B -> Model A -> Model C will save second +Model A but will never follow into Model C. +Nested relations of those kind need to be persisted 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 +- `visited (Set)`: internal parameter for recursive calls - already visited models +- `update_count (int)`: internal parameter for recursive calls - +number of updated instances + +**Returns**: + +`(int)`: number of updated/saved models + + +#### \_update\_and\_follow + +```python + | @staticmethod + | async _update_and_follow(rel: T, 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) -> T +``` + +Performs update of Model instance in the database. +Fields can be updated before or you can pass them as kwargs. + +Sends pre_update and post_update signals. + +Sets model save status to True. + +**Raises**: + +- `ModelPersistenceError`: If the pk column is not set will throw ModelPersistenceError + +**Arguments**: + +- `kwargs (Any)`: list of fields to update as field=value pairs + +**Returns**: + +`(Model)`: updated Model + + +#### delete + +```python + | async delete() -> int +``` + +Removes the Model instance from the database. + +Sends pre_delete and post_delete signals. + +Sets model save status to False. + +Note it does not delete the Model itself (python object). +So you can delete and later save (since pk is deleted no conflict will arise) +or update and the Model will be saved in database again. + +**Returns**: + +`(int)`: number of deleted rows (for some backends) + + +#### load + +```python + | async load() -> T +``` + +Allow to refresh existing Models fields from database. +Be careful as the related models can be overwritten by pk_only models in load. +Does NOT refresh the related models fields if they were loaded before. + +**Raises**: + +- `NoMatch`: If given pk is not found in database the NoMatch exception is raised. + +**Returns**: + +`(Model)`: reloaded Model + diff --git a/docs/api/models/new-basemodel.md b/docs/api/models/new-basemodel.md new file mode 100644 index 0000000..0190131 --- /dev/null +++ b/docs/api/models/new-basemodel.md @@ -0,0 +1,512 @@ + +# models.newbasemodel + + +## NewBaseModel Objects + +```python +class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass) +``` + +Main base class of ormar Model. +Inherits from pydantic BaseModel and has all mixins combined in ModelTableProxy. +Constructed with ModelMetaclass which in turn also inherits pydantic metaclass. + +Abstracts away all internals and helper functions, so final Model class has only +the logic concerned with database connection and data persistance. + + +#### \_\_slots\_\_ + + +#### \_\_init\_\_ + +```python + | __init__(*args: Any, **kwargs: Any) -> None +``` + +Initializer that creates a new ormar Model that is also pydantic Model at the +same time. + +Passed keyword arguments can be only field names and their corresponding values +as those will be passed to pydantic validation that will complain if extra +params are passed. + +If relations are defined each relation is expanded and children models are also +initialized and validated. Relation from both sides is registered so you can +access related models from both sides. + +Json fields are automatically loaded/dumped if needed. + +Models marked as abstract=True in internal Meta class cannot be initialized. + +Accepts also special __pk_only__ flag that indicates that Model is constructed +only with primary key value (so no other fields, it's a child model on other +Model), that causes skipping the validation, that's the only case when the +validation can be skipped. + +Accepts also special __excluded__ parameter that contains a set of fields that +should be explicitly set to None, as otherwise pydantic will try to populate +them with their default values if default is set. + +**Raises**: + +- `ModelError`: if abstract model is initialized or unknown field is passed + +**Arguments**: + +- `args (Any)`: ignored args +- `kwargs (Any)`: keyword arguments - all fields values and some special params + + +#### \_\_setattr\_\_ + +```python + | __setattr__(name: str, value: Any) -> None +``` + +Overwrites setattr in object to allow for special behaviour of certain params. + +Parameter "pk" is translated into actual primary key field name. + +Relations are expanded (child model constructed if needed) and registered on +both ends of the relation. The related models are handled by RelationshipManager +exposed at _orm param. + +Json fields converted if needed. + +Setting pk, foreign key value or any other field value sets Model save status +to False. Setting a reverse relation or many to many relation does not as it +does not modify the state of the model (but related model or through model). + +To short circuit all checks and expansions the set of attribute names present +on each model is gathered into _quick_access_fields that is looked first and +if field is in this set the object setattr is called directly. + +**Arguments**: + +- `name (str)`: name of the attribute to set +- `value (Any)`: value of the attribute to set + +**Returns**: + +`(None)`: None + + +#### \_\_getattribute\_\_ + +```python + | __getattribute__(item: str) -> Any +``` + +Because we need to overwrite getting the attribute by ormar instead of pydantic +as well as returning related models and not the value stored on the model the +__getattribute__ needs to be used not __getattr__. + +It's used to access all attributes so it can be a big overhead that's why a +number of short circuits is used. + +To short circuit all checks and expansions the set of attribute names present +on each model is gathered into _quick_access_fields that is looked first and +if field is in this set the object setattr is called directly. + +To avoid recursion object's getattribute is used to actually get the attribute +value from the model after the checks. + +Even the function calls are constructed with objects functions. + +Parameter "pk" is translated into actual primary key field name. + +Relations are returned so the actual related model is returned and not current +model's field. The related models are handled by RelationshipManager exposed +at _orm param. + +Json fields are converted if needed. + +**Arguments**: + +- `item (str)`: name of the attribute to retrieve + +**Returns**: + +`(Any)`: value of the attribute + + +#### \_extract\_related\_model\_instead\_of\_field + +```python + | _extract_related_model_instead_of_field(item: str) -> Optional[Union["T", Sequence["T"]]] +``` + +Retrieves the related model/models from RelationshipManager. + +**Arguments**: + +- `item (str)`: name of the relation + +**Returns**: + +`(Optional[Union[Model, List[Model]]])`: related model, list of related models or None + + +#### \_\_eq\_\_ + +```python + | __eq__(other: object) -> bool +``` + +Compares other model to this model. when == is called. + +**Arguments**: + +- `other (object)`: other model to compare + +**Returns**: + +`(bool)`: result of comparison + + +#### \_\_same\_\_ + +```python + | __same__(other: "NewBaseModel") -> bool +``` + +Used by __eq__, compares other model to this model. +Compares: +* _orm_ids, +* primary key values if it's set +* dictionary of own fields (excluding relations) + +**Arguments**: + +- `other (NewBaseModel)`: model to compare to + +**Returns**: + +`(bool)`: result of comparison + + +#### get\_name + +```python + | @classmethod + | get_name(cls, lower: bool = True) -> str +``` + +Returns name of the Model class, by default lowercase. + +**Arguments**: + +- `lower (bool)`: flag if name should be set to lowercase + +**Returns**: + +`(str)`: name of the model + + +#### pk\_column + +```python + | @property + | pk_column() -> sqlalchemy.Column +``` + +Retrieves primary key sqlalchemy column from models Meta.table. +Each model has to have primary key. +Only one primary key column is allowed. + +**Returns**: + +`(sqlalchemy.Column)`: primary key sqlalchemy column + + +#### saved + +```python + | @property + | saved() -> bool +``` + +Saved status of the model. Changed by setattr and loading from db + + +#### signals + +```python + | @property + | signals() -> "SignalEmitter" +``` + +Exposes signals from model Meta + + +#### pk\_type + +```python + | @classmethod + | pk_type(cls) -> Any +``` + +Shortcut to models primary key field type + + +#### db\_backend\_name + +```python + | @classmethod + | db_backend_name(cls) -> str +``` + +Shortcut to database dialect, +cause some dialect require different treatment + + +#### remove + +```python + | remove(parent: "T", name: str) -> None +``` + +Removes child from relation with given name in RelationshipManager + + +#### set\_save\_status + +```python + | set_save_status(status: bool) -> None +``` + +Sets value of the save status + + +#### get\_properties + +```python + | @classmethod + | get_properties(cls, include: Union[Set, Dict, None], exclude: Union[Set, Dict, None]) -> Set[str] +``` + +Returns a set of names of functions/fields decorated with +@property_field decorator. + +They are added to dictionary when called directly and therefore also are +present in fastapi responses. + +**Arguments**: + +- `include (Union[Set, Dict, None])`: fields to include +- `exclude (Union[Set, Dict, None])`: fields to exclude + +**Returns**: + +`(Set[str])`: set of property fields names + + +#### \_get\_related\_not\_excluded\_fields + +```python + | _get_related_not_excluded_fields(include: Optional[Dict], exclude: Optional[Dict]) -> List +``` + +Returns related field names applying on them include and exclude set. + +**Arguments**: + +- `include (Union[Set, Dict, None])`: fields to include +- `exclude (Union[Set, Dict, None])`: fields to exclude + +**Returns**: + +`(List of fields with relations that is not excluded)`: + + +#### \_extract\_nested\_models\_from\_list + +```python + | @staticmethod + | _extract_nested_models_from_list(models: MutableSequence, include: Union[Set, Dict, None], exclude: Union[Set, Dict, None]) -> List +``` + +Converts list of models into list of dictionaries. + +**Arguments**: + +- `models (List)`: List of models +- `include (Union[Set, Dict, None])`: fields to include +- `exclude (Union[Set, Dict, None])`: fields to exclude + +**Returns**: + +`(List[Dict])`: list of models converted to dictionaries + + +#### \_skip\_ellipsis + +```python + | _skip_ellipsis(items: Union[Set, Dict, None], key: str) -> Union[Set, Dict, None] +``` + +Helper to traverse the include/exclude dictionaries. +In dict() Ellipsis should be skipped as it indicates all fields required +and not the actual set/dict with fields names. + +**Arguments**: + +- `items (Union[Set, Dict, None])`: current include/exclude value +- `key (str)`: key for nested relations to check + +**Returns**: + +`(Union[Set, Dict, None])`: nested value of the items + + +#### \_extract\_nested\_models + +```python + | _extract_nested_models(nested: bool, dict_instance: Dict, include: Optional[Dict], exclude: Optional[Dict]) -> Dict +``` + +Traverse nested models and converts them into dictionaries. +Calls itself recursively if needed. + +**Arguments**: + +- `nested (bool)`: flag if current instance is nested +- `dict_instance (Dict)`: current instance dict +- `include (Optional[Dict])`: fields to include +- `exclude (Optional[Dict])`: fields to exclude + +**Returns**: + +`(Dict)`: current model dict with child models converted to dictionaries + + +#### 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" +``` + +Generate a dictionary representation of the model, +optionally specifying which fields to include or exclude. + +Nested models are also parsed to dictionaries. + +Additionally fields decorated with @property_field are also added. + +**Arguments**: + +- `include (Union[Set, Dict, None])`: fields to include +- `exclude (Union[Set, Dict, None])`: fields to exclude +- `by_alias (bool)`: flag to get values by alias - passed to pydantic +- `skip_defaults (bool)`: flag to not set values - passed to pydantic +- `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 + +**Returns**: + +`()`: + + +#### update\_from\_dict + +```python + | update_from_dict(value_dict: Dict) -> "NewBaseModel" +``` + +Updates self with values of fields passed in the dictionary. + +**Arguments**: + +- `value_dict (Dict)`: dictionary of fields names and values + +**Returns**: + +`(NewBaseModel)`: self + + +#### \_convert\_json + +```python + | _convert_json(column_name: str, value: Any, op: str) -> Union[str, Dict] +``` + +Converts value to/from json if needed (for Json columns). + +**Arguments**: + +- `column_name (str)`: name of the field +- `value (Any)`: value fo the field +- `op (str)`: operator on json + +**Returns**: + +`(Any)`: converted value if needed, else original value + + +#### \_is\_conversion\_to\_json\_needed + +```python + | _is_conversion_to_json_needed(column_name: str) -> bool +``` + +Checks if given column name is related to JSON field. + +**Arguments**: + +- `column_name (str)`: name of the field + +**Returns**: + +`(bool)`: result of the check + + +#### \_extract\_own\_model\_fields + +```python + | _extract_own_model_fields() -> Dict +``` + +Returns a dictionary with field names and values for fields that are not +relations fields (ForeignKey, ManyToMany etc.) + +**Returns**: + +`(Dict)`: dictionary of fields names and values. + + +#### \_extract\_model\_db\_fields + +```python + | _extract_model_db_fields() -> Dict +``` + +Returns a dictionary with field names and values for fields that are stored in +current model's table. + +That includes own non-relational fields ang foreign key fields. + +**Returns**: + +`(Dict)`: dictionary of fields names and values. + + +#### get\_relation\_model\_id + +```python + | get_relation_model_id(target_field: Type["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 + +**Returns**: + +`(Optional[int])`: value of pk if set + diff --git a/docs/api/query-set/clause.md b/docs/api/query-set/clause.md new file mode 100644 index 0000000..dcef64d --- /dev/null +++ b/docs/api/query-set/clause.md @@ -0,0 +1,174 @@ + +# queryset.clause + + +#### FILTER\_OPERATORS + + +#### ESCAPE\_CHARACTERS + + +## QueryClause Objects + +```python +class QueryClause() +``` + +Constructs where clauses from strings passed as arguments + + +#### \_\_init\_\_ + +```python + | __init__(model_cls: Type["Model"], filter_clauses: List, select_related: List) -> None +``` + + +#### filter + +```python + | filter(**kwargs: Any) -> Tuple[List[sqlalchemy.sql.expression.TextClause], List[str]] +``` + +Main external access point that processes the clauses into sqlalchemy text +clauses and updates select_related list with implicit related tables +mentioned in select_related strings but not included in select_related. + +**Arguments**: + +- `kwargs (Any)`: key, value pair with column names and values + +**Returns**: + +`(Tuple[List[sqlalchemy.sql.elements.TextClause], List[str]])`: Tuple with list of where clauses and updated select_related list + + +#### \_populate\_filter\_clauses + +```python + | _populate_filter_clauses(**kwargs: Any) -> Tuple[List[sqlalchemy.sql.expression.TextClause], List[str]] +``` + +Iterates all clauses and extracts used operator and field from related +models if needed. Based on the chain of related names the target table +is determined and the final clause is escaped if needed and compiled. + +**Arguments**: + +- `kwargs (Any)`: key, value pair with column names and values + +**Returns**: + +`(Tuple[List[sqlalchemy.sql.elements.TextClause], List[str]])`: Tuple with list of where clauses and updated select_related list + + +#### \_process\_column\_clause\_for\_operator\_and\_value + +```python + | _process_column_clause_for_operator_and_value(value: Any, op: str, column: sqlalchemy.Column, table: sqlalchemy.Table, table_prefix: str) -> sqlalchemy.sql.expression.TextClause +``` + +Escapes characters if it's required. +Substitutes values of the models if value is a ormar Model with its pk value. +Compiles the clause. + +**Arguments**: + +- `value (Any)`: value of the filter +- `op (str)`: filter operator +- `column (sqlalchemy.sql.schema.Column)`: column on which filter should be applied +- `table (sqlalchemy.sql.schema.Table)`: table on which filter should be applied +- `table_prefix (str)`: prefix from AliasManager + +**Returns**: + +`(sqlalchemy.sql.elements.TextClause)`: complied and escaped clause + + +#### \_determine\_filter\_target\_table + +```python + | _determine_filter_target_table(related_parts: List[str], select_related: List[str]) -> Tuple[List[str], str, Type["Model"]] +``` + +Adds related strings to select_related list otherwise the clause would fail as +the required columns would not be present. That means that select_related +list is filled with missing values present in filters. + +Walks the relation to retrieve the actual model on which the clause should be +constructed, extracts alias based on last relation leading to target model. + +**Arguments**: + +- `related_parts (List[str])`: list of split parts of related string +- `select_related (List[str])`: list of related models + +**Returns**: + +`(Tuple[List[str], str, Type[Model]])`: list of related models, table_prefix, final model class + + +#### \_compile\_clause + +```python + | _compile_clause(clause: sqlalchemy.sql.expression.BinaryExpression, column: sqlalchemy.Column, table: sqlalchemy.Table, table_prefix: str, modifiers: Dict) -> sqlalchemy.sql.expression.TextClause +``` + +Compiles the clause to str using appropriate database dialect, replace columns +names with aliased names and converts it back to TextClause. + +**Arguments**: + +- `clause (sqlalchemy.sql.elements.BinaryExpression)`: original not compiled clause +- `column (sqlalchemy.sql.schema.Column)`: column on which filter should be applied +- `table (sqlalchemy.sql.schema.Table)`: table on which filter should be applied +- `table_prefix (str)`: prefix from AliasManager +- `modifiers (Dict[str, NoneType])`: sqlalchemy modifiers - used only to escape chars here + +**Returns**: + +`(sqlalchemy.sql.elements.TextClause)`: compiled and escaped clause + + +#### \_escape\_characters\_in\_clause + +```python + | @staticmethod + | _escape_characters_in_clause(op: str, value: Any) -> Tuple[Any, bool] +``` + +Escapes the special characters ["%", "_"] if needed. +Adds `%` for `like` queries. + +**Raises**: + +- `QueryDefinitionError`: if contains or icontains is used with +ormar model instance + +**Arguments**: + +- `op (str)`: operator used in query +- `value (Any)`: value of the filter + +**Returns**: + +`(Tuple[Any, bool])`: escaped value and flag if escaping is needed + + +#### \_extract\_operator\_field\_and\_related + +```python + | @staticmethod + | _extract_operator_field_and_related(parts: List[str]) -> Tuple[str, str, Optional[List]] +``` + +Splits filter query key and extracts required parts. + +**Arguments**: + +- `parts (List[str])`: split filter query key + +**Returns**: + +`(Tuple[str, str, Optional[List]])`: operator, field_name, list of related parts + diff --git a/docs/api/query-set/filter-query.md b/docs/api/query-set/filter-query.md new file mode 100644 index 0000000..5a3b68e --- /dev/null +++ b/docs/api/query-set/filter-query.md @@ -0,0 +1,36 @@ + +# queryset.filter\_query + + +## FilterQuery Objects + +```python +class FilterQuery() +``` + +Modifies the select query with given list of where/filter clauses. + + +#### \_\_init\_\_ + +```python + | __init__(filter_clauses: List, exclude: bool = False) -> None +``` + + +#### apply + +```python + | apply(expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select +``` + +Applies all filter clauses if set. + +**Arguments**: + +- `expr (sqlalchemy.sql.selectable.Select)`: query to modify + +**Returns**: + +`(sqlalchemy.sql.selectable.Select)`: modified query + diff --git a/docs/api/query-set/join.md b/docs/api/query-set/join.md new file mode 100644 index 0000000..8519c54 --- /dev/null +++ b/docs/api/query-set/join.md @@ -0,0 +1,262 @@ + +# queryset.join + + +## JoinParameters Objects + +```python +class JoinParameters(NamedTuple) +``` + +Named tuple that holds set of parameters passed during join construction. + + +#### prev\_model + + +#### previous\_alias + + +#### from\_table + + +#### model\_cls + + +## SqlJoin Objects + +```python +class SqlJoin() +``` + + +#### \_\_init\_\_ + +```python + | __init__(used_aliases: List, select_from: sqlalchemy.sql.select, columns: List[sqlalchemy.Column], fields: Optional[Union[Set, Dict]], exclude_fields: Optional[Union[Set, Dict]], order_columns: Optional[List], sorted_orders: OrderedDict) -> None +``` + + +#### alias\_manager + +```python + | @staticmethod + | alias_manager(model_cls: Type["Model"]) -> AliasManager +``` + +Shortcut for ormars model AliasManager stored on Meta. + +**Arguments**: + +- `model_cls (Type[Model])`: ormar Model class + +**Returns**: + +`(AliasManager)`: alias manager from model's Meta + + +#### on\_clause + +```python + | @staticmethod + | on_clause(previous_alias: str, alias: str, from_clause: str, to_clause: str) -> text +``` + +Receives aliases and names of both ends of the join and combines them +into one text clause used in joins. + +**Arguments**: + +- `previous_alias (str)`: alias of previous table +- `alias (str)`: alias of current table +- `from_clause (str)`: from table name +- `to_clause (str)`: to table name + +**Returns**: + +`(sqlalchemy.text)`: clause combining all strings + + +#### update\_inclusions + +```python + | @staticmethod + | update_inclusions(model_cls: Type["Model"], fields: Optional[Union[Set, Dict]], exclude_fields: Optional[Union[Set, Dict]], nested_name: str) -> Tuple[Optional[Union[Dict, Set]], Optional[Union[Dict, Set]]] +``` + +Extract nested fields and exclude_fields if applicable. + +**Arguments**: + +- `model_cls (Type["Model"])`: ormar model class +- `fields (Optional[Union[Set, Dict]])`: fields to include +- `exclude_fields (Optional[Union[Set, Dict]])`: fields to exclude +- `nested_name (str)`: name of the nested field + +**Returns**: + +`(Tuple[Optional[Union[Dict, Set]], Optional[Union[Dict, Set]]])`: updated exclude and include fields from nested objects + + +#### build\_join + +```python + | build_join(item: str, join_parameters: JoinParameters) -> Tuple[List, sqlalchemy.sql.select, List, OrderedDict] +``` + +Main external access point for building a join. +Splits the join definition, updates fields and exclude_fields if needed, +handles switching to through models for m2m relations, returns updated lists of +used_aliases and sort_orders. + +**Arguments**: + +- `item (str)`: string with join definition +- `join_parameters (JoinParameters)`: parameters from previous/ current join + +**Returns**: + +`(Tuple[List[str], Join, List[TextClause], collections.OrderedDict])`: list of used aliases, select from, list of aliased columns, sort orders + + +#### \_build\_join\_parameters + +```python + | _build_join_parameters(part: str, join_params: JoinParameters, fields: Optional[Union[Set, Dict]], exclude_fields: Optional[Union[Set, Dict]], is_multi: bool = False) -> JoinParameters +``` + +Updates used_aliases to not join multiple times to the same table. +Updates join parameters with new values. + +**Arguments**: + +- `part (str)`: part of the join str definition +- `join_params (JoinParameters)`: parameters from previous/ current join +- `fields (Optional[Union[Set, Dict]])`: fields to include +- `exclude_fields (Optional[Union[Set, Dict]])`: fields to exclude +- `is_multi (bool)`: flag if the relation is m2m + +**Returns**: + +`(ormar.queryset.join.JoinParameters)`: updated join parameters + + +#### \_process\_join + +```python + | _process_join(join_params: JoinParameters, is_multi: bool, model_cls: Type["Model"], part: str, alias: str, fields: Optional[Union[Set, Dict]], exclude_fields: Optional[Union[Set, Dict]]) -> None +``` + +Resolves to and from column names and table names. + +Produces on_clause. + +Performs actual join updating select_from parameter. + +Adds aliases of required column to list of columns to include in query. + +Updates the used aliases list directly. + +Process order_by causes for non m2m relations. + +**Arguments**: + +- `join_params (JoinParameters)`: parameters from previous/ current join +- `is_multi (bool)`: flag if it's m2m relation +- `model_cls (ormar.models.metaclass.ModelMetaclass)`: +- `part (str)`: name of the field used in join +- `alias (str)`: alias of the current join +- `fields (Optional[Union[Set, Dict]])`: fields to include +- `exclude_fields (Optional[Union[Set, Dict]])`: fields to exclude + + +#### \_switch\_many\_to\_many\_order\_columns + +```python + | _switch_many_to_many_order_columns(part: str, new_part: str) -> None +``` + +Substitutes the name of the relation with actual model name in m2m order bys. + +**Arguments**: + +- `part (str)`: name of the field with relation +- `new_part (str)`: name of the target model + + +#### \_check\_if\_condition\_apply + +```python + | @staticmethod + | _check_if_condition_apply(condition: List, part: str) -> bool +``` + +Checks filter conditions to find if they apply to current join. + +**Arguments**: + +- `condition (List[str])`: list of parts of condition split by '__' +- `part (str)`: name of the current relation join. + +**Returns**: + +`(bool)`: result of the check + + +#### set\_aliased\_order\_by + +```python + | set_aliased_order_by(condition: List[str], alias: str, to_table: str, model_cls: Type["Model"]) -> None +``` + +Substitute hyphens ('-') with descending order. +Construct actual sqlalchemy text clause using aliased table and column name. + +**Arguments**: + +- `condition (List[str])`: list of parts of a current condition split by '__' +- `alias (str)`: alias of the table in current join +- `to_table (sqlalchemy.sql.elements.quoted_name)`: target table +- `model_cls (ormar.models.metaclass.ModelMetaclass)`: ormar model class + + +#### get\_order\_bys + +```python + | get_order_bys(alias: str, to_table: str, pkname_alias: str, part: str, model_cls: Type["Model"]) -> None +``` + +Triggers construction of order bys if they are given. +Otherwise by default each table is sorted by a primary key column asc. + +**Arguments**: + +- `alias (str)`: alias of current table in join +- `to_table (sqlalchemy.sql.elements.quoted_name)`: target table +- `pkname_alias (str)`: alias of the primary key column +- `part (str)`: name of the current relation join +- `model_cls (Type[Model])`: ormar model class + + +#### get\_to\_and\_from\_keys + +```python + | @staticmethod + | get_to_and_from_keys(join_params: JoinParameters, is_multi: bool, model_cls: Type["Model"], part: str) -> Tuple[str, str] +``` + +Based on the relation type, name of the relation and previous models and parts +stored in JoinParameters it resolves the current to and from keys, which are +different for ManyToMany relation, ForeignKey and reverse part of relations. + +**Arguments**: + +- `join_params (JoinParameters)`: parameters from previous/ current join +- `is_multi (bool)`: flag if the relation is of m2m type +- `model_cls (Type[Model])`: ormar model class +- `part (str)`: name of the current relation join + +**Returns**: + +`(Tuple[str, str])`: to key and from key + diff --git a/docs/api/query-set/limit-query.md b/docs/api/query-set/limit-query.md new file mode 100644 index 0000000..5e9e973 --- /dev/null +++ b/docs/api/query-set/limit-query.md @@ -0,0 +1,36 @@ + +# queryset.limit\_query + + +## LimitQuery Objects + +```python +class LimitQuery() +``` + +Modifies the select query with limit clause. + + +#### \_\_init\_\_ + +```python + | __init__(limit_count: Optional[int]) -> None +``` + + +#### apply + +```python + | apply(expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select +``` + +Applies the limit clause. + +**Arguments**: + +- `expr (sqlalchemy.sql.selectable.Select)`: query to modify + +**Returns**: + +`(sqlalchemy.sql.selectable.Select)`: modified query + diff --git a/docs/api/query-set/offset-query.md b/docs/api/query-set/offset-query.md new file mode 100644 index 0000000..be6cf64 --- /dev/null +++ b/docs/api/query-set/offset-query.md @@ -0,0 +1,36 @@ + +# queryset.offset\_query + + +## OffsetQuery Objects + +```python +class OffsetQuery() +``` + +Modifies the select query with offset if set + + +#### \_\_init\_\_ + +```python + | __init__(query_offset: Optional[int]) -> None +``` + + +#### apply + +```python + | apply(expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select +``` + +Applies the offset clause. + +**Arguments**: + +- `expr (sqlalchemy.sql.selectable.Select)`: query to modify + +**Returns**: + +`(sqlalchemy.sql.selectable.Select)`: modified query + diff --git a/docs/api/query-set/order-query.md b/docs/api/query-set/order-query.md new file mode 100644 index 0000000..6927644 --- /dev/null +++ b/docs/api/query-set/order-query.md @@ -0,0 +1,36 @@ + +# queryset.order\_query + + +## OrderQuery Objects + +```python +class OrderQuery() +``` + +Modifies the select query with given list of order_by clauses. + + +#### \_\_init\_\_ + +```python + | __init__(sorted_orders: Dict) -> None +``` + + +#### apply + +```python + | apply(expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select +``` + +Applies all order_by clauses if set. + +**Arguments**: + +- `expr (sqlalchemy.sql.selectable.Select)`: query to modify + +**Returns**: + +`(sqlalchemy.sql.selectable.Select)`: modified query + diff --git a/docs/api/query-set/prefetch-query.md b/docs/api/query-set/prefetch-query.md new file mode 100644 index 0000000..102fdf5 --- /dev/null +++ b/docs/api/query-set/prefetch-query.md @@ -0,0 +1,352 @@ + +# queryset.prefetch\_query + + +#### add\_relation\_field\_to\_fields + +```python +add_relation_field_to_fields(fields: Union[Set[Any], Dict[Any, Any], None], related_field_name: str) -> Union[Set[Any], Dict[Any, Any], None] +``` + +Adds related field into fields to include as otherwise it would be skipped. +Related field is added only if fields are already populated. +Empty fields implies all fields. + +**Arguments**: + +- `fields (Dict)`: Union[Set[Any], Dict[Any, Any], None] +- `related_field_name (str)`: name of the field with relation + +**Returns**: + +`(Union[Set[Any], Dict[Any, Any], None])`: updated fields dict + + +#### sort\_models + +```python +sort_models(models: List["Model"], orders_by: Dict) -> List["Model"] +``` + +Since prefetch query gets all related models by ids the sorting needs to happen in +python. Since by default models are already sorted by id here we resort only if +order_by parameters was set. + +**Arguments**: + +- `models (List[tests.test_prefetch_related.Division])`: list of models already fetched from db +- `orders_by (Dict[str, str])`: order by dictionary + +**Returns**: + +`(List[tests.test_prefetch_related.Division])`: sorted list of models + + +#### set\_children\_on\_model + +```python +set_children_on_model(model: "Model", related: str, children: Dict, model_id: int, models: Dict, orders_by: Dict) -> None +``` + +Extract ids of child models by given relation id key value. + +Based on those ids the actual children model instances are fetched from +already fetched data. + +If needed the child models are resorted according to passed orders_by dict. + +Also relation is registered as each child is set as parent related field name value. + +**Arguments**: + +- `model (Model)`: parent model instance +- `related (str)`: name of the related field +- `children (Dict[int, set])`: dictionary of children ids/ related field value +- `model_id (int)`: id of the model on which children should be set +- `models (Dict)`: dictionary of child models instances +- `orders_by (Dict)`: order_by dictionary + + +## PrefetchQuery Objects + +```python +class PrefetchQuery() +``` + +Query used to fetch related models in subsequent queries. +Each model is fetched only ones by the name of the relation. +That means that for each prefetch_related entry next query is issued to database. + + +#### \_\_init\_\_ + +```python + | __init__(model_cls: Type["Model"], fields: Optional[Union[Dict, Set]], exclude_fields: Optional[Union[Dict, Set]], prefetch_related: List, select_related: List, orders_by: List) -> None +``` + + +#### prefetch\_related + +```python + | async prefetch_related(models: Sequence["Model"], rows: List) -> Sequence["Model"] +``` + +Main entry point for prefetch_query. + +Receives list of already initialized parent models with all children from +select_related already populated. Receives also list of row sql result rows +as it's quicker to extract ids that way instead of calling each model. + +Returns list with related models already prefetched and set. + +**Arguments**: + +- `models (List[Model])`: list of already instantiated models from main query +- `rows (List[sqlalchemy.engine.result.RowProxy])`: row sql result of the main query before the prefetch + +**Returns**: + +`(List[Model])`: list of models with children prefetched + + +#### \_extract\_ids\_from\_raw\_data + +```python + | _extract_ids_from_raw_data(parent_model: Type["Model"], column_name: str) -> Set +``` + +Iterates over raw rows and extract id values of relation columns by using +prefixed column name. + +**Arguments**: + +- `parent_model (Type[Model])`: ormar model class +- `column_name (str)`: name of the relation column which is a key column + +**Returns**: + +`(set)`: set of ids of related model that should be extracted + + +#### \_extract\_ids\_from\_preloaded\_models + +```python + | _extract_ids_from_preloaded_models(parent_model: Type["Model"], column_name: str) -> Set +``` + +Extracts relation ids from already populated models if they were included +in the original query before. + +**Arguments**: + +- `parent_model (Type["Model"])`: model from which related ids should be extracted +- `column_name (str)`: name of the relation column which is a key column + +**Returns**: + +`(set)`: set of ids of related model that should be extracted + + +#### \_extract\_required\_ids + +```python + | _extract_required_ids(parent_model: Type["Model"], reverse: bool, related: str) -> Set +``` + +Delegates extraction of the fields to either get ids from raw sql response +or from already populated models. + +**Arguments**: + +- `parent_model (Type["Model"])`: model from which related ids should be extracted +- `reverse (bool)`: flag if the relation is reverse +- `related (str)`: name of the field with relation + +**Returns**: + +`(set)`: set of ids of related model that should be extracted + + +#### \_get\_filter\_for\_prefetch + +```python + | _get_filter_for_prefetch(parent_model: Type["Model"], target_model: Type["Model"], reverse: bool, related: str) -> List +``` + +Populates where clause with condition to return only models within the +set of extracted ids. + +If there are no ids for relation the empty list is returned. + +**Arguments**: + +- `parent_model (Type["Model"])`: model from which related ids should be extracted +- `target_model (Type["Model"])`: model to which relation leads to +- `reverse (bool)`: flag if the relation is reverse +- `related (str)`: name of the field with relation + +**Returns**: + +`(List[sqlalchemy.sql.elements.TextClause])`: + + +#### \_populate\_nested\_related + +```python + | _populate_nested_related(model: "Model", prefetch_dict: Dict, orders_by: Dict) -> "Model" +``` + +Populates all related models children of parent model that are +included in prefetch query. + +**Arguments**: + +- `model (Model)`: ormar model instance +- `prefetch_dict (Dict)`: dictionary of models to prefetch +- `orders_by (Dict)`: dictionary of order bys + +**Returns**: + +`(Model)`: model with children populated + + +#### \_prefetch\_related\_models + +```python + | async _prefetch_related_models(models: Sequence["Model"], rows: List) -> Sequence["Model"] +``` + +Main method of the query. + +Translates select nad prefetch list into dictionaries to avoid querying the +same related models multiple times. + +Keeps the list of already extracted models. + +Extracts the related models from the database and later populate all children +on each of the parent models from list. + +**Arguments**: + +- `models (List[Model])`: list of parent models from main query +- `rows (List[sqlalchemy.engine.result.RowProxy])`: raw response from sql query + +**Returns**: + +`(List[Model])`: list of models with prefetch children populated + + +#### \_extract\_related\_models + +```python + | async _extract_related_models(related: str, target_model: Type["Model"], prefetch_dict: Dict, select_dict: Dict, fields: Union[Set[Any], Dict[Any, Any], None], exclude_fields: Union[Set[Any], Dict[Any, Any], None], orders_by: Dict) -> None +``` + +Constructs queries with required ids and extracts data with fields that should +be included/excluded. + +Runs the queries against the database and populated dictionaries with ids and +with actual extracted children models. + +Calls itself recurrently to extract deeper nested relations of related model. + +**Arguments**: + +- `related (str)`: name of the relation +- `target_model (Type[Model])`: model to which relation leads to +- `prefetch_dict (Dict)`: prefetch related list converted into dictionary +- `select_dict (Dict)`: select related list converted into dictionary +- `fields (Union[Set[Any], Dict[Any, Any], None])`: fields to include +- `exclude_fields (Union[Set[Any], Dict[Any, Any], None])`: fields to exclude +- `orders_by (Dict)`: dictionary of order bys clauses + +**Returns**: + +`(None)`: None + + +#### \_run\_prefetch\_query + +```python + | async _run_prefetch_query(target_field: Type["BaseField"], fields: Union[Set[Any], Dict[Any, Any], None], exclude_fields: Union[Set[Any], Dict[Any, Any], None], filter_clauses: List) -> Tuple[str, List] +``` + +Actually runs the queries against the database and populates the raw response +for given related model. + +Returns table prefix as it's later needed to eventually initialize the children +models. + +**Arguments**: + +- `target_field (Type["BaseField"])`: ormar field with relation definition +- `fields (Union[Set[Any], Dict[Any, Any], None])`: fields to include +- `exclude_fields (Union[Set[Any], Dict[Any, Any], None])`: fields to exclude +- `filter_clauses (List[sqlalchemy.sql.elements.TextClause])`: list of clauses, actually one clause with ids of relation + +**Returns**: + +`(Tuple[str, List])`: table prefix and raw rows from sql response + + +#### \_get\_select\_related\_if\_apply + +```python + | @staticmethod + | _get_select_related_if_apply(related: str, select_dict: Dict) -> Dict +``` + +Extract nested part of select_related dictionary to extract models nested +deeper on related model and already loaded in select related query. + +**Arguments**: + +- `related (str)`: name of the relation +- `select_dict (Dict)`: dictionary of select related models in main query + +**Returns**: + +`(Dict)`: dictionary with nested part of select related + + +#### \_update\_already\_loaded\_rows + +```python + | _update_already_loaded_rows(target_field: Type["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 +- `prefetch_dict (Dict)`: dictionaries of related models to prefetch +- `orders_by (Dict)`: dictionary of order by clauses by model + + +#### \_populate\_rows + +```python + | _populate_rows(rows: List, target_field: Type["BaseField"], parent_model: Type["Model"], table_prefix: str, fields: Union[Set[Any], Dict[Any, Any], None], exclude_fields: Union[Set[Any], Dict[Any, Any], None], prefetch_dict: Dict, orders_by: Dict) -> None +``` + +Instantiates children models extracted from given relation. + +Populates them with their own nested children if they are included in prefetch +query. + +Sets the initialized models and ids of them under corresponding keys in +already_extracted dictionary. Later those instances will be fetched by ids +and set on the parent model after sorting if needed. + +**Arguments**: + +- `rows (List[sqlalchemy.engine.result.RowProxy])`: raw sql response from the prefetch query +- `target_field (Type["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 +- `fields (Union[Set[Any], Dict[Any, Any], None])`: fields to include +- `exclude_fields (Union[Set[Any], Dict[Any, Any], None])`: fields to exclude +- `prefetch_dict (Dict)`: dictionaries of related models to prefetch +- `orders_by (Dict)`: dictionary of order by clauses by model + diff --git a/docs/api/query-set/query-set.md b/docs/api/query-set/query-set.md new file mode 100644 index 0000000..e572a09 --- /dev/null +++ b/docs/api/query-set/query-set.md @@ -0,0 +1,671 @@ + +# queryset.queryset + + +## QuerySet Objects + +```python +class QuerySet() +``` + +Main class to perform database queries, exposed on each model as objects attribute. + + +#### \_\_init\_\_ + +```python + | __init__(model_cls: Type["Model"] = None, filter_clauses: List = None, exclude_clauses: List = None, select_related: List = None, limit_count: int = None, offset: int = None, columns: Dict = None, exclude_columns: Dict = None, order_bys: List = None, prefetch_related: List = None, limit_raw_sql: bool = False) -> None +``` + + +#### \_\_get\_\_ + +```python + | __get__(instance: Optional[Union["QuerySet", "QuerysetProxy"]], owner: Union[Type["Model"], Type["QuerysetProxy"]]) -> "QuerySet" +``` + + +#### model\_meta + +```python + | @property + | model_meta() -> "ModelMeta" +``` + +Shortcut to model class Meta set on QuerySet model. + +**Returns**: + +`(model Meta class)`: Meta class of the model + + +#### model + +```python + | @property + | model() -> Type["Model"] +``` + +Shortcut to model class set on QuerySet. + +**Returns**: + +`(Type[Model])`: model class + + +#### \_prefetch\_related\_models + +```python + | async _prefetch_related_models(models: Sequence[Optional["Model"]], rows: List) -> Sequence[Optional["Model"]] +``` + +Performs prefetch query for selected models names. + +**Arguments**: + +- `models (List[Model])`: list of already parsed main Models from main query +- `rows (List[sqlalchemy.engine.result.RowProxy])`: database rows from main query + +**Returns**: + +`(List[Model])`: list of models with prefetch models populated + + +#### \_process\_query\_result\_rows + +```python + | _process_query_result_rows(rows: List) -> Sequence[Optional["Model"]] +``` + +Process database rows and initialize ormar Model from each of the rows. + +**Arguments**: + +- `rows (List[sqlalchemy.engine.result.RowProxy])`: list of database rows from query result + +**Returns**: + +`(List[Model])`: list of models + + +#### check\_single\_result\_rows\_count + +```python + | @staticmethod + | check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None +``` + +Verifies if the result has one and only one row. + +**Arguments**: + +- `rows (List[Model])`: one element list of Models + + +#### database + +```python + | @property + | database() -> databases.Database +``` + +Shortcut to models database from Meta class. + +**Returns**: + +`(databases.Database)`: database + + +#### table + +```python + | @property + | table() -> sqlalchemy.Table +``` + +Shortcut to models table from Meta class. + +**Returns**: + +`(sqlalchemy.Table)`: database table + + +#### build\_select\_expression + +```python + | build_select_expression(limit: int = None, offset: int = None, order_bys: List = None) -> sqlalchemy.sql.select +``` + +Constructs the actual database query used in the QuerySet. +If any of the params is not passed the QuerySet own value is used. + +**Arguments**: + +- `limit (int)`: number to limit the query +- `offset (int)`: number to offset by +- `order_bys (List)`: list of order-by fields names + +**Returns**: + +`(sqlalchemy.sql.selectable.Select)`: built sqlalchemy select expression + + +#### filter + +```python + | filter(_exclude: bool = False, **kwargs: Any) -> "QuerySet" +``` + +Allows you to filter by any `Model` attribute/field +as well as to fetch instances, with a filter across an FK relationship. + +You can use special filter suffix to change the filter operands: + +* exact - like `album__name__exact='Malibu'` (exact match) +* iexact - like `album__name__iexact='malibu'` (exact match case insensitive) +* 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) +* gt - like `position__gt=3` (sql >) +* gte - like `position__gte=3` (sql >=) +* lt - like `position__lt=3` (sql <) +* lte - like `position__lte=3` (sql <=) +* startswith - like `album__name__startswith='Mal'` (exact start match) +* istartswith - like `album__name__istartswith='mal'` (case insensitive) +* endswith - like `album__name__endswith='ibu'` (exact end match) +* iendswith - like `album__name__iendswith='IBU'` (case insensitive) + +**Arguments**: + +- `_exclude (bool)`: flag if it should be exclude or filter +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(QuerySet)`: filtered QuerySet + + +#### exclude + +```python + | exclude(**kwargs: Any) -> "QuerySet" +``` + +Works exactly the same as filter and all modifiers (suffixes) are the same, +but returns a *not* condition. + +So if you use `filter(name='John')` which is `where name = 'John'` in SQL, +the `exclude(name='John')` equals to `where name <> 'John'` + +Note that all conditions are joined so if you pass multiple values it +becomes a union of conditions. + +`exclude(name='John', age>=35)` will become +`where not (name='John' and age>=35)` + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(QuerySet)`: filtered QuerySet + + +#### select\_related + +```python + | select_related(related: Union[List, str]) -> "QuerySet" +``` + +Allows to prefetch related models during the same query. + +**With `select_related` always only one query is run against the database**, +meaning that one (sometimes complicated) join is generated and later nested +models are processed in python. + +To fetch related model use `ForeignKey` names. + +To chain related `Models` relation use double underscores between names. + +**Arguments**: + +- `related (Union[List, str])`: list of relation field names, can be linked by '__' to nest + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### prefetch\_related + +```python + | prefetch_related(related: Union[List, str]) -> "QuerySet" +``` + +Allows to prefetch related models during query - but opposite to +`select_related` each subsequent model is fetched in a separate database query. + +**With `prefetch_related` always one query per Model is run against the +database**, meaning that you will have multiple queries executed one +after another. + +To fetch related model use `ForeignKey` names. + +To chain related `Models` relation use double underscores between names. + +**Arguments**: + +- `related (Union[List, str])`: list of relation field names, can be linked by '__' to nest + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### fields + +```python + | fields(columns: Union[List, str, Set, Dict]) -> "QuerySet" +``` + +With `fields()` you can select subset of model columns to limit the data load. + +Note that `fields()` and `exclude_fields()` works both for main models +(on normal queries like `get`, `all` etc.) +as well as `select_related` and `prefetch_related` +models (with nested notation). + +You can select specified fields by passing a `str, List[str], Set[str] or +dict` with nested definition. + +To include related models use notation +`{related_name}__{column}[__{optional_next} etc.]`. + +`fields()` can be called several times, building up the columns to select. + +If you include related models into `select_related()` call but you won't specify +columns for those models in fields - implies a list of all fields for +those nested models. + +Mandatory fields cannot be excluded as it will raise `ValidationError`, +to exclude a field it has to be nullable. + +Pk column cannot be excluded - it's always auto added even if +not explicitly included. + +You can also pass fields to include as dictionary or set. + +To mark a field as included in a dictionary use it's name as key +and ellipsis as value. + +To traverse nested models use nested dictionaries. + +To include fields at last level instead of nested dictionary a set can be used. + +To include whole nested model specify model related field name and ellipsis. + +**Arguments**: + +- `columns (Union[List, str, Set, Dict])`: columns to include + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### exclude\_fields + +```python + | exclude_fields(columns: Union[List, str, Set, Dict]) -> "QuerySet" +``` + +With `exclude_fields()` you can select subset of model columns that will +be excluded to limit the data load. + +It's the opposite of `fields()` method so check documentation above +to see what options are available. + +Especially check above how you can pass also nested dictionaries +and sets as a mask to exclude fields from whole hierarchy. + +Note that `fields()` and `exclude_fields()` works both for main models +(on normal queries like `get`, `all` etc.) +as well as `select_related` and `prefetch_related` models +(with nested notation). + +Mandatory fields cannot be excluded as it will raise `ValidationError`, +to exclude a field it has to be nullable. + +Pk column cannot be excluded - it's always auto added even +if explicitly excluded. + +**Arguments**: + +- `columns (Union[List, str, Set, Dict])`: columns to exclude + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### order\_by + +```python + | order_by(columns: Union[List, str]) -> "QuerySet" +``` + +With `order_by()` you can order the results from database based on your +choice of fields. + +You can provide a string with field name or list of strings with fields names. + +Ordering in sql will be applied in order of names you provide in order_by. + +By default if you do not provide ordering `ormar` explicitly orders by +all primary keys + +If you are sorting by nested models that causes that the result rows are +unsorted by the main model `ormar` will combine those children rows into +one main model. + +The main model will never duplicate in the result + +To order by main model field just provide a field name + +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. + +To sort in descending order provide a hyphen in front of the field name + +**Arguments**: + +- `columns (Union[List, str])`: columns by which models should be sorted + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### exists + +```python + | async exists() -> bool +``` + +Returns a bool value to confirm if there are rows matching the given criteria +(applied with `filter` and `exclude` if set). + +**Returns**: + +`(bool)`: result of the check + + +#### count + +```python + | async count() -> int +``` + +Returns number of rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +**Returns**: + +`(int)`: number of rows + + +#### update + +```python + | async update(each: bool = False, **kwargs: Any) -> int +``` + +Updates the model table after applying the filters from kwargs. + +You have to either pass a filter to narrow down a query or explicitly pass +each=True flag to affect whole table. + +**Arguments**: + +- `each (bool)`: flag if whole table should be affected if no filter is passed +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(int)`: number of updated rows + + +#### delete + +```python + | async delete(each: bool = False, **kwargs: Any) -> int +``` + +Deletes from the model table after applying the filters from kwargs. + +You have to either pass a filter to narrow down a query or explicitly pass +each=True flag to affect whole table. + +**Arguments**: + +- `each (bool)`: flag if whole table should be affected if no filter is passed +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(int)`: number of deleted rows + + +#### limit + +```python + | limit(limit_count: int, limit_raw_sql: bool = None) -> "QuerySet" +``` + +You can limit the results to desired number of parent models. + +To limit the actual number of database query rows instead of number of main +models use the `limit_raw_sql` parameter flag, and set it to `True`. + +**Arguments**: + +- `limit_raw_sql (bool)`: flag if raw sql should be limited +- `limit_count (int)`: number of models to limit + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### offset + +```python + | offset(offset: int, limit_raw_sql: bool = None) -> "QuerySet" +``` + +You can also offset the results by desired number of main models. + +To offset the actual number of database query rows instead of number of main +models use the `limit_raw_sql` parameter flag, and set it to `True`. + +**Arguments**: + +- `limit_raw_sql (bool)`: flag if raw sql should be offset +- `offset (int)`: numbers of models to offset + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### first + +```python + | async first(**kwargs: Any) -> "Model" +``` + +Gets the first row from the db ordered by primary key column ascending. + +**Raises**: + +- `NoMatch`: if no rows are returned +- `MultipleMatches`: if more than 1 row is returned. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: returned model + + +#### get + +```python + | async get(**kwargs: Any) -> "Model" +``` + +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. + +**Raises**: + +- `NoMatch`: if no rows are returned +- `MultipleMatches`: if more than 1 row is returned. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: returned model + + +#### get\_or\_create + +```python + | async get_or_create(**kwargs: Any) -> "Model" +``` + +Combination of create and get methods. + +Tries to get a row meeting the criteria fro kwargs +and if `NoMatch` exception is raised +it creates a new one with given kwargs. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: returned or created Model + + +#### update\_or\_create + +```python + | async update_or_create(**kwargs: Any) -> "Model" +``` + +Updates the model, or in case there is no match in database creates a new one. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: updated or created model + + +#### all + +```python + | async all(**kwargs: Any) -> Sequence[Optional["Model"]] +``` + +Returns all rows from a database for given model for set filter options. + +Passing kwargs is a shortcut and equals to calling `filter(**kwrags).all()`. + +If there are no rows meeting the criteria an empty list is returned. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(List[Model])`: list of returned models + + +#### create + +```python + | async create(**kwargs: Any) -> "Model" +``` + +Creates the model instance, saves it in a database and returns the updates model +(with pk populated if not passed and autoincrement is set). + +The allowed kwargs are `Model` fields names and proper value types. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: created model + + +#### bulk\_create + +```python + | async bulk_create(objects: List["Model"]) -> None +``` + +Performs a bulk update in one database session to speed up the process. + +Allows you to create multiple objects at once. + +A valid list of `Model` objects needs to be passed. + +Bulk operations do not send signals. + +**Arguments**: + +- `objects (List[Model])`: list of ormar models already initialized and ready to save. + + +#### bulk\_update + +```python + | async bulk_update(objects: List["Model"], columns: List[str] = None) -> None +``` + +Performs bulk update in one database session to speed up the process. + +Allows to update multiple instance at once. + +All `Models` passed need to have primary key column populated. + +You can also select which fields to update by passing `columns` list +as a list of string names. + +Bulk operations do not send signals. + +**Arguments**: + +- `objects (List[Model])`: list of ormar models +- `columns (List[str])`: list of columns to update + diff --git a/docs/api/query-set/query.md b/docs/api/query-set/query.md new file mode 100644 index 0000000..4166562 --- /dev/null +++ b/docs/api/query-set/query.md @@ -0,0 +1,157 @@ + +# queryset.query + + +## Query Objects + +```python +class Query() +``` + + +#### \_\_init\_\_ + +```python + | __init__(model_cls: Type["Model"], filter_clauses: List, exclude_clauses: List, select_related: List, limit_count: Optional[int], offset: Optional[int], fields: Optional[Union[Dict, Set]], exclude_fields: Optional[Union[Dict, Set]], order_bys: Optional[List], limit_raw_sql: bool) -> None +``` + + +#### \_init\_sorted\_orders + +```python + | _init_sorted_orders() -> None +``` + +Initialize empty order_by dict to be populated later during the query call + + +#### prefixed\_pk\_name + +```python + | @property + | prefixed_pk_name() -> str +``` + +Shortcut for extracting prefixed with alias primary key column name from main +model + +**Returns**: + +`(str)`: alias of pk column prefix with table name. + + +#### alias + +```python + | alias(name: str) -> str +``` + +Shortcut to extracting column alias from given master model. + +**Arguments**: + +- `name (str)`: name of column + +**Returns**: + +`(str)`: alias of given column name + + +#### apply\_order\_bys\_for\_primary\_model + +```python + | apply_order_bys_for_primary_model() -> None +``` + +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. + + +#### \_pagination\_query\_required + +```python + | _pagination_query_required() -> bool +``` + +Checks 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**: + +`(bool)`: result of the check + + +#### build\_select\_expression + +```python + | build_select_expression() -> Tuple[sqlalchemy.sql.select, List[str]] +``` + +Main entry point from outside (after proper initialization). + +Extracts columns list to fetch, +construct all required joins for select related, +then applies all conditional and sort clauses. + +Returns ready to run query with all joins and clauses. + +**Returns**: + +`(sqlalchemy.sql.selectable.Select)`: ready to run query with all joins and clauses. + + +#### \_build\_pagination\_subquery + +```python + | _build_pagination_subquery() -> sqlalchemy.sql.select +``` + +In order to apply limit and offset on main table in join only +(otherwise you can get only partially constructed main model +if number of children exceeds the applied limit and select_related is used) + +Used also to change first and get() without argument behaviour. +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 + + +#### \_apply\_expression\_modifiers + +```python + | _apply_expression_modifiers(expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select +``` + +Receives the select query (might be join) and applies: +* Filter clauses +* Exclude filter clauses +* Limit clauses +* Offset clauses +* Order by clauses + +Returns complete ready to run query. + +**Arguments**: + +- `expr (sqlalchemy.sql.selectable.Select)`: select expression before clauses + +**Returns**: + +`(sqlalchemy.sql.selectable.Select)`: expresion with all present clauses applied + + +#### \_reset\_query\_parameters + +```python + | _reset_query_parameters() -> None +``` + +Although it should be created each time before the call we reset the key params +anyway. + diff --git a/docs/api/query-set/utils.md b/docs/api/query-set/utils.md new file mode 100644 index 0000000..27989a3 --- /dev/null +++ b/docs/api/query-set/utils.md @@ -0,0 +1,152 @@ + +# queryset.utils + + +#### 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 +``` + +Checks if given name is not present in the current level of the structure. +Checks if given name is not the last name in the split list of parts. +Checks if the given name in current level is not a dictionary. + +All those checks verify if there is a need for deeper traversal. + +**Arguments**: + +- `part (str)`: +- `parts (List[str])`: +- `current_level (Any)`: current level of the traversed structure + +**Returns**: + +`(bool)`: result of the check + + +#### translate\_list\_to\_dict + +```python +translate_list_to_dict(list_to_trans: Union[List, Set], is_order: bool = False) -> Dict +``` + +Splits the list of strings by '__' and converts them to dictionary with nested +models grouped by parent model. That way each model appears only once in the whole +dictionary and children are grouped under parent name. + +Default required key ise Ellipsis like in pydantic. + +**Arguments**: + +- `list_to_trans (set)`: input list +- `is_order (bool)`: flag if change affects order_by clauses are they require special +default value with sort order. + +**Returns**: + +`(Dict)`: converted to dictionary input list + + +#### convert\_set\_to\_required\_dict + +```python +convert_set_to_required_dict(set_to_convert: set) -> Dict +``` + +Converts set to dictionary of required keys. +Required key is Ellipsis. + +**Arguments**: + +- `set_to_convert (set)`: set to convert to dict + +**Returns**: + +`(Dict)`: set converted to dict of ellipsis + + +#### update + +```python +update(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 + +```python +update_dict_from_list(curr_dict: Dict, list_to_update: Union[List, Set]) -> Dict +``` + +Converts the list into dictionary and later performs special update, where +nested keys that are sets or dicts are combined and not overwritten. + +**Arguments**: + +- `curr_dict (Dict)`: dict to update +- `list_to_update (List[str])`: list with values to update the dict + +**Returns**: + +`(Dict)`: updated dict + + +#### extract\_nested\_models + +```python +extract_nested_models(model: "Model", model_type: Type["Model"], select_dict: Dict, extracted: Dict) -> None +``` + +Iterates over model relations and extracts all nested models from select_dict and +puts them in corresponding list under relation name in extracted dict.keys + +Basically flattens all relation to dictionary of all related models, that can be +used on several models and extract all of their children into dictionary of lists +witch children models. + +Goes also into nested relations if needed (specified in select_dict). + +**Arguments**: + +- `model (Model)`: parent Model +- `model_type (Type[Model])`: parent model class +- `select_dict (Dict)`: dictionary of related models from select_related +- `extracted (Dict)`: dictionary with already extracted models + + +#### extract\_models\_to\_dict\_of\_lists + +```python +extract_models_to_dict_of_lists(model_type: Type["Model"], models: Sequence["Model"], select_dict: Dict, extracted: Dict = None) -> Dict +``` + +Receives a list of models and extracts all of the children and their children +into dictionary of lists with children models, flattening the structure to one dict +with all children models under their relation keys. + +**Arguments**: + +- `model_type (Type[Model])`: parent model class +- `models (List[Model])`: list of models from which related models should be extracted. +- `select_dict (Dict)`: dictionary of related models from select_related +- `extracted (Dict)`: dictionary with already extracted models + +**Returns**: + +`(Dict)`: dictionary of lists f related models + diff --git a/docs/api/relations/alias-manager.md b/docs/api/relations/alias-manager.md new file mode 100644 index 0000000..4ffffe9 --- /dev/null +++ b/docs/api/relations/alias-manager.md @@ -0,0 +1,127 @@ + +# relations.alias\_manager + + +#### get\_table\_alias + +```python +get_table_alias() -> str +``` + +Creates a random string that is used to alias tables in joins. +It's necessary that each relation has it's own aliases cause you can link +to the same target tables from multiple fields on one model as well as from +multiple different models in one join. + +**Returns**: + +`(str)`: randomly generated alias + + +## AliasManager Objects + +```python +class AliasManager() +``` + +Keep all aliases of relations between different tables. +One global instance is shared between all models. + + +#### \_\_init\_\_ + +```python + | __init__() -> None +``` + + +#### prefixed\_columns + +```python + | @staticmethod + | prefixed_columns(alias: str, table: sqlalchemy.Table, fields: List = None) -> List[text] +``` + +Creates a list of aliases sqlalchemy text clauses from +string alias and sqlalchemy.Table. + +Optional list of fields to include can be passed to extract only those columns. +List has to have sqlalchemy names of columns (ormar aliases) not the ormar ones. + +**Arguments**: + +- `alias (str)`: alias of given table +- `table (sqlalchemy.Table)`: table from which fields should be aliased +- `fields (Optional[List[str]])`: fields to include + +**Returns**: + +`(List[text])`: list of sqlalchemy text clauses with "column name as aliased name" + + +#### prefixed\_table\_name + +```python + | @staticmethod + | prefixed_table_name(alias: str, name: str) -> text +``` + +Creates text clause with table name with aliased name. + +**Arguments**: + +- `alias (str)`: alias of given table +- `name (str)`: table name + +**Returns**: + +`(sqlalchemy text clause)`: sqlalchemy text clause as "table_name aliased_name" + + +#### add\_relation\_type + +```python + | add_relation_type(source_model: Type["Model"], relation_name: str, reverse_name: str = None, is_multi: bool = False) -> None +``` + +Registers the relations defined in ormar models. +Given the relation it registers also the reverse side of this relation. + +Used by both ForeignKey and ManyToMany relations. + +Each relation is registered as Model name and relation name. +Each alias registered has to be unique. + +Aliases are used to construct joins to assure proper links between tables. +That way you can link to the same target tables from multiple fields +on one model as well as from multiple different models in one join. + +**Arguments**: + +- `source_model (source Model)`: model with relation defined +- `relation_name (str)`: name of the relation to define +- `reverse_name (Optional[str])`: name of related_name fo given relation for m2m relations +- `is_multi (bool)`: flag if relation being registered is a through m2m model + +**Returns**: + +`(None)`: none + + +#### resolve\_relation\_alias + +```python + | resolve_relation_alias(from_model: Type["Model"], relation_name: str) -> str +``` + +Given model and relation name returns the alias for this relation. + +**Arguments**: + +- `from_model (source Model)`: model with relation defined +- `relation_name (str)`: name of the relation field + +**Returns**: + +`(str)`: alias of the relation + diff --git a/docs/api/relations/queryset-proxy.md b/docs/api/relations/queryset-proxy.md new file mode 100644 index 0000000..38cabb2 --- /dev/null +++ b/docs/api/relations/queryset-proxy.md @@ -0,0 +1,595 @@ + +# relations.querysetproxy + + +## QuerysetProxy Objects + +```python +class QuerysetProxy(ormar.QuerySetProtocol) +``` + +Exposes QuerySet methods on relations, but also handles creating and removing +of through Models for m2m relations. + + +#### \_\_init\_\_ + +```python + | __init__(relation: "Relation", type_: "RelationType", qryset: "QuerySet" = None) -> None +``` + + +#### queryset + +```python + | @property + | queryset() -> "QuerySet" +``` + +Returns queryset if it's set, AttributeError otherwise. + +**Returns**: + +`(QuerySet)`: QuerySet + + +#### queryset + +```python + | @queryset.setter + | queryset(value: "QuerySet") -> None +``` + +Set's the queryset. Initialized in RelationProxy. + +**Arguments**: + +- `value (QuerySet)`: QuerySet + + +#### \_assign\_child\_to\_parent + +```python + | _assign_child_to_parent(child: Optional["T"]) -> None +``` + +Registers child in parents RelationManager. + +**Arguments**: + +- `child (Model)`: child to register on parent side. + + +#### \_register\_related + +```python + | _register_related(child: Union["T", Sequence[Optional["T"]]]) -> None +``` + +Registers child/ children in parents RelationManager. + +**Arguments**: + +- `child (Union[Model,List[Model]])`: child or list of children models to register. + + +#### \_clean\_items\_on\_load + +```python + | _clean_items_on_load() -> None +``` + +Cleans the current list of the related models. + + +#### create\_through\_instance + +```python + | async create_through_instance(child: "T") -> None +``` + +Crete a through model instance in the database for m2m relations. + +**Arguments**: + +- `child (Model)`: child model instance + + +#### delete\_through\_instance + +```python + | async delete_through_instance(child: "T") -> None +``` + +Removes through model instance from the database for m2m relations. + +**Arguments**: + +- `child (Model)`: child model instance + + +#### exists + +```python + | async exists() -> bool +``` + +Returns a bool value to confirm if there are rows matching the given criteria +(applied with `filter` and `exclude` if set). + +Actual call delegated to QuerySet. + +**Returns**: + +`(bool)`: result of the check + + +#### count + +```python + | async count() -> int +``` + +Returns number of rows matching the given criteria +(applied with `filter` and `exclude` if set before). + +Actual call delegated to QuerySet. + +**Returns**: + +`(int)`: number of rows + + +#### clear + +```python + | async clear(keep_reversed: bool = True) -> int +``` + +Removes all related models from given relation. + +Removes all through models for m2m relation. + +For reverse FK relations keep_reversed flag marks if the reversed models +should be kept or deleted from the database too (False means that models +will be deleted, and not only removed from relation). + +**Arguments**: + +- `keep_reversed (bool)`: flag if reverse models in reverse FK should be deleted +or not, keep_reversed=False deletes them from database. + +**Returns**: + +`(int)`: number of deleted models + + +#### first + +```python + | async first(**kwargs: Any) -> "Model" +``` + +Gets the first row from the db ordered by primary key column ascending. + +Actual call delegated to QuerySet. + +List of related models is cleared before the call. + +**Arguments**: + +- `kwargs ()`: + +**Returns**: + +`(_asyncio.Future)`: + + +#### get + +```python + | async get(**kwargs: Any) -> "Model" +``` + +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. + +Actual call delegated to QuerySet. + +List of related models is cleared before the call. + +**Raises**: + +- `NoMatch`: if no rows are returned +- `MultipleMatches`: if more than 1 row is returned. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: returned model + + +#### all + +```python + | async all(**kwargs: Any) -> Sequence[Optional["Model"]] +``` + +Returns all rows from a database for given model for set filter options. + +Passing kwargs is a shortcut and equals to calling `filter(**kwrags).all()`. + +If there are no rows meeting the criteria an empty list is returned. + +Actual call delegated to QuerySet. + +List of related models is cleared before the call. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(List[Model])`: list of returned models + + +#### create + +```python + | async create(**kwargs: Any) -> "Model" +``` + +Creates the model instance, saves it in a database and returns the updates model +(with pk populated if not passed and autoincrement is set). + +The allowed kwargs are `Model` fields names and proper value types. + +For m2m relation the through model is created automatically. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: created model + + +#### get\_or\_create + +```python + | async get_or_create(**kwargs: Any) -> "Model" +``` + +Combination of create and get methods. + +Tries to get a row meeting the criteria fro kwargs +and if `NoMatch` exception is raised +it creates a new one with given kwargs. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: returned or created Model + + +#### update\_or\_create + +```python + | async update_or_create(**kwargs: Any) -> "Model" +``` + +Updates the model, or in case there is no match in database creates a new one. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(Model)`: updated or created model + + +#### filter + +```python + | filter(**kwargs: Any) -> "QuerysetProxy" +``` + +Allows you to filter by any `Model` attribute/field +as well as to fetch instances, with a filter across an FK relationship. + +You can use special filter suffix to change the filter operands: + +* exact - like `album__name__exact='Malibu'` (exact match) +* iexact - like `album__name__iexact='malibu'` (exact match case insensitive) +* 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) +* gt - like `position__gt=3` (sql >) +* gte - like `position__gte=3` (sql >=) +* lt - like `position__lt=3` (sql <) +* lte - like `position__lte=3` (sql <=) +* startswith - like `album__name__startswith='Mal'` (exact start match) +* istartswith - like `album__name__istartswith='mal'` (case insensitive) +* endswith - like `album__name__endswith='ibu'` (exact end match) +* iendswith - like `album__name__iendswith='IBU'` (case insensitive) + +Actual call delegated to QuerySet. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(QuerysetProxy)`: filtered QuerysetProxy + + +#### exclude + +```python + | exclude(**kwargs: Any) -> "QuerysetProxy" +``` + +Works exactly the same as filter and all modifiers (suffixes) are the same, +but returns a *not* condition. + +So if you use `filter(name='John')` which is `where name = 'John'` in SQL, +the `exclude(name='John')` equals to `where name <> 'John'` + +Note that all conditions are joined so if you pass multiple values it +becomes a union of conditions. + +`exclude(name='John', age>=35)` will become +`where not (name='John' and age>=35)` + +Actual call delegated to QuerySet. + +**Arguments**: + +- `kwargs (Any)`: fields names and proper value types + +**Returns**: + +`(QuerysetProxy)`: filtered QuerysetProxy + + +#### select\_related + +```python + | select_related(related: Union[List, str]) -> "QuerysetProxy" +``` + +Allows to prefetch related models during the same query. + +**With `select_related` always only one query is run against the database**, +meaning that one (sometimes complicated) join is generated and later nested +models are processed in python. + +To fetch related model use `ForeignKey` names. + +To chain related `Models` relation use double underscores between names. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `related (Union[List, str])`: list of relation field names, can be linked by '__' to nest + +**Returns**: + +`(QuerysetProxy)`: QuerysetProxy + + +#### prefetch\_related + +```python + | prefetch_related(related: Union[List, str]) -> "QuerysetProxy" +``` + +Allows to prefetch related models during query - but opposite to +`select_related` each subsequent model is fetched in a separate database query. + +**With `prefetch_related` always one query per Model is run against the +database**, meaning that you will have multiple queries executed one +after another. + +To fetch related model use `ForeignKey` names. + +To chain related `Models` relation use double underscores between names. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `related (Union[List, str])`: list of relation field names, can be linked by '__' to nest + +**Returns**: + +`(QuerysetProxy)`: QuerysetProxy + + +#### limit + +```python + | limit(limit_count: int) -> "QuerysetProxy" +``` + +You can limit the results to desired number of parent models. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `limit_count (int)`: number of models to limit + +**Returns**: + +`(QuerysetProxy)`: QuerysetProxy + + +#### offset + +```python + | offset(offset: int) -> "QuerysetProxy" +``` + +You can also offset the results by desired number of main models. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `offset (int)`: numbers of models to offset + +**Returns**: + +`(QuerysetProxy)`: QuerysetProxy + + +#### fields + +```python + | fields(columns: Union[List, str, Set, Dict]) -> "QuerysetProxy" +``` + +With `fields()` you can select subset of model columns to limit the data load. + +Note that `fields()` and `exclude_fields()` works both for main models +(on normal queries like `get`, `all` etc.) +as well as `select_related` and `prefetch_related` +models (with nested notation). + +You can select specified fields by passing a `str, List[str], Set[str] or +dict` with nested definition. + +To include related models use notation +`{related_name}__{column}[__{optional_next} etc.]`. + +`fields()` can be called several times, building up the columns to select. + +If you include related models into `select_related()` call but you won't specify +columns for those models in fields - implies a list of all fields for +those nested models. + +Mandatory fields cannot be excluded as it will raise `ValidationError`, +to exclude a field it has to be nullable. + +Pk column cannot be excluded - it's always auto added even if +not explicitly included. + +You can also pass fields to include as dictionary or set. + +To mark a field as included in a dictionary use it's name as key +and ellipsis as value. + +To traverse nested models use nested dictionaries. + +To include fields at last level instead of nested dictionary a set can be used. + +To include whole nested model specify model related field name and ellipsis. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `columns (Union[List, str, Set, Dict])`: columns to include + +**Returns**: + +`(QuerysetProxy)`: QuerysetProxy + + +#### exclude\_fields + +```python + | exclude_fields(columns: Union[List, str, Set, Dict]) -> "QuerysetProxy" +``` + +With `exclude_fields()` you can select subset of model columns that will +be excluded to limit the data load. + +It's the opposite of `fields()` method so check documentation above +to see what options are available. + +Especially check above how you can pass also nested dictionaries +and sets as a mask to exclude fields from whole hierarchy. + +Note that `fields()` and `exclude_fields()` works both for main models +(on normal queries like `get`, `all` etc.) +as well as `select_related` and `prefetch_related` models +(with nested notation). + +Mandatory fields cannot be excluded as it will raise `ValidationError`, +to exclude a field it has to be nullable. + +Pk column cannot be excluded - it's always auto added even +if explicitly excluded. + +Actual call delegated to QuerySet. + +**Arguments**: + +- `columns (Union[List, str, Set, Dict])`: columns to exclude + +**Returns**: + +`(QuerysetProxy)`: QuerysetProxy + + +#### order\_by + +```python + | order_by(columns: Union[List, str]) -> "QuerysetProxy" +``` + +With `order_by()` you can order the results from database based on your +choice of fields. + +You can provide a string with field name or list of strings with fields names. + +Ordering in sql will be applied in order of names you provide in order_by. + +By default if you do not provide ordering `ormar` explicitly orders by +all primary keys + +If you are sorting by nested models that causes that the result rows are +unsorted by the main model `ormar` will combine those children rows into +one main model. + +The main model will never duplicate in the result + +To order by main model field just provide a field name + +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. + +To sort in descending order provide a hyphen in front of the field name + +Actual call delegated to QuerySet. + +**Arguments**: + +- `columns (Union[List, str])`: columns by which models should be sorted + +**Returns**: + +`(QuerysetProxy)`: QuerysetProxy + diff --git a/docs/api/relations/relation-manager.md b/docs/api/relations/relation-manager.md new file mode 100644 index 0000000..818bdd9 --- /dev/null +++ b/docs/api/relations/relation-manager.md @@ -0,0 +1,159 @@ + +# relations.relation\_manager + + +## RelationsManager Objects + +```python +class RelationsManager() +``` + +Manages relations on a Model, each Model has it's own instance. + + +#### \_\_init\_\_ + +```python + | __init__(related_fields: List[Type[ForeignKeyField]] = None, owner: "NewBaseModel" = None) -> None +``` + + +#### \_get\_relation\_type + +```python + | _get_relation_type(field: Type[BaseField]) -> RelationType +``` + +Returns type of the relation declared on a field. + +**Arguments**: + +- `field (Type[BaseField])`: field with relation declaration + +**Returns**: + +`(RelationType)`: type of the relation defined on field + + +#### \_add\_relation + +```python + | _add_relation(field: Type[BaseField]) -> None +``` + +Registers relation in the manager. +Adds Relation instance under field.name. + +**Arguments**: + +- `field (Type[BaseField])`: field with relation declaration + + +#### \_\_contains\_\_ + +```python + | __contains__(item: str) -> bool +``` + +Checks if relation with given name is already registered. + +**Arguments**: + +- `item (str)`: name of attribute + +**Returns**: + +`(bool)`: result of the check + + +#### get + +```python + | get(name: str) -> Optional[Union["T", Sequence["T"]]] +``` + +Returns the related model/models if relation is set. +Actual call is delegated to Relation instance registered under relation name. + +**Arguments**: + +- `name (str)`: name of the relation + +**Returns**: + +`(Optional[Union[Model, List[Model]])`: related model or list of related models if set + + +#### \_get + +```python + | _get(name: str) -> Optional[Relation] +``` + +Returns the actual relation and not the related model(s). + +**Arguments**: + +- `name (str)`: name of the relation + +**Returns**: + +`(ormar.relations.relation.Relation)`: Relation instance + + +#### add + +```python + | @staticmethod + | add(parent: "Model", child: "Model", child_name: str, virtual: bool, relation_name: str) -> None +``` + +Adds relation on both sides -> meaning on both child and parent models. +One side of the relation is always weakref proxy to avoid circular refs. + +Based on the side from which relation is added and relation name actual names +of parent and child relations are established. The related models are registered +on both ends. + +**Arguments**: + +- `parent (Model)`: parent model on which relation should be registered +- `child (Model)`: child model to register +- `child_name (str)`: potential child name used if related name is not set +- `virtual (bool)`: +- `relation_name (str)`: name of the relation + + +#### remove + +```python + | remove(name: str, child: Union["NewBaseModel", Type["NewBaseModel"]]) -> None +``` + +Removes given child from relation with given name. +Since you can have many relations between two models you need to pass a name +of relation from which you want to remove the child. + +**Arguments**: + +- `name (str)`: name of the relation +- `child (Union[Model, Type[Model]])`: child to remove from relation + + +#### remove\_parent + +```python + | @staticmethod + | remove_parent(item: Union["NewBaseModel", Type["NewBaseModel"]], parent: "Model", name: str) -> None +``` + +Removes given parent from relation with given name. +Since you can have many relations between two models you need to pass a name +of relation from which you want to remove the parent. + +**Arguments**: + +- `item (Union[Model, Type[Model]])`: model with parent registered +- `parent (Model)`: parent Model +- `name (str)`: name of the relation + diff --git a/docs/api/relations/relation-proxy.md b/docs/api/relations/relation-proxy.md new file mode 100644 index 0000000..a25122a --- /dev/null +++ b/docs/api/relations/relation-proxy.md @@ -0,0 +1,151 @@ + +# relations.relation\_proxy + + +## RelationProxy Objects + +```python +class RelationProxy(list) +``` + +Proxy of the Relation that is a list with special methods. + + +#### \_\_init\_\_ + +```python + | __init__(relation: "Relation", type_: "RelationType", field_name: str, data_: Any = None) -> None +``` + + +#### related\_field\_name + +```python + | @property + | related_field_name() -> str +``` + +On first access calculates the name of the related field, later stored in +_related_field_name property. + +**Returns**: + +`(str)`: name of the related field + + +#### \_\_getattribute\_\_ + +```python + | __getattribute__(item: str) -> Any +``` + +Since some QuerySetProxy methods overwrite builtin list methods we +catch calls to them and delegate it to QuerySetProxy instead. + +**Arguments**: + +- `item (str)`: name of attribute + +**Returns**: + +`(Any)`: value of attribute + + +#### \_\_getattr\_\_ + +```python + | __getattr__(item: str) -> Any +``` + +Delegates calls for non existing attributes to QuerySetProxy. + +**Arguments**: + +- `item (str)`: name of attribute/method + +**Returns**: + +`(method)`: method from QuerySetProxy if exists + + +#### \_initialize\_queryset + +```python + | _initialize_queryset() -> None +``` + +Initializes the QuerySetProxy if not yet initialized. + + +#### \_check\_if\_queryset\_is\_initialized + +```python + | _check_if_queryset_is_initialized() -> bool +``` + +Checks if the QuerySetProxy is already set and ready. + +**Returns**: + +`(bool)`: result of the check + + +#### \_check\_if\_model\_saved + +```python + | _check_if_model_saved() -> None +``` + +Verifies if the parent model of the relation has been already saved. +Otherwise QuerySetProxy cannot filter by parent primary key. + + +#### \_set\_queryset + +```python + | _set_queryset() -> "QuerySet" +``` + +Creates new QuerySet with relation model and pre filters it with currents +parent model primary key, so all queries by definition are already related +to the parent model only, without need for user to filter them. + +**Returns**: + +`(QuerySet)`: initialized QuerySet + + +#### remove + +```python + | async remove(item: "Model", keep_reversed: bool = True) -> None +``` + +Removes the item from relation with parent. + +Through models are automatically deleted for m2m relations. + +For reverse FK relations keep_reversed flag marks if the reversed models +should be kept or deleted from the database too (False means that models +will be deleted, and not only removed from relation). + +**Arguments**: + +- `item (Model)`: child to remove from relation +- `keep_reversed (bool)`: flag if the reversed model should be kept or deleted too + + +#### add + +```python + | async add(item: "Model") -> None +``` + +Adds child model to relation. + +For ManyToMany relations through instance is automatically created. + +**Arguments**: + +- `item (Model)`: child to add to relation + diff --git a/docs/api/relations/relation.md b/docs/api/relations/relation.md new file mode 100644 index 0000000..141d1b3 --- /dev/null +++ b/docs/api/relations/relation.md @@ -0,0 +1,128 @@ + +# relations.relation + + +## RelationType Objects + +```python +class RelationType(Enum) +``` + +Different types of relations supported by ormar: + +* ForeignKey = PRIMARY +* reverse ForeignKey = REVERSE +* ManyToMany = MULTIPLE + + +#### PRIMARY + + +#### REVERSE + + +#### MULTIPLE + + +## Relation Objects + +```python +class Relation() +``` + +Keeps related Models and handles adding/removing of the children. + + +#### \_\_init\_\_ + +```python + | __init__(manager: "RelationsManager", type_: RelationType, field_name: str, to: Type["T"], through: Type["T"] = None) -> None +``` + +Initialize the Relation and keep the related models either as instances of +passed Model, or as a RelationProxy which is basically a list of models with +some special behavior, as it exposes QuerySetProxy and allows querying the +related models already pre filtered by parent model. + +**Arguments**: + +- `manager (RelationsManager)`: reference to relation manager +- `type_ (RelationType)`: type of the relation +- `field_name (str)`: name of the relation field +- `to (Type[Model])`: model to which relation leads to +- `through (Type[Model])`: model through which relation goes for m2m relations + + +#### \_clean\_related + +```python + | _clean_related() -> None +``` + +Removes dead weakrefs from RelationProxy. + + +#### \_find\_existing + +```python + | _find_existing(child: Union["NewBaseModel", Type["NewBaseModel"]]) -> Optional[int] +``` + +Find child model in RelationProxy if exists. + +**Arguments**: + +- `child (Model)`: child model to find + +**Returns**: + +`(Optional[ind])`: index of child in RelationProxy + + +#### add + +```python + | add(child: "T") -> None +``` + +Adds child Model to relation, either sets child as related model or adds +it to the list in RelationProxy depending on relation type. + +**Arguments**: + +- `child (Model)`: model to add to relation + + +#### remove + +```python + | remove(child: Union["NewBaseModel", Type["NewBaseModel"]]) -> None +``` + +Removes child Model from relation, either sets None as related model or removes +it from the list in RelationProxy depending on relation type. + +**Arguments**: + +- `child (Model)`: model to remove from relation + + +#### get + +```python + | get() -> Optional[Union[List["T"], "T"]] +``` + +Return the related model or models from RelationProxy. + +**Returns**: + +`(Optional[Union[List[Model], Model]])`: related model/models if set + + +#### \_\_repr\_\_ + +```python + | __repr__() -> str +``` + diff --git a/docs/api/relations/utils.md b/docs/api/relations/utils.md new file mode 100644 index 0000000..cf3c945 --- /dev/null +++ b/docs/api/relations/utils.md @@ -0,0 +1,26 @@ + +# relations.utils + + +#### get\_relations\_sides\_and\_names + +```python +get_relations_sides_and_names(to_field: Type[BaseField], parent: "Model", child: "Model", child_name: str, virtual: bool, relation_name: str) -> Tuple["Model", "Model", str, str] +``` + +Determines the names of child and parent relations names, as well as +changes one of the sides of the relation into weakref.proxy to model. + +**Arguments**: + +- `to_field (BaseField)`: field with relation definition +- `parent (Model)`: parent model +- `child (Model)`: child model +- `child_name (str)`: name of the child +- `virtual (bool)`: flag if relation is virtual +- `relation_name ()`: + +**Returns**: + +`(Tuple["Model", "Model", str, str])`: parent, child, child_name, to_name + diff --git a/docs/api/signals/decorators.md b/docs/api/signals/decorators.md new file mode 100644 index 0000000..de7fe8b --- /dev/null +++ b/docs/api/signals/decorators.md @@ -0,0 +1,130 @@ + +# decorators.signals + + +#### receiver + +```python +receiver(signal: str, senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for given signal name. + +**Arguments**: + +- `signal (str)`: name of the signal to register to +- `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\_save + +```python +post_save(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for post_save 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\_update + +```python +post_update(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for post_update 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\_delete + +```python +post_delete(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for post_delete 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\_save + +```python +pre_save(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for pre_save 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\_update + +```python +pre_update(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for pre_update 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\_delete + +```python +pre_delete(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable +``` + +Connect given function to all senders for pre_delete 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/api/signals/signal.md b/docs/api/signals/signal.md new file mode 100644 index 0000000..547ecf8 --- /dev/null +++ b/docs/api/signals/signal.md @@ -0,0 +1,143 @@ + +# signals + +Signals and SignalEmitter that gathers the signals on models Meta. +Used to signal receivers functions about events, i.e. post_save, pre_delete etc. + + +#### \_\_all\_\_ + + +# signals.signal + + +#### callable\_accepts\_kwargs + +```python +callable_accepts_kwargs(func: Callable) -> bool +``` + +Checks if function accepts **kwargs. + +**Arguments**: + +- `func (function)`: function which signature needs to be checked + +**Returns**: + +`(bool)`: result of the check + + +#### make\_id + +```python +make_id(target: Any) -> Union[int, Tuple[int, int]] +``` + +Creates id of a function or method to be used as key to store signal + +**Arguments**: + +- `target (Any)`: target which id we want + +**Returns**: + +`(int)`: id of the target + + +## Signal Objects + +```python +class Signal() +``` + +Signal that notifies all receiver functions. +In ormar used by models to send pre_save, post_save etc. signals. + + +#### \_\_init\_\_ + +```python + | __init__() -> None +``` + + +#### connect + +```python + | connect(receiver: Callable) -> None +``` + +Connects given receiver function to the signal. + +**Raises**: + +- `SignalDefinitionError`: if receiver is not callable +or not accept **kwargs + +**Arguments**: + +- `receiver (Callable)`: receiver function + + +#### disconnect + +```python + | disconnect(receiver: Callable) -> bool +``` + +Removes the receiver function from the signal. + +**Arguments**: + +- `receiver (Callable)`: receiver function + +**Returns**: + +`(bool)`: flag if receiver was removed + + +#### send + +```python + | async send(sender: Type["Model"], **kwargs: Any) -> None +``` + +Notifies all receiver functions with given kwargs + +**Arguments**: + +- `sender (Type["Model"])`: model that sends the signal +- `kwargs (Any)`: arguments passed to receivers + + +## SignalEmitter Objects + +```python +class SignalEmitter() +``` + +Emitter that registers the signals in internal dictionary. +If signal with given name does not exist it's auto added on access. + + +#### \_\_init\_\_ + +```python + | __init__() -> None +``` + + +#### \_\_getattr\_\_ + +```python + | __getattr__(item: str) -> Signal +``` + + +#### \_\_setattr\_\_ + +```python + | __setattr__(key: str, value: Any) -> None +``` + diff --git a/docs/releases.md b/docs/releases.md index ca9c939..580c0f1 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -27,6 +27,7 @@ as the same model can be registered multiple times and `ormar` needs to know fro * Divide relations section into subsections * Divide fields section into subsections * Add model inheritance section +* Add API (BETA) documentation # 0.7.5 diff --git a/mkdocs.yml b/mkdocs.yml index 5a48d36..126fc97 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,11 +24,58 @@ nav: - PyCharm plugin: plugin.md - Contributing: contributing.md - Release Notes: releases.md + - Api (BETA): + - Index: api/index.md + - Models: + - Helpers: + - api/models/helpers/models.md + - api/models/helpers/pydantic.md + - api/models/helpers/relations.md + - api/models/helpers/sqlalchemy.md + - Mixins: + - Alias Mixin: api/models/mixins/alias-mixin.md + - Excludable Mixin: api/models/mixins/excludable-mixin.md + - Merge Model Mixin: api/models/mixins/merge-model-mixin.md + - Prefetch Query Mixin: api/models/mixins/prefetch-query-mixin.md + - Relation Mixin: api/models/mixins/relation-mixin.md + - Save Prepare Mixin: api/models/mixins/save-prepare-mixin.md + - api/models/model.md + - New BaseModel: api/models/new-basemodel.md + - Model Table Proxy: api/models/model-table-proxy.md + - Model Metaclass: api/models/model-metaclass.md + - Fields: + - Base Field: api/fields/base-field.md + - Model Fields: api/fields/model-fields.md + - Foreign Key: api/fields/foreign-key.md + - Many To Many: api/fields/many-to-many.md + - api/fields/decorators.md + - Query Set: + - Query Set: api/query-set/query-set.md + - api/query-set/query.md + - Prefetch Query: api/query-set/prefetch-query.md + - api/query-set/join.md + - api/query-set/clause.md + - Filter Query: api/query-set/filter-query.md + - Order Query: api/query-set/order-query.md + - Limit Query: api/query-set/limit-query.md + - Offset Query: api/query-set/offset-query.md + - api/query-set/utils.md + - Relations: + - Relation Manager: api/relations/relation-manager.md + - api/relations/relation.md + - Relation Proxy: api/relations/relation-proxy.md + - Queryset Proxy: api/relations/queryset-proxy.md + - Alias Manager: api/relations/alias-manager.md + - api/relations/utils.md + - Signals: + - api/signals/signal.md + - api/signals/decorators.md + - Exceptions: api/exceptions.md repo_name: collerek/ormar repo_url: https://github.com/collerek/ormar -google_analytics: - - UA-72514911-3 - - auto +#google_analytics: +# - UA-72514911-3 +# - auto theme: name: material highlightjs: true diff --git a/ormar/__init__.py b/ormar/__init__.py index 044217b..88f81ba 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -1,3 +1,24 @@ +""" +The `ormar` package is an async mini ORM for Python, with support for **Postgres, +MySQL**, and **SQLite**. + +The main benefit of using `ormar` are: + +* getting an **async ORM that can be used with async frameworks** +(fastapi, starlette etc.) +* getting just **one model to maintain** - you don't have to maintain pydantic +and other orm model (sqlalchemy, peewee, gino etc.) + +The goal was to create a simple ORM that can be **used directly +(as request and response models) +with `fastapi`** that bases it's data validation on pydantic. + +Ormar - apart form obvious ORM in name - get it's name from ormar in swedish which means +snakes, and ormar(e) in italian which means cabinet. + +And what's a better name for python ORM than snakes cabinet :) + +""" from ormar.decorators import ( post_delete, post_save, diff --git a/ormar/decorators/__init__.py b/ormar/decorators/__init__.py index 2b5e668..69925ce 100644 --- a/ormar/decorators/__init__.py +++ b/ormar/decorators/__init__.py @@ -2,6 +2,7 @@ Module with all decorators that are exposed for users. Currently only: + * property_field - exposing @property like function as field in Model.dict() * predefined signals decorators (pre/post + save/update/delete) diff --git a/ormar/decorators/property_field.py b/ormar/decorators/property_field.py index 732cff7..69ec9da 100644 --- a/ormar/decorators/property_field.py +++ b/ormar/decorators/property_field.py @@ -13,7 +13,7 @@ def property_field(func: Callable) -> Union[property, Callable]: mypy validation will complain about this. Note that "fields" exposed like this do not go through validation. - :raises: ModelDefinitionError if method has any other argument than self. + :raises ModelDefinitionError: if method has any other argument than self. :param func: decorated function to be exposed :type func: Callable :return: decorated function passed in func param, with set __property_field__ = True diff --git a/ormar/exceptions.py b/ormar/exceptions.py index 3dbf763..cef4c83 100644 --- a/ormar/exceptions.py +++ b/ormar/exceptions.py @@ -1,3 +1,8 @@ +""" +Gathers all exceptions thrown by ormar. +""" + + class AsyncOrmException(Exception): """ Base ormar Exception @@ -8,7 +13,8 @@ class AsyncOrmException(Exception): class ModelDefinitionError(AsyncOrmException): """ - Raised for errors related to the model definition itself. + Raised for errors related to the model definition itself: + * setting @property_field on method with arguments other than func(self) * defining a Field without required parameters * defining a model with more than one primary_key @@ -46,7 +52,8 @@ class MultipleMatches(AsyncOrmException): class QueryDefinitionError(AsyncOrmException): """ - Raised for errors in query definition. + Raised for errors in query definition: + * using contains or icontains filter with instance of the Model * using Queryset.update() without filter and setting each flag to True * using Queryset.delete() without filter and setting each flag to True diff --git a/ormar/fields/foreign_key.py b/ormar/fields/foreign_key.py index b920e0c..7a6bcb0 100644 --- a/ormar/fields/foreign_key.py +++ b/ormar/fields/foreign_key.py @@ -77,6 +77,11 @@ class UniqueColumns(UniqueConstraint): @dataclass class ForeignKeyConstraint: + """ + Internal container to store ForeignKey definitions used later + to produce sqlalchemy.ForeignKeys + """ + name: str ondelete: str onupdate: str @@ -114,10 +119,10 @@ def ForeignKey( # noqa CFQ002 It is for reversed FK and auto generated FK on through model in Many2Many relations. :type virtual: bool :param onupdate: parameter passed to sqlalchemy.ForeignKey. - How to treat child rows on update of parent (the one wher FK is defined) model. + How to treat child rows on update of parent (the one where FK is defined) model. :type onupdate: str :param ondelete: parameter passed to sqlalchemy.ForeignKey. - How to treat child rows on delete of parent (the one wher FK is defined) model. + How to treat child rows on delete of parent (the one where FK is defined) model. :type ondelete: str :param kwargs: all other args to be populated by BaseField :type kwargs: Any diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index ef35030..4a30ee3 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -116,6 +116,10 @@ class ModelFieldFactory: class String(ModelFieldFactory, str): + """ + String field factory that construct Field classes and populated their values. + """ + _type = str def __new__( # type: ignore # noqa CFQ002 @@ -142,10 +146,24 @@ class String(ModelFieldFactory, str): @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.String(length=kwargs.get("max_length")) @classmethod def validate(cls, **kwargs: Any) -> None: + """ + Used to validate if all required parameters on a given field type are set. + :param kwargs: all params passed during construction + :type kwargs: Any + """ max_length = kwargs.get("max_length", None) if max_length is None or max_length <= 0: raise ModelDefinitionError( @@ -154,6 +172,10 @@ class String(ModelFieldFactory, str): class Integer(ModelFieldFactory, int): + """ + Integer field factory that construct Field classes and populated their values. + """ + _type = int def __new__( # type: ignore @@ -184,10 +206,23 @@ class Integer(ModelFieldFactory, int): @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.Integer() class Text(ModelFieldFactory, str): + """ + Text field factory that construct Field classes and populated their values. + """ + _type = str def __new__( # type: ignore @@ -206,10 +241,23 @@ class Text(ModelFieldFactory, str): @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.Text() class Float(ModelFieldFactory, float): + """ + Float field factory that construct Field classes and populated their values. + """ + _type = float def __new__( # type: ignore @@ -234,6 +282,15 @@ class Float(ModelFieldFactory, float): @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.Float() @@ -246,46 +303,115 @@ if TYPE_CHECKING: # pragma: nocover else: class Boolean(ModelFieldFactory, int): + """ + Boolean field factory that construct Field classes and populated their values. + """ + _type = bool @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.Boolean() class DateTime(ModelFieldFactory, datetime.datetime): + """ + DateTime field factory that construct Field classes and populated their values. + """ + _type = datetime.datetime @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.DateTime() class Date(ModelFieldFactory, datetime.date): + """ + Date field factory that construct Field classes and populated their values. + """ + _type = datetime.date @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.Date() class Time(ModelFieldFactory, datetime.time): + """ + Time field factory that construct Field classes and populated their values. + """ + _type = datetime.time @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.Time() class JSON(ModelFieldFactory, pydantic.Json): + """ + JSON field factory that construct Field classes and populated their values. + """ + _type = pydantic.Json @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.JSON() class BigInteger(Integer, int): + """ + BigInteger field factory that construct Field classes and populated their values. + """ + _type = int def __new__( # type: ignore @@ -316,10 +442,23 @@ class BigInteger(Integer, int): @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ return sqlalchemy.BigInteger() class Decimal(ModelFieldFactory, decimal.Decimal): + """ + Decimal field factory that construct Field classes and populated their values. + """ + _type = decimal.Decimal def __new__( # type: ignore # noqa CFQ002 @@ -359,12 +498,26 @@ class Decimal(ModelFieldFactory, decimal.Decimal): @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ precision = kwargs.get("precision") scale = kwargs.get("scale") return sqlalchemy.DECIMAL(precision=precision, scale=scale) @classmethod def validate(cls, **kwargs: Any) -> None: + """ + Used to validate if all required parameters on a given field type are set. + :param kwargs: all params passed during construction + :type kwargs: Any + """ precision = kwargs.get("precision") scale = kwargs.get("scale") if precision is None or precision < 0 or scale is None or scale < 0: @@ -374,6 +527,10 @@ class Decimal(ModelFieldFactory, decimal.Decimal): class UUID(ModelFieldFactory, uuid.UUID): + """ + UUID field factory that construct Field classes and populated their values. + """ + _type = uuid.UUID def __new__( # type: ignore # noqa CFQ002 @@ -392,5 +549,14 @@ class UUID(ModelFieldFactory, uuid.UUID): @classmethod def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ uuid_format = kwargs.get("uuid_format", "hex") return sqlalchemy_uuid.UUID(uuid_format=uuid_format) diff --git a/ormar/models/__init__.py b/ormar/models/__init__.py index 9e366f1..88a39c6 100644 --- a/ormar/models/__init__.py +++ b/ormar/models/__init__.py @@ -1,3 +1,9 @@ +""" +Definition of Model, it's parents NewBaseModel and mixins used by models. +Also defines a Metaclass that handles all constructions and relations registration, +ass well as vast number of helper functions for pydantic, sqlalchemy and relations. +""" + from ormar.models.newbasemodel import NewBaseModel # noqa I100 from ormar.models.model import Model # noqa I100 diff --git a/ormar/models/helpers/models.py b/ormar/models/helpers/models.py index 4b5a364..61da7bb 100644 --- a/ormar/models/helpers/models.py +++ b/ormar/models/helpers/models.py @@ -61,7 +61,7 @@ def validate_related_names_in_relations( (populated by default as model.name.lower()+'s'). Also related_names have to be unique for given related model. - :raises: ModelDefinitionError if validation of related_names fail + :raises ModelDefinitionError: if validation of related_names fail :param model_fields: dictionary of declared ormar model fields :type model_fields: Dict[str, ormar.Field] :param new_model: diff --git a/ormar/models/helpers/relations.py b/ormar/models/helpers/relations.py index c9f8f48..4792531 100644 --- a/ormar/models/helpers/relations.py +++ b/ormar/models/helpers/relations.py @@ -157,7 +157,7 @@ def verify_related_name_dont_duplicate( auto generated) is already used on related model, but is connected with other model than the one that we connect right now. - :raises: ModelDefinitionError if name is already used but lead to different related + :raises ModelDefinitionError: if name is already used but lead to different related model :param child: related Model class :type child: ormar.models.metaclass.ModelMetaclass @@ -191,7 +191,7 @@ def reverse_field_not_already_registered( """ Checks if child is already registered in parents pydantic fields. - :raises: ModelDefinitionError if related name is already used but lead to different + :raises ModelDefinitionError: if related name is already used but lead to different related model :param child: related Model class :type child: ormar.models.metaclass.ModelMetaclass diff --git a/ormar/models/helpers/sqlalchemy.py b/ormar/models/helpers/sqlalchemy.py index 723cb54..37cdaa9 100644 --- a/ormar/models/helpers/sqlalchemy.py +++ b/ormar/models/helpers/sqlalchemy.py @@ -84,7 +84,7 @@ def check_pk_column_validity( was not already set (only one allowed per model) and if field is not marked as pydantic_only as it needs to be a database field. - :raises: ModelDefintionError if pkname already set or field is pydantic_only + :raises ModelDefintionError: if pkname already set or field is pydantic_only :param field_name: name of field :type field_name: str :param field: ormar.Field @@ -121,7 +121,7 @@ def sqlalchemy_columns_from_model_fields( Append fields to columns if it's not pydantic_only, virtual ForeignKey or ManyToMany field. - :raises: ModelDefinitionError if validation of related_names fail, + :raises ModelDefinitionError: if validation of related_names fail, or pkname validation fails. :param model_fields: dictionary of declared ormar model fields :type model_fields: Dict[str, ormar.Field] @@ -162,7 +162,7 @@ def populate_meta_tablename_columns_and_pk( If not calls the sqlalchemy_columns_from_model_fields to populate columns from ormar.fields definitions. - :raises: if pkname is not present raises ModelDefinitionError. + :raises ModelDefinitionError: if pkname is not present raises ModelDefinitionError. Each model has to have pk. :param name: name of the current Model diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 9d438d5..17e844d 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -48,7 +48,7 @@ CONFIG_KEY = "Config" class ModelMeta: """ Class used for type hinting. - Users can subclass this one for conveniance but it's not required. + Users can subclass this one for convenience but it's not required. The only requirement is that ormar.Model has to have inner class with name Meta. """ @@ -86,7 +86,7 @@ def choices_validator(cls: Type["Model"], values: Dict[str, Any]) -> Dict[str, A Validator that is attached to pydantic model pre root validators. Validator checks if field value is in field.choices list. - :raises: ValueError if field value is outside of allowed choices. + :raises ValueError: if field value is outside of allowed choices. :param cls: constructed class :type cls: Model class :param values: dictionary of field values (pydantic side) @@ -321,7 +321,7 @@ def copy_data_from_parent_model( # noqa: CCR001 Since relation fields requires different related_name for different children - :raises: ModelDefinitionError if non abstract model is subclassed + :raises ModelDefinitionError: if non abstract model is subclassed :param base_class: one of the parent classes :type base_class: Model or model parent class :param curr_class: current constructed class @@ -500,6 +500,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): Construct parent pydantic Metaclass/ Model. If class has Meta class declared (so actual ormar Models) it also: + * populate sqlalchemy columns, pkname and tables from model_fields * register reverse relationships on related models * registers all relations in alias manager that populates table_prefixes diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index 0450683..0e0884f 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -12,7 +12,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): """ @classmethod - def _prepare_model_to_save(cls, new_kwargs: dict) -> dict: + def prepare_model_to_save(cls, new_kwargs: dict) -> dict: """ Combines all preparation methods before saving. Removes primary key for if it's nullable or autoincrement pk field, diff --git a/ormar/models/model.py b/ormar/models/model.py index 9412a26..63388cb 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -456,7 +456,7 @@ class Model(NewBaseModel): Sets model save status to True. - :raises: If the pk column is not set will throw ModelPersistenceError + :raises ModelPersistenceError: If the pk column is not set :param kwargs: list of fields to update as field=value pairs :type kwargs: Any @@ -512,7 +512,7 @@ class Model(NewBaseModel): Be careful as the related models can be overwritten by pk_only models in load. Does NOT refresh the related models fields if they were loaded before. - :raises: If given pk is not found in database the NoMatch exception is raised. + :raises NoMatch: If given pk is not found in database. :return: reloaded Model :rtype: Model diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index dc0026c..6a3ea7a 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -103,7 +103,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass should be explicitly set to None, as otherwise pydantic will try to populate them with their default values if default is set. - :raises: ModelError if abstract model is initialized or unknown field is passed + :raises ModelError: if abstract model is initialized or unknown field is passed :param args: ignored args :type args: Any :param kwargs: keyword arguments - all fields values and some special params diff --git a/ormar/queryset/clause.py b/ormar/queryset/clause.py index d0963cf..4746db4 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -245,7 +245,7 @@ class QueryClause: Escapes the special characters ["%", "_"] if needed. Adds `%` for `like` queries. - :raises: QueryDefinitionError if contains or icontains is used with + :raises QueryDefinitionError: if contains or icontains is used with ormar model instance :param op: operator used in query :type op: str diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index ffa31cc..599f4f0 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -647,8 +647,8 @@ class QuerySet: """ Gets the first row from the db ordered by primary key column ascending. - :raises: NoMatch if no rows are returned - :raises: MultipleMatches if more than 1 row is returned. + :raises NoMatch: if no rows are returned + :raises MultipleMatches: if more than 1 row is returned. :param kwargs: fields names and proper value types :type kwargs: Any :return: returned model @@ -675,8 +675,8 @@ class QuerySet: Passing a criteria is actually calling filter(**kwargs) method described below. - :raises: NoMatch if no rows are returned - :raises: MultipleMatches if more than 1 row is returned. + :raises NoMatch: if no rows are returned + :raises MultipleMatches: if more than 1 row is returned. :param kwargs: fields names and proper value types :type kwargs: Any :return: returned model @@ -771,7 +771,7 @@ class QuerySet: :rtype: Model """ new_kwargs = dict(**kwargs) - new_kwargs = self.model._prepare_model_to_save(new_kwargs) + new_kwargs = self.model.prepare_model_to_save(new_kwargs) expr = self.table.insert() expr = expr.values(**new_kwargs) @@ -817,7 +817,7 @@ class QuerySet: ready_objects = [] for objt in objects: new_kwargs = objt.dict() - new_kwargs = objt._prepare_model_to_save(new_kwargs) + new_kwargs = objt.prepare_model_to_save(new_kwargs) ready_objects.append(new_kwargs) expr = self.table.insert() diff --git a/ormar/queryset/utils.py b/ormar/queryset/utils.py index 4c82310..12a7fa5 100644 --- a/ormar/queryset/utils.py +++ b/ormar/queryset/utils.py @@ -88,7 +88,7 @@ def convert_set_to_required_dict(set_to_convert: set) -> Dict: :param set_to_convert: set to convert to dict :type set_to_convert: set :return: set converted to dict of ellipsis - :rtype: Dict[str, ellipsis] + :rtype: Dict """ new_dict = dict() for key in set_to_convert: diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index 1356a3a..386ac63 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -211,8 +211,8 @@ class QuerysetProxy(ormar.QuerySetProtocol): List of related models is cleared before the call. - :raises: NoMatch if no rows are returned - :raises: MultipleMatches if more than 1 row is returned. + :raises NoMatch: if no rows are returned + :raises MultipleMatches: if more than 1 row is returned. :param kwargs: fields names and proper value types :type kwargs: Any :return: returned model diff --git a/ormar/relations/relation.py b/ormar/relations/relation.py index cb4561f..d037d4a 100644 --- a/ormar/relations/relation.py +++ b/ormar/relations/relation.py @@ -16,10 +16,11 @@ if TYPE_CHECKING: # pragma no cover class RelationType(Enum): """ - Different types of relations supported by ormar. - ForeignKey = PRIMARY - reverse ForeignKey = REVERSE - ManyToMany = MULTIPLE + Different types of relations supported by ormar: + + * ForeignKey = PRIMARY + * reverse ForeignKey = REVERSE + * ManyToMany = MULTIPLE """ PRIMARY = 1 diff --git a/ormar/signals/__init__.py b/ormar/signals/__init__.py index 6f4706e..127fc2f 100644 --- a/ormar/signals/__init__.py +++ b/ormar/signals/__init__.py @@ -1,3 +1,7 @@ +""" +Signals and SignalEmitter that gathers the signals on models Meta. +Used to signal receivers functions about events, i.e. post_save, pre_delete etc. +""" from ormar.signals.signal import Signal, SignalEmitter __all__ = ["Signal", "SignalEmitter"] diff --git a/ormar/signals/signal.py b/ormar/signals/signal.py index 0dfff24..e2c5275 100644 --- a/ormar/signals/signal.py +++ b/ormar/signals/signal.py @@ -14,7 +14,7 @@ def callable_accepts_kwargs(func: Callable) -> bool: :param func: function which signature needs to be checked :type func: function - :return: + :return: result of the check :rtype: bool """ return any( @@ -51,7 +51,7 @@ class Signal: """ Connects given receiver function to the signal. - :raises: SignalDefinitionError if receiver is not callable + :raises SignalDefinitionError: if receiver is not callable or not accept **kwargs :param receiver: receiver function :type receiver: Callable diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml new file mode 100644 index 0000000..36dea6e --- /dev/null +++ b/pydoc-markdown.yml @@ -0,0 +1,152 @@ +output_directory: docs/api +loaders: + - type: python + search_path: [ormar/] +processors: + - type: filter + documented_only: false + skip_empty_modules: false + exclude_private: false + exclude_special: false + - type: sphinx + - type: crossref +renderer: + type: mkdocs + pages: + - title: Models + children: + - title: Model Metaclass + contents: + - models.metaclass.* + - title: Model + contents: + - models.model.* + - title: New BaseModel + contents: + - models.newbasemodel.* + - title: Model Table Proxy + contents: + - models.modelproxy.* + - title: Helpers + children: + - title: models + contents: + - models.helpers.models.* + - title: pydantic + contents: + - models.helpers.pydantic.* + - title: relations + contents: + - models.helpers.relations.* + - title: sqlalchemy + contents: + - models.helpers.sqlalchemy.* + - title: Mixins + children: + - title: Alias Mixin + contents: + - models.mixins.alias_mixin.* + - title: Excludable Mixin + contents: + - models.mixins.excludable_mixin.* + - title: Merge Model Mixin + contents: + - models.mixins.merge_mixin.* + - title: Prefetch Query Mixin + contents: + - models.mixins.prefetch_mixin.* + - title: Relation Mixin + contents: + - models.mixins.relation_mixin.* + - title: Save Prepare Mixin + contents: + - models.mixins.save_mixin.* + - title: Fields + children: + - title: Base Field + contents: + - fields.base.* + - title: Model Fields + contents: + - fields.model_fields.* + - title: Foreign Key + contents: + - fields.foreign_key.* + - title: Many To Many + contents: + - fields.many_to_many.* + - title: Decorators + contents: + - decorators.property_field.* + - title: Query Set + children: + - title: Query Set + contents: + - queryset.queryset.* + - title: Query + contents: + - queryset.query.* + - title: Prefetch Query + contents: + - queryset.prefetch_query.* + - title: Join + contents: + - queryset.join.* + - title: Clause + contents: + - queryset.clause.* + - title: Filter Query + contents: + - queryset.filter_query.* + - title: Order Query + contents: + - queryset.order_query.* + - title: Limit Query + contents: + - queryset.limit_query.* + - title: Offset Query + contents: + - queryset.offset_query.* + - title: Utils + contents: + - queryset.utils.* + - title: Relations + children: + - title: Relation Manager + contents: + - relations.relation_manager.* + - title: Relation + contents: + - relations.relation.* + - title: Relation Proxy + contents: + - relations.relation_proxy.* + - title: Queryset Proxy + contents: + - relations.querysetproxy.* + - title: Alias Manager + contents: + - relations.alias_manager.* + - title: Utils + contents: + - relations.utils.* + - title: Signals + children: + - title: Signal + contents: + - signals.* + - title: Decorators + contents: + - decorators.signals.* + - title: Exceptions + contents: + - exceptions.* + mkdocs_config: + site_name: Ormar + theme: + name: material + highlightjs: true + hljs_languages: + - python + palette: + primary: indigo diff --git a/requirements.txt b/requirements.txt index 1e8b113..f47a05d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,5 +36,11 @@ flake8-cognitive-complexity flake8-functions flake8-expression-complexity +# Documantation +mkdocs +mkdocs-material +mkdocs-material-extensions +pydoc-markdown + # Performance testing yappi