diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c57598f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: https://github.com/psf/black + rev: 21.9b0 + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + args: [ '--max-line-length=88' ] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.910 + hooks: + - id: mypy + args: [--no-strict-optional, --ignore-missing-imports] + additional_dependencies: [ + types-ujson>=0.1.1, + types-PyMySQL>=1.0.2, + types-ipaddress>=1.0.0, + types-enum34>=1.1.0, + types-cryptography>=3.3.5, + types-orjson>=3.6.0, + types-aiofiles>=0.1.9, + types-pkg-resources>=0.1.3, + types-requests>=2.25.9, + types-toml>=0.10.0, + pydantic>=1.8.2 + ] diff --git a/docs/models/methods.md b/docs/models/methods.md index 0761a29..7cbecea 100644 --- a/docs/models/methods.md +++ b/docs/models/methods.md @@ -17,6 +17,25 @@ especially `dict()` and `json()` methods that can also accept `exclude`, `includ To read more check [pydantic][pydantic] documentation +## construct + +`construct` is a raw equivalent of `__init__` method used for construction of new instances. + +The difference is that `construct` skips validations, so it should be used when you know that data is correct and can be trusted. +The benefit of using construct is the speed of execution due to skipped validation. + +!!!note + Note that in contrast to `pydantic.construct` method - the `ormar` equivalent will also process the nested related models. + +!!!warning + Bear in mind that due to skipped validation the `construct` method does not perform any conversions, checks etc. + So it's your responsibility to provide tha data that is valid and can be consumed by the database. + + The only two things that construct still performs are: + + * Providing a `default` value for not set fields + * Initialize nested ormar models if you pass a dictionary or a primary key value + ## dict `dict` is a method inherited from `pydantic`, yet `ormar` adds its own parameters and has some nuances when working with default values, @@ -363,10 +382,16 @@ class Category(BaseModel): items: Optional[List[Item]] ``` -Of course you can use also deeply nested structures and ormar will generate it pydantic equivalent you (in a way that exclude loops). +Of course, you can use also deeply nested structures and ormar will generate it pydantic equivalent you (in a way that exclude loops). Note how `Item` model above does not have a reference to `Category` although in ormar the relation is bidirectional (and `ormar.Item` has `categories` field). +!!!warning + Note that the generated pydantic model will inherit all **field** validators from the original `ormar` model, that includes the ormar choices validator as well as validators defined with `pydantic.validator` decorator. + + But, at the same time all root validators present on `ormar` models will **NOT** be copied to the generated pydantic model. Since root validator can operate on all fields and a user can exclude some fields during generation of pydantic model it's not safe to copy those validators. + If required, you need to redefine/ manually copy them to generated pydantic model. + ## load By default when you query a table without prefetching related models, the ormar will still construct diff --git a/docs/releases.md b/docs/releases.md index babf6e6..e2b11bf 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,3 +1,10 @@ +# 0.10.21 + +## 🐛 Fixes + +* Add `ormar` implementation of `construct` classmethod that allows to build `Model` instances without validating the input to speed up the whole flow, if your data is already validated [#318](https://github.com/collerek/ormar/issues/318) +* Fix for "inheriting" field validators from `ormar` model when newly created pydanic model is generated with `get_pydantic` [#365](https://github.com/collerek/ormar/issues/365) + # 0.10.20 ## ✨ Features diff --git a/ormar/__init__.py b/ormar/__init__.py index f444fb6..21ab87e 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -61,6 +61,7 @@ from ormar.fields import ( LargeBinary, ManyToMany, ManyToManyField, + SQL_ENCODERS_MAP, SmallInteger, String, Text, @@ -132,6 +133,7 @@ __all__ = [ "or_", "EncryptBackends", "ENCODERS_MAP", + "SQL_ENCODERS_MAP", "DECODERS_MAP", "LargeBinary", "Extra", diff --git a/ormar/fields/__init__.py b/ormar/fields/__init__.py index 9ea9387..e90b5df 100644 --- a/ormar/fields/__init__.py +++ b/ormar/fields/__init__.py @@ -24,7 +24,7 @@ from ormar.fields.model_fields import ( Time, UUID, ) -from ormar.fields.parsers import DECODERS_MAP, ENCODERS_MAP +from ormar.fields.parsers import DECODERS_MAP, ENCODERS_MAP, SQL_ENCODERS_MAP from ormar.fields.sqlalchemy_encrypted import EncryptBackend, EncryptBackends from ormar.fields.through_field import Through, ThroughField @@ -54,6 +54,7 @@ __all__ = [ "EncryptBackend", "DECODERS_MAP", "ENCODERS_MAP", + "SQL_ENCODERS_MAP", "LargeBinary", "UniqueColumns", ] diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index 6003620..a819b61 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -1,11 +1,13 @@ import datetime import decimal import uuid -from typing import Any, Optional, TYPE_CHECKING, Union, overload +from enum import Enum +from typing import Any, Optional, Set, TYPE_CHECKING, Type, Union, overload import pydantic import sqlalchemy +import ormar # noqa I101 from ormar import ModelDefinitionError # noqa I101 from ormar.fields import sqlalchemy_uuid from ormar.fields.base import BaseField # noqa I101 @@ -60,6 +62,49 @@ def is_auto_primary_key(primary_key: bool, autoincrement: bool) -> bool: return primary_key and autoincrement +def convert_choices_if_needed( + field_type: "Type", + choices: Set, + nullable: bool, + scale: int = None, + represent_as_str: bool = False, +) -> Set: + """ + Converts dates to isoformat as fastapi can check this condition in routes + and the fields are not yet parsed. + Converts enums to list of it's values. + Converts uuids to strings. + Converts decimal to float with given scale. + + :param field_type: type o the field + :type field_type: Type + :param choices: set of choices + :type choices: Set + :param scale: scale for decimals + :type scale: int + :param nullable: flag if field_nullable + :type nullable: bool + :param represent_as_str: flag for bytes fields + :type represent_as_str: bool + :param scale: scale for decimals + :type scale: int + :return: value, choices list + :rtype: Tuple[Any, Set] + """ + choices = {o.value if isinstance(o, Enum) else o for o in choices} + encoder = ormar.ENCODERS_MAP.get(field_type, lambda x: x) + if field_type == decimal.Decimal: + precision = scale + choices = {encoder(o, precision) for o in choices} + elif field_type == bytes: + choices = {encoder(o, represent_as_str) for o in choices} + elif encoder: + choices = {encoder(o) for o in choices} + if nullable: + choices.add(None) + return choices + + class ModelFieldFactory: """ Default field factory that construct Field classes and populated their values. @@ -96,6 +141,16 @@ class ModelFieldFactory: else (nullable if sql_nullable is None else sql_nullable) ) + choices = set(kwargs.pop("choices", [])) + if choices: + choices = convert_choices_if_needed( + field_type=cls._type, + choices=choices, + nullable=nullable, + scale=kwargs.get("scale", None), + represent_as_str=kwargs.get("represent_as_base64_str", False), + ) + namespace = dict( __type__=cls._type, __pydantic_type__=overwrite_pydantic_type @@ -114,7 +169,7 @@ class ModelFieldFactory: pydantic_only=pydantic_only, autoincrement=autoincrement, column_type=cls.get_column_type(**kwargs), - choices=set(kwargs.pop("choices", [])), + choices=choices, encrypt_secret=encrypt_secret, encrypt_backend=encrypt_backend, encrypt_custom_backend=encrypt_custom_backend, diff --git a/ormar/fields/parsers.py b/ormar/fields/parsers.py index e0f1a53..e8b7301 100644 --- a/ormar/fields/parsers.py +++ b/ormar/fields/parsers.py @@ -1,6 +1,8 @@ +import base64 import datetime import decimal -from typing import Any +import uuid +from typing import Any, Callable, Dict, Union import pydantic from pydantic.datetime_parse import parse_date, parse_datetime, parse_time @@ -19,21 +21,55 @@ def encode_bool(value: bool) -> str: return "true" if value else "false" +def encode_decimal(value: decimal.Decimal, precision: int = None) -> float: + if precision: + return ( + round(float(value), precision) + if isinstance(value, decimal.Decimal) + else value + ) + return float(value) + + +def encode_bytes(value: Union[str, bytes], represent_as_string: bool = False) -> bytes: + if represent_as_string: + return value if isinstance(value, bytes) else base64.b64decode(value) + return value if isinstance(value, bytes) else value.encode("utf-8") + + def encode_json(value: Any) -> str: - value = json.dumps(value) if not isinstance(value, str) else value + value = json.dumps(value) if not isinstance(value, str) else re_dump_value(value) value = value.decode("utf-8") if isinstance(value, bytes) else value return value -ENCODERS_MAP = { - bool: encode_bool, +def re_dump_value(value: str) -> Union[str, bytes]: + """ + Rw-dumps choices due to different string representation in orjson and json + :param value: string to re-dump + :type value: str + :return: re-dumped choices + :rtype: List[str] + """ + try: + result: Union[str, bytes] = json.dumps(json.loads(value)) + except json.JSONDecodeError: + result = value + return result + + +ENCODERS_MAP: Dict[type, Callable] = { datetime.datetime: lambda x: x.isoformat(), datetime.date: lambda x: x.isoformat(), datetime.time: lambda x: x.isoformat(), pydantic.Json: encode_json, - decimal.Decimal: float, + decimal.Decimal: encode_decimal, + uuid.UUID: str, + bytes: encode_bytes, } +SQL_ENCODERS_MAP: Dict[type, Callable] = {bool: encode_bool, **ENCODERS_MAP} + DECODERS_MAP = { bool: parse_bool, datetime.datetime: parse_datetime, diff --git a/ormar/fields/sqlalchemy_encrypted.py b/ormar/fields/sqlalchemy_encrypted.py index 97769ba..88dc0af 100644 --- a/ormar/fields/sqlalchemy_encrypted.py +++ b/ormar/fields/sqlalchemy_encrypted.py @@ -160,7 +160,7 @@ class EncryptedString(types.TypeDecorator): try: value = self._underlying_type.process_bind_param(value, dialect) except AttributeError: - encoder = ormar.ENCODERS_MAP.get(self.type_, None) + encoder = ormar.SQL_ENCODERS_MAP.get(self.type_, None) if encoder: value = encoder(value) # type: ignore diff --git a/ormar/models/helpers/pydantic.py b/ormar/models/helpers/pydantic.py index 1423176..03ab736 100644 --- a/ormar/models/helpers/pydantic.py +++ b/ormar/models/helpers/pydantic.py @@ -50,9 +50,10 @@ def get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField": :return: newly created pydantic field :rtype: pydantic.ModelField """ + type_ = model.Meta.model_fields[field_name].__type__ return ModelField( name=field_name, - type_=model.Meta.model_fields[field_name].__type__, # type: ignore + type_=type_, # type: ignore model_config=model.__config__, required=not model.Meta.model_fields[field_name].nullable, class_validators={}, diff --git a/ormar/models/helpers/relations.py b/ormar/models/helpers/relations.py index 68bf893..c6d1ba4 100644 --- a/ormar/models/helpers/relations.py +++ b/ormar/models/helpers/relations.py @@ -101,6 +101,7 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None: :type model_field: relation Field """ related_name = model_field.get_related_name() + # TODO: Reverse relations does not register pydantic fields? if model_field.is_multi: model_field.to.Meta.model_fields[related_name] = ManyToMany( # type: ignore model_field.owner, diff --git a/ormar/models/helpers/validation.py b/ormar/models/helpers/validation.py index 11db796..b3e8e15 100644 --- a/ormar/models/helpers/validation.py +++ b/ormar/models/helpers/validation.py @@ -1,30 +1,37 @@ import base64 -import datetime import decimal import numbers -import uuid -from enum import Enum -from typing import Any, Dict, List, Set, TYPE_CHECKING, Tuple, Type, Union +from typing import ( + Any, + Callable, + Dict, + List, + Set, + TYPE_CHECKING, + Type, + Union, +) try: import orjson as json except ImportError: # pragma: no cover - import json # type: ignore + import json # type: ignore # noqa: F401 import pydantic -from pydantic.fields import SHAPE_LIST +from pydantic.class_validators import make_generic_validator +from pydantic.fields import ModelField, SHAPE_LIST from pydantic.main import SchemaExtraCallable import ormar # noqa: I100, I202 -from ormar.fields import BaseField from ormar.models.helpers.models import meta_field_not_set from ormar.queryset.utils import translate_list_to_dict if TYPE_CHECKING: # pragma no cover from ormar import Model + from ormar.fields import BaseField -def check_if_field_has_choices(field: BaseField) -> bool: +def check_if_field_has_choices(field: "BaseField") -> bool: """ Checks if given field has choices populated. A if it has one, a validator for this field needs to be attached. @@ -37,110 +44,56 @@ def check_if_field_has_choices(field: BaseField) -> bool: return hasattr(field, "choices") and bool(field.choices) -def convert_choices_if_needed( # noqa: CCR001 - field: "BaseField", value: Any -) -> Tuple[Any, List]: +def convert_value_if_needed(field: "BaseField", value: Any) -> Any: """ Converts dates to isoformat as fastapi can check this condition in routes and the fields are not yet parsed. - Converts enums to list of it's values. - Converts uuids to strings. - Converts decimal to float with given scale. :param field: ormar field to check with choices :type field: BaseField :param value: current values of the model to verify - :type value: Dict - :return: value, choices list - :rtype: Tuple[Any, List] - """ - # TODO use same maps as with EncryptedString - choices = [o.value if isinstance(o, Enum) else o for o in field.choices] - - if field.__type__ in [datetime.datetime, datetime.date, datetime.time]: - value = value.isoformat() if not isinstance(value, str) else value - choices = [o.isoformat() for o in field.choices] - elif field.__type__ == pydantic.Json: - value = ( - json.dumps(value) if not isinstance(value, str) else re_dump_value(value) - ) - value = value.decode("utf-8") if isinstance(value, bytes) else value - choices = [re_dump_value(x) for x in field.choices] - elif field.__type__ == uuid.UUID: - value = str(value) if not isinstance(value, str) else value - choices = [str(o) for o in field.choices] - elif field.__type__ == decimal.Decimal: - precision = field.scale # type: ignore - value = ( - round(float(value), precision) - if isinstance(value, decimal.Decimal) - else value - ) - choices = [round(float(o), precision) for o in choices] - elif field.__type__ == bytes: - if field.represent_as_base64_str: - value = value if isinstance(value, bytes) else base64.b64decode(value) - else: - value = value if isinstance(value, bytes) else value.encode("utf-8") - - return value, choices - - -def re_dump_value(value: str) -> str: - """ - Rw-dumps choices due to different string representation in orjson and json - :param value: string to re-dump - :type value: str - :return: re-dumped choices - :rtype: List[str] - """ - try: - result: Union[str, bytes] = json.dumps(json.loads(value)) - except json.JSONDecodeError: - result = value - return result.decode("utf-8") if isinstance(result, bytes) else result - - -def validate_choices(field: "BaseField", value: Any) -> None: - """ - Validates if given value is in provided choices. - - :raises ValueError: If value is not in choices. - :param field:field to validate - :type field: BaseField - :param value: value of the field :type value: Any + :return: value, choices list + :rtype: Any """ - value, choices = convert_choices_if_needed(field=field, value=value) - if field.nullable: - choices.append(None) - if value is not ormar.Undefined and value not in choices: - raise ValueError( - f"{field.name}: '{value}' " f"not in allowed choices set:" f" {choices}" - ) + encoder = ormar.ENCODERS_MAP.get(field.__type__, lambda x: x) + if field.__type__ == decimal.Decimal: + precision = field.scale # type: ignore + value = encoder(value, precision) + elif field.__type__ == bytes: + represent_as_string = field.represent_as_base64_str + value = encoder(value, represent_as_string) + elif encoder: + value = encoder(value) + return value -def 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. +def generate_validator(ormar_field: "BaseField") -> Callable: + choices = ormar_field.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) - :type values: Dict[str, Any] - :return: values if pass validation, otherwise exception is raised - :rtype: Dict[str, Any] - """ - for field_name, field in cls.Meta.model_fields.items(): - if check_if_field_has_choices(field): - value = values.get(field_name, ormar.Undefined) - validate_choices(field=field, value=value) - return values + def validate_choices(cls: type, value: Any, field: "ModelField") -> None: + """ + Validates if given value is in provided choices. + + :raises ValueError: If value is not in choices. + :param field:field to validate + :type field: BaseField + :param value: value of the field + :type value: Any + """ + adjusted_value = convert_value_if_needed(field=ormar_field, value=value) + if adjusted_value is not ormar.Undefined and adjusted_value not in choices: + raise ValueError( + f"{field.name}: '{adjusted_value}' " + f"not in allowed choices set:" + f" {choices}" + ) + return value + + return validate_choices def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> Dict: @@ -172,7 +125,7 @@ def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> D def populates_sample_fields_values( - example: Dict[str, Any], name: str, field: BaseField, relation_map: Dict = None + example: Dict[str, Any], name: str, field: "BaseField", relation_map: Dict = None ) -> None: """ Iterates the field and sets fields to sample values @@ -350,15 +303,14 @@ def populate_choices_validators(model: Type["Model"]) -> None: # noqa CCR001 """ fields_with_choices = [] if not meta_field_not_set(model=model, field_name="model_fields"): + if hasattr(model, "_choices_fields"): + return + model._choices_fields = set() for name, field in model.Meta.model_fields.items(): if check_if_field_has_choices(field): fields_with_choices.append(name) - validators = getattr(model, "__pre_root_validators__", []) - if choices_validator not in validators: - validators.append(choices_validator) - model.__pre_root_validators__ = validators - if not model._choices_fields: - model._choices_fields = set() + validator = make_generic_validator(generate_validator(field)) + model.__fields__[name].validators.append(validator) model._choices_fields.add(name) if fields_with_choices: diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 49043cc..2bee725 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -106,7 +106,6 @@ def add_cached_properties(new_model: Type["Model"]) -> None: new_model._through_names = None new_model._related_fields = None new_model._pydantic_fields = {name for name in new_model.__fields__} - new_model._choices_fields = set() new_model._json_fields = set() new_model._bytes_fields = set() diff --git a/ormar/models/mixins/pydantic_mixin.py b/ormar/models/mixins/pydantic_mixin.py index ce63f05..1ecdd89 100644 --- a/ormar/models/mixins/pydantic_mixin.py +++ b/ormar/models/mixins/pydantic_mixin.py @@ -1,3 +1,4 @@ +import copy import string from random import choices from typing import ( @@ -82,7 +83,9 @@ class PydanticMixin(RelationMixin): (pydantic.BaseModel,), {"__annotations__": fields_dict, **defaults}, ) - return cast(Type[pydantic.BaseModel], model) + model = cast(Type[pydantic.BaseModel], model) + cls._copy_field_validators(model=model) + return model @classmethod def _determine_pydantic_field_type( @@ -111,3 +114,33 @@ class PydanticMixin(RelationMixin): if target is not None and field.nullable: target = Optional[target] return target + + @classmethod + def _copy_field_validators(cls, model: Type[pydantic.BaseModel]) -> None: + """ + Copy field validators from ormar model to generated pydantic model. + """ + for field_name, field in model.__fields__.items(): + if ( + field_name not in cls.__fields__ + or cls.Meta.model_fields[field_name].is_relation + ): + continue + validators = cls.__fields__[field_name].validators + already_attached = [ + validator.__wrapped__ for validator in field.validators # type: ignore + ] + validators_to_copy = [ + validator + for validator in validators + if validator.__wrapped__ not in already_attached # type: ignore + ] + field.validators.extend(copy.deepcopy(validators_to_copy)) + class_validators = cls.__fields__[field_name].class_validators + field.class_validators.update(copy.deepcopy(class_validators)) + field.pre_validators = copy.deepcopy( + cls.__fields__[field_name].pre_validators + ) + field.post_validators = copy.deepcopy( + cls.__fields__[field_name].post_validators + ) diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index d1769f9..de3923e 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -11,9 +11,10 @@ from typing import ( cast, ) -import ormar +import pydantic + +import ormar # noqa: I100, I202 from ormar.exceptions import ModelPersistenceError -from ormar.models.helpers.validation import validate_choices from ormar.models.mixins import AliasMixin from ormar.models.mixins.relation_mixin import RelationMixin @@ -29,6 +30,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): if TYPE_CHECKING: # pragma: nocover _choices_fields: Optional[Set] _skip_ellipsis: Callable + __fields__: Dict[str, pydantic.fields.ModelField] @classmethod def prepare_model_to_save(cls, new_kwargs: dict) -> dict: @@ -180,9 +182,18 @@ class SavePrepareMixin(RelationMixin, AliasMixin): if not cls._choices_fields: return new_kwargs - for field_name, field in cls.Meta.model_fields.items(): - if field_name in new_kwargs and field_name in cls._choices_fields: - validate_choices(field=field, value=new_kwargs.get(field_name)) + fields_to_check = [ + field + for field in cls.Meta.model_fields.values() + if field.name in cls._choices_fields and field.name in new_kwargs + ] + for field in fields_to_check: + if new_kwargs[field.name] not in field.choices: + raise ValueError( + f"{field.name}: '{new_kwargs[field.name]}' " + f"not in allowed choices set:" + f" {field.choices}" + ) return new_kwargs @staticmethod diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index b18080d..1fbf109 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -14,6 +14,7 @@ from typing import ( TYPE_CHECKING, Tuple, Type, + TypeVar, Union, cast, ) @@ -50,8 +51,11 @@ if TYPE_CHECKING: # pragma no cover from ormar.models import Model from ormar.signals import SignalEmitter + T = TypeVar("T", bound="NewBaseModel") + IntStr = Union[int, str] DictStrAny = Dict[str, Any] + SetStr = Set[str] AbstractSetIntStr = AbstractSet[IntStr] MappingIntStrAny = Mapping[IntStr, Any] @@ -86,7 +90,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass _related_names: Optional[Set] _through_names: Optional[Set] _related_names_hash: str - _choices_fields: Optional[Set] + _choices_fields: Set _pydantic_fields: Set _quick_access_fields: Set _json_fields: Set @@ -785,6 +789,51 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass data = data["__root__"] return self.__config__.json_dumps(data, default=encoder, **dumps_kwargs) + @classmethod + def construct( + cls: Type["T"], _fields_set: Optional["SetStr"] = None, **values: Any + ) -> "T": + own_values = { + k: v for k, v in values.items() if k not in cls.extract_related_names() + } + model = cls.__new__(cls) + fields_values: Dict[str, Any] = {} + for name, field in cls.__fields__.items(): + if name in own_values: + fields_values[name] = own_values[name] + elif not field.required: + fields_values[name] = field.get_default() + fields_values.update(own_values) + object.__setattr__(model, "__dict__", fields_values) + model._initialize_internal_attributes() + cls._construct_relations(model=model, values=values) + if _fields_set is None: + _fields_set = set(values.keys()) + object.__setattr__(model, "__fields_set__", _fields_set) + return model + + @classmethod + def _construct_relations(cls: Type["T"], model: "T", values: Dict) -> None: + present_relations = [ + relation for relation in cls.extract_related_names() if relation in values + ] + for relation in present_relations: + value_to_set = values[relation] + if not isinstance(value_to_set, list): + value_to_set = [value_to_set] + relation_field = cls.Meta.model_fields[relation] + relation_value = [ + relation_field.expand_relationship(x, model, to_register=False) + for x in value_to_set + ] + + for child in relation_value: + model._orm.add( + parent=cast("Model", child), + child=cast("Model", model), + field=cast("ForeignKeyField", relation_field), + ) + def update_from_dict(self, value_dict: Dict) -> "NewBaseModel": """ Updates self with values of fields passed in the dictionary. @@ -879,6 +928,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :return: dictionary of fields names and values. :rtype: Dict """ + # TODO: Cache this dictionary? self_fields = self._extract_own_model_fields() self_fields = { k: v diff --git a/ormar/queryset/utils.py b/ormar/queryset/utils.py index f99c698..4ebe07c 100644 --- a/ormar/queryset/utils.py +++ b/ormar/queryset/utils.py @@ -29,8 +29,8 @@ def check_node_not_dict_or_not_last_node( :param part: :type part: str - :param parts: - :type parts: List[str] + :param is_last: flag to check if last element + :type is_last: bool :param current_level: current level of the traversed structure :type current_level: Any :return: result of the check @@ -52,7 +52,7 @@ def translate_list_to_dict( # noqa: CCR001 Default required key ise Ellipsis like in pydantic. :param list_to_trans: input list - :type list_to_trans: set + :type list_to_trans: Union[List, Set] :param is_order: flag if change affects order_by clauses are they require special default value with sort order. :type is_order: bool diff --git a/poetry.lock b/poetry.lock index f81abd0..6c7fe55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -51,7 +51,7 @@ typing_extensions = ">=3.7.2" [[package]] name = "anyio" -version = "3.3.2" +version = "3.3.3" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "dev" optional = false @@ -125,6 +125,21 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +[[package]] +name = "backports.entry-points-selectable" +version = "1.1.0" +description = "Compatibility shim providing selectable entry points for older implementations" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] + [[package]] name = "bandit" version = "1.7.0" @@ -188,9 +203,17 @@ python-versions = "*" [package.dependencies] pycparser = "*" +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + [[package]] name = "charset-normalizer" -version = "2.0.6" +version = "2.0.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false @@ -201,7 +224,7 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.2" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -260,7 +283,7 @@ immutables = ">=0.9" [[package]] name = "coverage" -version = "6.0.1" +version = "6.0.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -364,6 +387,14 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] +[[package]] +name = "distlib" +version = "0.3.3" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "docspec" version = "1.2.0" @@ -412,6 +443,18 @@ dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,< doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] +[[package]] +name = "filelock" +version = "3.3.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + [[package]] name = "flake8" version = "3.9.2" @@ -595,9 +638,20 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] docs = ["sphinx"] +[[package]] +name = "identify" +version = "2.3.0" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.extras] +license = ["editdistance-s"] + [[package]] name = "idna" -version = "3.2" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false @@ -634,6 +688,21 @@ docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "importlib-resources" +version = "5.2.2" +description = "Read resources from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] + [[package]] name = "iniconfig" version = "1.1.1" @@ -786,6 +855,14 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "nr.fs" version = "1.6.3" @@ -920,6 +997,24 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "2.15.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-resources = {version = "*", markers = "python_version < \"3.7\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + [[package]] name = "psycopg2-binary" version = "2.9.1" @@ -1380,6 +1475,27 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "virtualenv" +version = "20.8.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +"backports.entry-points-selectable" = ">=1.0.4" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + [[package]] name = "watchdog" version = "2.1.6" @@ -1393,7 +1509,7 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "wrapt" -version = "1.13.1" +version = "1.13.2" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false @@ -1434,7 +1550,7 @@ sqlite = [] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "b91a03b4747e33b324f8856d2b5efddf0ceaad773473409b0acff2668013bc4b" +content-hash = "2941acd715d35ca51eeaae5dffe2d7a9b2bff0ae7b74d56479c2cfedccdd3e63" [metadata.files] aiocontextvars = [ @@ -1454,8 +1570,8 @@ aiosqlite = [ {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, ] anyio = [ - {file = "anyio-3.3.2-py3-none-any.whl", hash = "sha256:c32da314c510b34a862f5afeaf8a446ffed2c2fde21583e654bd71ecfb5b744b"}, - {file = "anyio-3.3.2.tar.gz", hash = "sha256:0b993a2ef6c1dc456815c2b5ca2819f382f20af98087cc2090a4afed3a501436"}, + {file = "anyio-3.3.3-py3-none-any.whl", hash = "sha256:56ceaeed2877723578b1341f4f68c29081db189cfb40a97d1922b9513f6d7db6"}, + {file = "anyio-3.3.3.tar.gz", hash = "sha256:8eccec339cb4a856c94a75d50fc1d451faf32a05ef406be462e2efc59c9838b0"}, ] astpretty = [ {file = "astpretty-2.1.0-py2.py3-none-any.whl", hash = "sha256:f81f14b5636f7af81fadb1e3c09ca7702ce4615500d9cc6d6829befb2dec2e3c"}, @@ -1488,6 +1604,10 @@ attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] +"backports.entry-points-selectable" = [ + {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, + {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, +] bandit = [ {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, @@ -1547,13 +1667,17 @@ cffi = [ {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, ] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] charset-normalizer = [ - {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"}, - {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, ] click = [ - {file = "click-8.0.2-py3-none-any.whl", hash = "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8"}, - {file = "click-8.0.2.tar.gz", hash = "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] codecov = [ {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, @@ -1575,39 +1699,39 @@ contextvars = [ {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, ] coverage = [ - {file = "coverage-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:abe8207dfb8a61ded9cd830d26c1073c8218fc0ae17eb899cfe8ec0fafae6e22"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83faa3692e8306b20293889714fdf573d10ef5efc5843bd7c7aea6971487bd6a"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f82a17f2a77958f3eef40ad385fc82d4c6ba9a77a51a174efe03ce75daebbc16"}, - {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b06f4f1729e2963281d9cd6e65e6976bf27b44d4c07ac5b47223ce45f822cec"}, - {file = "coverage-6.0.1-cp310-cp310-win32.whl", hash = "sha256:7600fac458f74c68b097379f76f3a6e3a630493fc7fc94b6508fedd9d498c194"}, - {file = "coverage-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c5f39d1556e75fc3c4fb071f9e7cfa618895a999a0de763a541d730775d0d5f"}, - {file = "coverage-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3edbb3ec580c73e5a264f5d04f30245bc98eff1a26765d46c5c65134f0a0e2f7"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea452a2d83964d08232ade470091015e7ab9b8f53acbec10f2210fbab4ce7e43"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1770d24f45f1f2daeae34cfa3b33fcb29702153544cd2ad40d58399dd4ff53b5"}, - {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ad7182a82843f9f85487f44567c8c688f16c906bdb8d0e44ae462aed61cb8f1b"}, - {file = "coverage-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:9d242a2434801ef5125330deddb4cddba8990c9a49b3dec99dca17dd7eefba5a"}, - {file = "coverage-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e66c50f0ab445fec920a9f084914ea1776a809e3016c3738519048195f851bbb"}, - {file = "coverage-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b1ceacb86e0a9558061dcc6baae865ed25933ea57effea644f21657cdce19bc"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e15ab5afbee34abf716fece80ea33ea09a82e7450512f022723b1a82ec9a4e"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6873f3f954d3e3ab8b1881f4e5307cc19f70c9f931c41048d9f7e6fd946eabe7"}, - {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0898d6948b31df13391cd40568de8f35fa5901bc922c5ae05cf070587cb9c666"}, - {file = "coverage-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9c416ba03844608f45661a5b48dc59c6b5e89956efe388564dd138ca8caf540b"}, - {file = "coverage-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:66fe33e9e0df58675e08e83fe257f89e7f625e7633ea93d0872154e09cce2724"}, - {file = "coverage-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:353a50f123f0185cdb7a1e1e3e2cfb9d1fd7e293cfaf68eedaf5bd8e02e3ec32"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81a4e667c45b13658b84f9b8f1d32ef86d5405fabcbd181b76b9e51d295f397"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17426808e8e0824f864876312d41961223bf5e503bf8f1f846735279a60ea345"}, - {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e11cca9eb5c9b3eaad899728ee2ce916138399ee8cbbccaadc1871fecb750827"}, - {file = "coverage-6.0.1-cp38-cp38-win32.whl", hash = "sha256:0a7e55cc9f7efa22d5cc9966276ec7a40a8803676f6ccbfdc06a486fba9aa9ee"}, - {file = "coverage-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:4eb9cd910ca8e243f930243a9940ea1a522e32435d15668445753d087c30ee12"}, - {file = "coverage-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83682b73785d2e078e0b5f63410b8125b122e1a22422640c57edd4011c950f3e"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b45f89a8ef65c29195f8f28dbe215f44ccb29d934f3e862d2a5c12e38698a793"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73880a80fad0597eca43e213e5e1711bf6c0fcdb7eb6b01b3b17841ebe5a7f8d"}, - {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f398d38e6ebc2637863db1d7be3d4f9c5174e7d24bb3b0716cdb1f204669cbcf"}, - {file = "coverage-6.0.1-cp39-cp39-win32.whl", hash = "sha256:1864bdf9b2ccb43e724051bc23a1c558daf101ad4488ede1945f2a8be1facdad"}, - {file = "coverage-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c9c413c4397d4cdc7ca89286158d240ce524f9667b52c9a64dd7e13d16cf8815"}, - {file = "coverage-6.0.1-pp36-none-any.whl", hash = "sha256:65da6e3e8325291f012921bbf71fea0a97824e1c573981871096aac6e2cf0ec5"}, - {file = "coverage-6.0.1-pp37-none-any.whl", hash = "sha256:07efe1fbd72e67df026ad5109bcd216acbbd4a29d5208b3dab61779bae6b7b26"}, - {file = "coverage-6.0.1.tar.gz", hash = "sha256:3490ff6dbf3f7accf0750136ed60ae1f487bccc1f097740e3b21262bc9c89854"}, + {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, + {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, + {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, + {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, + {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, + {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, + {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, + {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, + {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, + {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, + {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, + {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, + {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, + {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, + {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, + {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, + {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, + {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, ] cryptography = [ {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, @@ -1651,6 +1775,10 @@ deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] +distlib = [ + {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, + {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, +] docspec = [ {file = "docspec-1.2.0-py3-none-any.whl", hash = "sha256:df3d2d014e0a77ac0997c9052102cf2262f12640c87af3782e2310589be4bb4c"}, {file = "docspec-1.2.0.tar.gz", hash = "sha256:5206c061d2c0171add8412028a79b436acc87786cfc582aeda341beda81ae582"}, @@ -1663,6 +1791,10 @@ fastapi = [ {file = "fastapi-0.70.0-py3-none-any.whl", hash = "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c"}, {file = "fastapi-0.70.0.tar.gz", hash = "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced"}, ] +filelock = [ + {file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"}, + {file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"}, +] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, @@ -1767,9 +1899,13 @@ greenlet = [ {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, ] +identify = [ + {file = "identify-2.3.0-py2.py3-none-any.whl", hash = "sha256:d1e82c83d063571bb88087676f81261a4eae913c492dafde184067c584bc7c05"}, + {file = "identify-2.3.0.tar.gz", hash = "sha256:fd08c97f23ceee72784081f1ce5125c8f53a02d3f2716dde79a6ab8f1039fea5"}, +] idna = [ - {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, - {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] immutables = [ {file = "immutables-0.16-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:acbfa79d44228d96296279068441f980dc63dbed52522d9227ff9f4d96c6627e"}, @@ -1804,6 +1940,10 @@ importlib-metadata = [ {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, ] +importlib-resources = [ + {file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"}, + {file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1931,6 +2071,10 @@ mysqlclient = [ {file = "mysqlclient-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:fc575093cf81b6605bed84653e48b277318b880dc9becf42dd47fa11ffd3e2b6"}, {file = "mysqlclient-2.0.3.tar.gz", hash = "sha256:f6ebea7c008f155baeefe16c56cd3ee6239f7a5a9ae42396c2f1860f08a7c432"}, ] +nodeenv = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] "nr.fs" = [ {file = "nr.fs-1.6.3-py2.py3-none-any.whl", hash = "sha256:64108c168ea2e8077fdf5f0c5417459d1a145fe34cb305fe90faeb75b4e8b421"}, {file = "nr.fs-1.6.3.tar.gz", hash = "sha256:788aa0a04c4143f95c5245bc8ccc0c0872e932be533bd37780fbb55afcdf124a"}, @@ -2002,13 +2146,12 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] +pre-commit = [ + {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, + {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, +] psycopg2-binary = [ {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24b0b6688b9f31a911f2361fe818492650795c9e5d3a1bc647acbd7440142a4f"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:542875f62bc56e91c6eac05a0deadeae20e1730be4c6334d8f04c944fcd99759"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661509f51531ec125e52357a489ea3806640d0ca37d9dada461ffc69ee1e7b6e"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:d92272c7c16e105788efe2cfa5d680f07e34e0c29b03c1908f8636f55d5f915a"}, - {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:736b8797b58febabb85494142c627bd182b50d2a7ec65322983e71065ad3034c"}, {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, @@ -2345,6 +2488,10 @@ urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] +virtualenv = [ + {file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"}, + {file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"}, +] watchdog = [ {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, @@ -2371,51 +2518,50 @@ watchdog = [ {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, ] wrapt = [ - {file = "wrapt-1.13.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:97f016514ceac524832e7d1bd41cf928b992ebe0324d59736f84ad5f4bbe0632"}, - {file = "wrapt-1.13.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:0b2cbe418beeff3aadb3afc39a67d3f5f6a3eb020ceb5f2bcf56bef14b33629a"}, - {file = "wrapt-1.13.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:95c9fcfc326fdd3e2fd264e808f6474ca7ffd253feb3a505ee5ceb4d78216ef7"}, - {file = "wrapt-1.13.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:db0daf2afca9f3b3a76e96ecb5f55ba82615ec584471d7aa27c1bdeb9e3888bb"}, - {file = "wrapt-1.13.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1b46e4fe0f9efbfaf1ee82fc79f9cb044c69b67b181c58370440d396fe40736e"}, - {file = "wrapt-1.13.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b0eed9b295039a619f64667f27cffbffcfc0559073d562700912ca6266bc8b28"}, - {file = "wrapt-1.13.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8a6ba1b00d07f5a90a2d2eb1804a42e2067a6145b7745a8297664a75a8a232ba"}, - {file = "wrapt-1.13.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:947a8d9d7829364e11eca88af18394713c8f98571cbc672b12545977d837f054"}, - {file = "wrapt-1.13.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6aa687da5565674c9696fafd2b8d44a04fb697ec2431af21c3def9cbedc4082a"}, - {file = "wrapt-1.13.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:7929ce97be2f7c49f454a6f8e014225e53cc3767fe48cce94b188de2225232ac"}, - {file = "wrapt-1.13.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2d18618440df6bc072762625e9c843d32a7328347c321b89f8df3a7c4a72ce6c"}, - {file = "wrapt-1.13.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:cb0b12b365b054bee2a53078a67df81781be0686cc3f3ab8bbdd16b2e188570a"}, - {file = "wrapt-1.13.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:3816922f0941f1637869a04e25d1e5261dfa55cc6b39c73872cbf192ea562443"}, - {file = "wrapt-1.13.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:b41ce8ee3825634e67883dd4dab336f95d0cc9d223fb7e224dcd36d66af93694"}, - {file = "wrapt-1.13.1-cp35-cp35m-win32.whl", hash = "sha256:d0ae90fd60c7473e437b0dd48ae323c11f631fe47c243056f9e7505d26e8e2f6"}, - {file = "wrapt-1.13.1-cp35-cp35m-win_amd64.whl", hash = "sha256:f4377eda306b488255ea4336662cd9015a902d6dc2ed77a3e4c1e3b42387453a"}, - {file = "wrapt-1.13.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bc42803987eb46b5fc67ec9a072df15a72ee9db61e3b7dd955d82581bf141f60"}, - {file = "wrapt-1.13.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:04a00cef5d1b9e0e8db997816437b436e859106283c4771a40c4de4759344765"}, - {file = "wrapt-1.13.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:836c73f53a0cefc7ba10c6f4a0d78894cb4876f56035fe500b029e0a1ae0ffe9"}, - {file = "wrapt-1.13.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:6c241b4ef0744590ae0ee89305743977e478200cff961bdcc6b3d0530aea3377"}, - {file = "wrapt-1.13.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:19b2c992668c9ca764899bae52987a04041ebc21859d2646db0b27e089c2fd6b"}, - {file = "wrapt-1.13.1-cp36-cp36m-win32.whl", hash = "sha256:9d200716eb4bb1d73f47f3ccc4f98fdf979dcc82d752183828f1be2e332b6874"}, - {file = "wrapt-1.13.1-cp36-cp36m-win_amd64.whl", hash = "sha256:77fef0bfdc612f5f30e43392a9f67dddaf4f48f299421bf25f910d0f47173f3d"}, - {file = "wrapt-1.13.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1137e6aef3ac267c2af7d3af0266ef3f8dd1e5cde67b8eac9fa3b94e7fa0ada"}, - {file = "wrapt-1.13.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:972099fa9cf4e43c255701c78ec5098c2fec4d6ea669a110b3414a158e772b0a"}, - {file = "wrapt-1.13.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5dc6c8cfaf4ff2a4632f8f97d29f555d6951eb0f905d3d47b3fd69bddb653214"}, - {file = "wrapt-1.13.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:f1e2cea943192e24070b65bda862901c02bdf7c6abcd66ef5381ad6511921067"}, - {file = "wrapt-1.13.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8a184c655bb41295a9b0c28745a1b762c0c86025e43808b7e814f9cedc6c563d"}, - {file = "wrapt-1.13.1-cp37-cp37m-win32.whl", hash = "sha256:6b81913fdba96e286f0c6007eb61f0158e64a1941bfc72fee61b34a4f8f9877f"}, - {file = "wrapt-1.13.1-cp37-cp37m-win_amd64.whl", hash = "sha256:aa637733f1d599077522f6a1f0c6c40389aa90a44cba37afcefef26f8e53d28f"}, - {file = "wrapt-1.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec803c9d6e4ce037201132d903ff8b0dd26c9688be50ce4c77c420c076e78ff7"}, - {file = "wrapt-1.13.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8055f8cc9a80dc1db01f31af6399b83f597ec164f07b7251d2a1bf1c6c025190"}, - {file = "wrapt-1.13.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3658ae9c704906cab5865a00c1aa9e1fd3555074d1a4462fa1742d7fea8260ae"}, - {file = "wrapt-1.13.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9f839c47698052ef5c2c094e21f8a06d0828aebe52d20cdb505faa318c62e886"}, - {file = "wrapt-1.13.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fd5320bf61a2e8d3b46d9e183323293c9a695df8f38c98d17c45e1846758f9a9"}, - {file = "wrapt-1.13.1-cp38-cp38-win32.whl", hash = "sha256:e2eb4f38441b56698b4d40d48fd331e4e8a0477264785d08cbced63813d4bd29"}, - {file = "wrapt-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:2f6fbea8936ba862425664fc689182a8ef50a6d88cd49f3cd073eccd3e78c930"}, - {file = "wrapt-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f3f99bb8eed5d394bbb898c5191ed91ebf21187d52b2c45895733ae2798f373"}, - {file = "wrapt-1.13.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:21c1710f61aa95b4be83a32b6d6facbb0efdfac22dee65e1caa72a83deed7cda"}, - {file = "wrapt-1.13.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:40fd2cebad4010787de4221ec27a650635eed3e49e4bbfa8244fc34836cc2457"}, - {file = "wrapt-1.13.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:c803526c0d3fa426e06de379b4eb56102234f2dc3c3a24a500d7962a83ca6166"}, - {file = "wrapt-1.13.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e5a0727ea56de6e9a17693589bcf913d6bf1ec49f12d4671993321f3325fda4f"}, - {file = "wrapt-1.13.1-cp39-cp39-win32.whl", hash = "sha256:04312fbf51e9dd15261228e6b4bed0c0ed5723ccf986645d2c7308511dccba35"}, - {file = "wrapt-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd705e341baccc3d1ef20e790b1f6383bd4ae92a77ba87a86ece8189fab8793c"}, - {file = "wrapt-1.13.1.tar.gz", hash = "sha256:909a80ce028821c7ad01bdcaa588126825931d177cdccd00b3545818d4a195ce"}, + {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"}, + {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"}, + {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"}, + {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"}, + {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"}, + {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"}, + {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"}, + {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"}, + {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"}, + {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"}, + {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"}, + {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"}, + {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"}, + {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"}, + {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"}, + {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"}, ] yappi = [ {file = "yappi-1.3.3.tar.gz", hash = "sha256:855890cd9a90d833dd2df632d648de8ccd0a4c3131f1edc8abd004db0625b5e8"}, diff --git a/pyproject.toml b/pyproject.toml index 65b4708..ffdc89e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ormar" -version = "0.10.20" +version = "0.10.21" description = "A simple async ORM with fastapi in mind and pydantic validation." authors = ["Radosław Drążkiewicz "] license = "MIT" @@ -115,6 +115,8 @@ dataclasses = { version = ">=0.6.0,<0.8 || >0.8,<1.0.0" } # Performance testing yappi = "^1.3.3" +pre-commit = "^2.15.0" + [tool.poetry.extras] postgresql = ["asyncpg", "psycopg2-binary"] postgres = ["asyncpg", "psycopg2-binary"] diff --git a/tests/test_fastapi/test_choices_schema.py b/tests/test_fastapi/test_choices_schema.py index 86336c5..800c90e 100644 --- a/tests/test_fastapi/test_choices_schema.py +++ b/tests/test_fastapi/test_choices_schema.py @@ -121,7 +121,6 @@ def test_all_endpoints(): "blob_col": blob.decode("utf-8"), }, ) - assert response.status_code == 200 item = Organisation(**response.json()) assert item.pk is not None diff --git a/tests/test_fastapi/test_excludes_with_get_pydantic.py b/tests/test_fastapi/test_excludes_with_get_pydantic.py index d9e9de4..b18976a 100644 --- a/tests/test_fastapi/test_excludes_with_get_pydantic.py +++ b/tests/test_fastapi/test_excludes_with_get_pydantic.py @@ -4,11 +4,8 @@ from fastapi import FastAPI from starlette.testclient import TestClient from tests.settings import DATABASE_URL -from tests.test_inheritance_and_pydantic_generation.test_geting_the_pydantic_models import ( +from tests.test_inheritance_and_pydantic_generation.test_geting_pydantic_models import ( Category, - Item, - MutualA, - MutualB, SelfRef, database, metadata, @@ -53,7 +50,9 @@ app.post("/categories/", response_model=Category)(create_category) response_model=SelfRef.get_pydantic(exclude={"parent", "children__name"}), ) async def create_selfref( - selfref: SelfRef.get_pydantic(exclude={"children__name"}), # type: ignore + selfref: SelfRef.get_pydantic( # type: ignore + exclude={"children__name"} # noqa: F821 + ), ): selfr = SelfRef(**selfref.dict()) await selfr.save() diff --git a/tests/test_inheritance_and_pydantic_generation/test_geting_the_pydantic_models.py b/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py similarity index 100% rename from tests/test_inheritance_and_pydantic_generation/test_geting_the_pydantic_models.py rename to tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py diff --git a/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py b/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py index 96d5f98..ffc515e 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py +++ b/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py @@ -14,7 +14,7 @@ class BaseMeta(ormar.ModelMeta): metadata = metadata -class TestModel(ormar.Model): +class NewTestModel(ormar.Model): class Meta: database = database metadata = metadata @@ -37,5 +37,5 @@ def create_test_database(): def test_model_field_order(): - TestCreate = TestModel.get_pydantic(exclude={"a"}) + TestCreate = NewTestModel.get_pydantic(exclude={"a"}) assert list(TestCreate.__fields__.keys()) == ["b", "c", "d", "e", "f"] diff --git a/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py b/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py new file mode 100644 index 0000000..d105481 --- /dev/null +++ b/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py @@ -0,0 +1,67 @@ +import enum + +import databases +import pydantic +import pytest +import sqlalchemy +from pydantic import ValidationError + + +import ormar +from tests.settings import DATABASE_URL + +metadata = sqlalchemy.MetaData() +database = databases.Database(DATABASE_URL) + + +class BaseMeta(ormar.ModelMeta): + database = database + metadata = metadata + + +class EnumExample(str, enum.Enum): + A = "A" + B = "B" + C = "C" + + +class ModelExample(ormar.Model): + class Meta(ormar.ModelMeta): + database = database + metadata = metadata + tablename = "examples" + + id: int = ormar.Integer(primary_key=True) + str_field: str = ormar.String(min_length=5, max_length=10, nullable=False) + enum_field: str = ormar.String( + max_length=1, nullable=False, choices=list(EnumExample) + ) + + @pydantic.validator("str_field") + def validate_str_field(cls, v): + if " " not in v: + raise ValueError("must contain a space") + return v + + +ModelExampleCreate = ModelExample.get_pydantic(exclude={"id"}) + + +def test_ormar_validator(): + ModelExample(str_field="a aaaaaa", enum_field="A") + with pytest.raises(ValidationError) as e: + ModelExample(str_field="aaaaaaa", enum_field="A") + assert "must contain a space" in str(e) + with pytest.raises(ValidationError) as e: + ModelExample(str_field="a aaaaaaa", enum_field="Z") + assert "not in allowed choices" in str(e) + + +def test_pydantic_validator(): + ModelExampleCreate(str_field="a aaaaaa", enum_field="A") + with pytest.raises(ValidationError) as e: + ModelExampleCreate(str_field="aaaaaaa", enum_field="A") + assert "must contain a space" in str(e) + with pytest.raises(ValidationError) as e: + ModelExampleCreate(str_field="a aaaaaaa", enum_field="Z") + assert "not in allowed choices" in str(e) diff --git a/tests/test_model_definition/test_model_construct.py b/tests/test_model_definition/test_model_construct.py new file mode 100644 index 0000000..87bd4ba --- /dev/null +++ b/tests/test_model_definition/test_model_construct.py @@ -0,0 +1,87 @@ +from typing import List + +import databases +import pytest +import sqlalchemy + +import ormar +from tests.settings import DATABASE_URL + +database = databases.Database(DATABASE_URL, force_rollback=True) +metadata = sqlalchemy.MetaData() + + +class NickNames(ormar.Model): + class Meta: + tablename = "nicks" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, nullable=False, name="hq_name") + + +class NicksHq(ormar.Model): + class Meta: + tablename = "nicks_x_hq" + metadata = metadata + database = database + + +class HQ(ormar.Model): + class Meta: + tablename = "hqs" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, nullable=False, name="hq_name") + nicks: List[NickNames] = ormar.ManyToMany(NickNames, through=NicksHq) + + +class Company(ormar.Model): + class Meta: + tablename = "companies" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, nullable=False, name="company_name") + founded: int = ormar.Integer(nullable=True) + hq: HQ = ormar.ForeignKey(HQ) + + +@pytest.fixture(autouse=True, scope="module") +def create_test_database(): + engine = sqlalchemy.create_engine(DATABASE_URL) + metadata.drop_all(engine) + metadata.create_all(engine) + yield + metadata.drop_all(engine) + + +@pytest.mark.asyncio +async def test_init_and_construct_has_same_effect(): + async with database: + async with database.transaction(force_rollback=True): + hq = await HQ.objects.create(name="Main") + comp = Company(name="Banzai", hq=hq, founded=1988) + comp2 = Company.construct(**dict(name="Banzai", hq=hq, founded=1988)) + assert comp.dict() == comp2.dict() + + comp3 = Company.construct(**dict(name="Banzai", hq=hq.dict(), founded=1988)) + assert comp.dict() == comp3.dict() + + +@pytest.mark.asyncio +async def test_init_and_construct_has_same_effect_with_m2m(): + async with database: + async with database.transaction(force_rollback=True): + n1 = await NickNames(name="test").save() + n2 = await NickNames(name="test2").save() + hq = HQ(name="Main", nicks=[n1, n2]) + hq2 = HQ.construct(**dict(name="Main", nicks=[n1, n2])) + assert hq.dict() == hq2.dict() + + hq3 = HQ.construct(**dict(name="Main", nicks=[n1.dict(), n2.dict()])) + assert hq.dict() == hq3.dict() diff --git a/tests/test_model_definition/test_save_status.py b/tests/test_model_definition/test_save_status.py index 93e89ac..9762810 100644 --- a/tests/test_model_definition/test_save_status.py +++ b/tests/test_model_definition/test_save_status.py @@ -63,7 +63,7 @@ def create_test_database(): @pytest.mark.asyncio -async def test_instantation_false_save_true(): +async def test_instantiation_false_save_true(): async with database: async with database.transaction(force_rollback=True): comp = Company(name="Banzai", founded=1988)