From e4b4d9451d8b9470d77359b9821fb7d185234b90 Mon Sep 17 00:00:00 2001 From: collerek Date: Sun, 3 Jan 2021 17:54:09 +0100 Subject: [PATCH] fill part of queryset docstrings --- README.md | 1 + docs/index.md | 1 + ormar/models/mixins/save_mixin.py | 43 ++- ormar/models/modelproxy.py | 3 +- ormar/queryset/filter_query.py | 12 + ormar/queryset/limit_query.py | 12 + ormar/queryset/offset_query.py | 12 + ormar/queryset/order_query.py | 12 + ormar/queryset/queryset.py | 471 +++++++++++++++++++++++++++--- ormar/relations/querysetproxy.py | 4 +- 10 files changed, 524 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index ea70cfa..8804d8d 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ assert len(tracks) == 1 * `create(**kwargs): -> Model` * `get(**kwargs): -> Model` * `get_or_create(**kwargs) -> Model` +* `first(): -> Model` * `update(each: bool = False, **kwargs) -> int` * `update_or_create(**kwargs) -> Model` * `bulk_create(objects: List[Model]) -> None` diff --git a/docs/index.md b/docs/index.md index ea70cfa..8804d8d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -149,6 +149,7 @@ assert len(tracks) == 1 * `create(**kwargs): -> Model` * `get(**kwargs): -> Model` * `get_or_create(**kwargs) -> Model` +* `first(): -> Model` * `update(each: bool = False, **kwargs) -> int` * `update_or_create(**kwargs) -> Model` * `bulk_create(objects: List[Model]) -> None` diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index 2287b86..0450683 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -2,14 +2,55 @@ from typing import Dict import ormar from ormar.exceptions import ModelPersistenceError +from ormar.models.mixins import AliasMixin from ormar.models.mixins.relation_mixin import RelationMixin -class SavePrepareMixin(RelationMixin): +class SavePrepareMixin(RelationMixin, AliasMixin): """ Used to prepare models to be saved in database """ + @classmethod + 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, + 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). + + :param new_kwargs: dictionary of model that is about to be saved + :type new_kwargs: Dict[str, str] + :return: dictionary of model that is about to be saved + :rtype: Dict[str, str] + """ + new_kwargs = cls._remove_pk_from_kwargs(new_kwargs) + new_kwargs = cls.substitute_models_with_pks(new_kwargs) + new_kwargs = cls.populate_default_values(new_kwargs) + new_kwargs = cls.translate_columns_to_aliases(new_kwargs) + return new_kwargs + + @classmethod + def _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. + + :param new_kwargs: dictionary of model that is about to be saved + :type new_kwargs: Dict[str, str] + :return: dictionary of model that is about to be saved + :rtype: Dict[str, str] + """ + pkname = cls.Meta.pkname + pk = cls.Meta.model_fields[pkname] + if new_kwargs.get(pkname, ormar.Undefined) is None and ( + pk.nullable or pk.autoincrement + ): + del new_kwargs[pkname] + return new_kwargs + @classmethod def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001 """ diff --git a/ormar/models/modelproxy.py b/ormar/models/modelproxy.py index 2be3bde..fd1b8e6 100644 --- a/ormar/models/modelproxy.py +++ b/ormar/models/modelproxy.py @@ -1,5 +1,4 @@ from ormar.models.mixins import ( - AliasMixin, ExcludableMixin, MergeModelMixin, PrefetchQueryMixin, @@ -8,7 +7,7 @@ from ormar.models.mixins import ( class ModelTableProxy( - PrefetchQueryMixin, MergeModelMixin, AliasMixin, SavePrepareMixin, ExcludableMixin + PrefetchQueryMixin, MergeModelMixin, SavePrepareMixin, ExcludableMixin ): """ Used to combine all mixins with different set of functionalities. diff --git a/ormar/queryset/filter_query.py b/ormar/queryset/filter_query.py index f55d4e0..cb43170 100644 --- a/ormar/queryset/filter_query.py +++ b/ormar/queryset/filter_query.py @@ -4,11 +4,23 @@ import sqlalchemy class FilterQuery: + """ + Modifies the select query with given list of where/filter clauses. + """ + def __init__(self, filter_clauses: List, exclude: bool = False) -> None: self.exclude = exclude self.filter_clauses = filter_clauses def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + """ + Applies all filter clauses if set. + + :param expr: query to modify + :type expr: sqlalchemy.sql.selectable.Select + :return: modified query + :rtype: sqlalchemy.sql.selectable.Select + """ if self.filter_clauses: if len(self.filter_clauses) == 1: clause = self.filter_clauses[0] diff --git a/ormar/queryset/limit_query.py b/ormar/queryset/limit_query.py index af59326..a8fa921 100644 --- a/ormar/queryset/limit_query.py +++ b/ormar/queryset/limit_query.py @@ -4,10 +4,22 @@ import sqlalchemy class LimitQuery: + """ + Modifies the select query with limit clause. + """ + def __init__(self, limit_count: Optional[int]) -> None: self.limit_count = limit_count def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + """ + Applies the limit clause. + + :param expr: query to modify + :type expr: sqlalchemy.sql.selectable.Select + :return: modified query + :rtype: sqlalchemy.sql.selectable.Select + """ if self.limit_count: expr = expr.limit(self.limit_count) return expr diff --git a/ormar/queryset/offset_query.py b/ormar/queryset/offset_query.py index ce87296..2970f6e 100644 --- a/ormar/queryset/offset_query.py +++ b/ormar/queryset/offset_query.py @@ -4,10 +4,22 @@ import sqlalchemy class OffsetQuery: + """ + Modifies the select query with offset if set + """ + def __init__(self, query_offset: Optional[int]) -> None: self.query_offset = query_offset def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + """ + Applies the offset clause. + + :param expr: query to modify + :type expr: sqlalchemy.sql.selectable.Select + :return: modified query + :rtype: sqlalchemy.sql.selectable.Select + """ if self.query_offset: expr = expr.offset(self.query_offset) return expr diff --git a/ormar/queryset/order_query.py b/ormar/queryset/order_query.py index 5d4964f..4515749 100644 --- a/ormar/queryset/order_query.py +++ b/ormar/queryset/order_query.py @@ -4,10 +4,22 @@ import sqlalchemy class OrderQuery: + """ + Modifies the select query with given list of order_by clauses. + """ + def __init__(self, sorted_orders: Dict) -> None: self.sorted_orders = sorted_orders def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + """ + Applies all order_by clauses if set. + + :param expr: query to modify + :type expr: sqlalchemy.sql.selectable.Select + :return: modified query + :rtype: sqlalchemy.sql.selectable.Select + """ if self.sorted_orders: for order in list(self.sorted_orders.values()): if order is not None: diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index 68448a4..ffa31cc 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -20,6 +20,10 @@ if TYPE_CHECKING: # pragma no cover class QuerySet: + """ + Main class to perform database queries, exposed on each model as objects attribute. + """ + def __init__( # noqa CFQ002 self, model_cls: Type["Model"] = None, @@ -57,12 +61,24 @@ class QuerySet: @property def model_meta(self) -> "ModelMeta": + """ + Shortcut to model class Meta set on QuerySet model. + + :return: Meta class of the model + :rtype: model Meta class + """ if not self.model_cls: # pragma nocover raise ValueError("Model class of QuerySet is not initialized") return self.model_cls.Meta @property def model(self) -> Type["Model"]: + """ + Shortcut to model class set on QuerySet. + + :return: model class + :rtype: Type[Model] + """ if not self.model_cls: # pragma nocover raise ValueError("Model class of QuerySet is not initialized") return self.model_cls @@ -70,6 +86,16 @@ class QuerySet: async def _prefetch_related_models( self, models: Sequence[Optional["Model"]], rows: List ) -> Sequence[Optional["Model"]]: + """ + Performs prefetch query for selected models names. + + :param models: list of already parsed main Models from main query + :type models: List[Model] + :param rows: database rows from main query + :type rows: List[sqlalchemy.engine.result.RowProxy] + :return: list of models with prefetch models populated + :rtype: List[Model] + """ query = PrefetchQuery( model_cls=self.model, fields=self._columns, @@ -81,6 +107,14 @@ class QuerySet: return await query.prefetch_related(models=models, rows=rows) # type: ignore def _process_query_result_rows(self, rows: List) -> Sequence[Optional["Model"]]: + """ + Process database rows and initialize ormar Model from each of the rows. + + :param rows: list of database rows from query result + :type rows: List[sqlalchemy.engine.result.RowProxy] + :return: list of models + :rtype: List[Model] + """ result_rows = [ self.model.from_row( row=row, @@ -94,24 +128,14 @@ class QuerySet: return self.model.merge_instances_list(result_rows) # type: ignore return result_rows - def _prepare_model_to_save(self, new_kwargs: dict) -> dict: - new_kwargs = self._remove_pk_from_kwargs(new_kwargs) - new_kwargs = self.model.substitute_models_with_pks(new_kwargs) - new_kwargs = self.model.populate_default_values(new_kwargs) - new_kwargs = self.model.translate_columns_to_aliases(new_kwargs) - return new_kwargs - - def _remove_pk_from_kwargs(self, new_kwargs: dict) -> dict: - pkname = self.model_meta.pkname - pk = self.model_meta.model_fields[pkname] - if new_kwargs.get(pkname, ormar.Undefined) is None and ( - pk.nullable or pk.autoincrement - ): - del new_kwargs[pkname] - return new_kwargs - @staticmethod def check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None: + """ + Verifies if the result has one and only one row. + + :param rows: one element list of Models + :type rows: List[Model] + """ if not rows or rows[0] is None: raise NoMatch() if len(rows) > 1: @@ -119,15 +143,40 @@ class QuerySet: @property def database(self) -> databases.Database: + """ + Shortcut to models database from Meta class. + + :return: database + :rtype: databases.Database + """ return self.model_meta.database @property def table(self) -> sqlalchemy.Table: + """ + Shortcut to models table from Meta class. + + :return: database table + :rtype: sqlalchemy.Table + """ return self.model_meta.table def build_select_expression( self, 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. + + :param limit: number to limit the query + :type limit: int + :param offset: number to offset by + :type offset: int + :param order_bys: list of order-by fields names + :type order_bys: List + :return: built sqlalchemy select expression + :rtype: sqlalchemy.sql.selectable.Select + """ qry = Query( model_cls=self.model, select_related=self._select_related, @@ -145,6 +194,33 @@ class QuerySet: return exp def filter(self, _exclude: bool = False, **kwargs: Any) -> "QuerySet": # noqa: A003 + """ + 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) + + :param _exclude: flag if it should be exclude or filter + :type _exclude: bool + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: filtered QuerySet + :rtype: QuerySet + """ qryclause = QueryClause( model_cls=self.model, select_related=self._select_related, @@ -173,9 +249,43 @@ class QuerySet: ) def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003 + """ + 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)` + + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: filtered QuerySet + :rtype: QuerySet + """ return self.filter(_exclude=True, **kwargs) def select_related(self, 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. + + :param related: list of relation field names, can be linked by '__' to nest + :type related: Union[List, str] + :return: QuerySet + :rtype: QuerySet + """ if not isinstance(related, list): related = [related] @@ -195,6 +305,23 @@ class QuerySet: ) def prefetch_related(self, 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. + + :param related: list of relation field names, can be linked by '__' to nest + :type related: Union[List, str] + :return: QuerySet + :rtype: QuerySet + """ if not isinstance(related, list): related = [related] @@ -213,31 +340,49 @@ class QuerySet: limit_raw_sql=self.limit_sql_raw, ) - def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet": - if isinstance(columns, str): - columns = [columns] - - current_excluded = self._exclude_columns - if not isinstance(columns, dict): - current_excluded = update_dict_from_list(current_excluded, columns) - else: - current_excluded = update(current_excluded, columns) - - return self.__class__( - model_cls=self.model, - filter_clauses=self.filter_clauses, - exclude_clauses=self.exclude_clauses, - select_related=self._select_related, - limit_count=self.limit_count, - offset=self.query_offset, - columns=self._columns, - exclude_columns=current_excluded, - order_bys=self.order_bys, - prefetch_related=self._prefetch_related, - limit_raw_sql=self.limit_sql_raw, - ) - def fields(self, 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. + + :param columns: columns to include + :type columns: Union[List, str, Set, Dict] + :return: QuerySet + :rtype: QuerySet + """ if isinstance(columns, str): columns = [columns] @@ -261,7 +406,88 @@ class QuerySet: limit_raw_sql=self.limit_sql_raw, ) + def exclude_fields(self, 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. + + :param columns: columns to exclude + :type columns: Union[List, str, Set, Dict] + :return: QuerySet + :rtype: QuerySet + """ + if isinstance(columns, str): + columns = [columns] + + current_excluded = self._exclude_columns + if not isinstance(columns, dict): + current_excluded = update_dict_from_list(current_excluded, columns) + else: + current_excluded = update(current_excluded, columns) + + return self.__class__( + model_cls=self.model, + filter_clauses=self.filter_clauses, + exclude_clauses=self.exclude_clauses, + select_related=self._select_related, + limit_count=self.limit_count, + offset=self.query_offset, + columns=self._columns, + exclude_columns=current_excluded, + order_bys=self.order_bys, + prefetch_related=self._prefetch_related, + limit_raw_sql=self.limit_sql_raw, + ) + def order_by(self, 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 + + :param columns: columns by which models should be sorted + :type columns: Union[List, str] + :return: QuerySet + :rtype: QuerySet + """ if not isinstance(columns, list): columns = [columns] @@ -281,16 +507,43 @@ class QuerySet: ) async def exists(self) -> bool: + """ + Returns a bool value to confirm if there are rows matching the given criteria + (applied with `filter` and `exclude` if set). + + :return: result of the check + :rtype: bool + """ expr = self.build_select_expression() expr = sqlalchemy.exists(expr).select() return await self.database.fetch_val(expr) async def count(self) -> int: + """ + Returns number of rows matching the given criteria + (applied with `filter` and `exclude` if set before). + + :return: number of rows + :rtype: int + """ expr = self.build_select_expression().alias("subquery_for_count") expr = sqlalchemy.func.count().select().select_from(expr) return await self.database.fetch_val(expr) async def update(self, 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. + + :param each: flag if whole table should be affected if no filter is passed + :type each: bool + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: number of updated rows + :rtype: int + """ self_fields = self.model.extract_db_own_fields().union( self.model.extract_related_names() ) @@ -307,6 +560,19 @@ class QuerySet: return await self.database.execute(expr) async def delete(self, 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. + + :param each: flag if whole table should be affected if no filter is passed + :type each: bool + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: number of deleted rows + :rtype:int + """ if kwargs: return await self.filter(**kwargs).delete() if not each and not self.filter_clauses: @@ -320,6 +586,19 @@ class QuerySet: return await self.database.execute(expr) def limit(self, 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`. + + :param limit_raw_sql: flag if raw sql should be limited + :type limit_raw_sql: bool + :param limit_count: number of models to limit + :type limit_count: int + :return: QuerySet + :rtype: QuerySet + """ limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql return self.__class__( model_cls=self.model, @@ -336,6 +615,19 @@ class QuerySet: ) def offset(self, 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`. + + :param limit_raw_sql: flag if raw sql should be offset + :type limit_raw_sql: bool + :param offset: numbers of models to offset + :type offset: int + :return: QuerySet + :rtype: QuerySet + """ limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql return self.__class__( model_cls=self.model, @@ -352,6 +644,16 @@ class QuerySet: ) async def first(self, **kwargs: Any) -> "Model": + """ + 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. + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: returned model + :rtype: Model + """ if kwargs: return await self.filter(**kwargs).first() @@ -366,6 +668,20 @@ class QuerySet: return processed_rows[0] # type: ignore async def get(self, **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 + :raises: MultipleMatches if more than 1 row is returned. + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: returned model + :rtype: Model + """ if kwargs: return await self.filter(**kwargs).get() @@ -384,12 +700,32 @@ class QuerySet: return processed_rows[0] # type: ignore async def get_or_create(self, **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. + + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: returned or created Model + :rtype: Model + """ try: return await self.get(**kwargs) except NoMatch: return await self.create(**kwargs) async def update_or_create(self, **kwargs: Any) -> "Model": + """ + Updates the model, or in case there is no match in database creates a new one. + + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: updated or created model + :rtype: Model + """ pk_name = self.model_meta.pkname if "pk" in kwargs: kwargs[pk_name] = kwargs.pop("pk") @@ -399,6 +735,18 @@ class QuerySet: return await model.update(**kwargs) async def all(self, **kwargs: Any) -> Sequence[Optional["Model"]]: # noqa: A003 + """ + 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. + + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: list of returned models + :rtype: List[Model] + """ if kwargs: return await self.filter(**kwargs).all() @@ -411,9 +759,19 @@ class QuerySet: return result_rows async def create(self, **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. + + :param kwargs: fields names and proper value types + :type kwargs: Any + :return: created model + :rtype: Model + """ new_kwargs = dict(**kwargs) - new_kwargs = self._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) @@ -444,10 +802,22 @@ class QuerySet: return instance async def bulk_create(self, 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. + + :param objects: list of ormar models already initialized and ready to save. + :type objects: List[Model] + """ ready_objects = [] for objt in objects: new_kwargs = objt.dict() - new_kwargs = self._prepare_model_to_save(new_kwargs) + new_kwargs = objt._prepare_model_to_save(new_kwargs) ready_objects.append(new_kwargs) expr = self.table.insert() @@ -459,6 +829,23 @@ class QuerySet: async def bulk_update( # noqa: CCR001 self, 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. + + :param objects: list of ormar models + :type objects: List[Model] + :param columns: list of columns to update + :type columns: List[str] + """ ready_objects = [] pk_name = self.model_meta.pkname if not columns: diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index 5539872..1356a3a 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -376,7 +376,7 @@ class QuerysetProxy(ormar.QuerySetProtocol): Actual call delegated to QuerySet. :param related: list of relation field names, can be linked by '__' to nest - :type related: str + :type related: Union[List, str] :return: QuerysetProxy :rtype: QuerysetProxy """ @@ -399,7 +399,7 @@ class QuerysetProxy(ormar.QuerySetProtocol): Actual call delegated to QuerySet. :param related: list of relation field names, can be linked by '__' to nest - :type related: str + :type related: Union[List, str] :return: QuerysetProxy :rtype: QuerysetProxy """