pax_global_header00006660000000000000000000000064142257717000014517gustar00rootroot0000000000000052 comment=d23070db6d2aaabf629781749327b44bff974ce3 djantic-0.7.0/000077500000000000000000000000001422577170000131375ustar00rootroot00000000000000djantic-0.7.0/.bumpversion.cfg000066400000000000000000000004221422577170000162450ustar00rootroot00000000000000[bumpversion] current_version = 0.7.0 commit = True [bumpversion:file:pyproject.toml] search = version = "{current_version}" replace = version = "{new_version}" [bumpversion:file:setup.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" djantic-0.7.0/.github/000077500000000000000000000000001422577170000144775ustar00rootroot00000000000000djantic-0.7.0/.github/workflows/000077500000000000000000000000001422577170000165345ustar00rootroot00000000000000djantic-0.7.0/.github/workflows/publish.yml000066400000000000000000000016621422577170000207320ustar00rootroot00000000000000name: Publish on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.8' - uses: actions/cache@v2 name: Configure pip caching with: path: ~/.cache/pip key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }} restore-keys: | ${{ runner.os }}-publish-pip- - name: Install dependencies run: | pip install -U pip poetry pip install setuptools wheel twine poetry config --local virtualenvs.in-project true poetry install - name: Run tests run: bin/test - name: Publish if: success() env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/* djantic-0.7.0/.github/workflows/test.yml000066400000000000000000000015221422577170000202360ustar00rootroot00000000000000name: Test on: push: branches: ["main"] pull_request: branches: ["main"] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.7, 3.8, 3.9, 3.10.0] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v2 name: Configure pip caching with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} restore-keys: | ${{ runner.os }}-pip- - name: Install dependencies run: | pip install -U pip poetry tox tox-gh-actions - name: "Run tox targets for ${{ matrix.python-version }}" run: "python -m tox" djantic-0.7.0/.gitignore000066400000000000000000000034641422577170000151360ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ .idea .DS_Store *.sublime-workspace .vscode djantic-0.7.0/LICENSE000066400000000000000000000020601422577170000141420ustar00rootroot00000000000000MIT License Copyright (c) 2021 Jordan Eremieff Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. djantic-0.7.0/README.md000066400000000000000000000177461422577170000144350ustar00rootroot00000000000000

Djantic

Pydantic model support for Django

GitHub Workflow Status (Test) PyPi package Supported Python versions Supported Django versions

--- **Documentation**: https://jordaneremieff.github.io/djantic/ --- Djantic is a library that provides a configurable utility class for automatically creating a Pydantic model instance for any Django model class. It is intended to support all of the underlying Pydantic model functionality such as JSON schema generation and introduces custom behaviour for exporting Django model instance data. ## Quickstart Install using pip: ```shell pip install djantic ``` Create a model schema: ```python from users.models import User from djantic import ModelSchema class UserSchema(ModelSchema): class Config: model = User print(UserSchema.schema()) ``` **Output:** ```python { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "profile": {"title": "Profile", "description": "None", "type": "integer"}, "id": {"title": "Id", "description": "id", "type": "integer"}, "first_name": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string", }, "last_name": { "title": "Last Name", "description": "last_name", "maxLength": 50, "type": "string", }, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "updated_at": { "title": "Updated At", "description": "updated_at", "type": "string", "format": "date-time", }, }, "required": ["first_name", "email", "created_at", "updated_at"], } ``` See https://pydantic-docs.helpmanual.io/usage/models/ for more. ### Loading and exporting model instances Use the `from_orm` method on the model schema to load a Django model instance for export: ```python user = User.objects.create( first_name="Jordan", last_name="Eremieff", email="jordan@eremieff.com" ) user_schema = UserSchema.from_orm(user) print(user_schema.json(indent=2)) ``` **Output:** ```json { "profile": null, "id": 1, "first_name": "Jordan", "last_name": "Eremieff", "email": "jordan@eremieff.com", "created_at": "2020-08-15T16:50:30.606345+00:00", "updated_at": "2020-08-15T16:50:30.606452+00:00" } ``` ### Using multiple level relations Djantic supports multiple level relations. This includes foreign keys, many-to-many, and one-to-one relationships. Consider the following example Django model and Djantic model schema definitions for a number of related database records: ```python # models.py from django.db import models class OrderUser(models.Model): email = models.EmailField(unique=True) class OrderUserProfile(models.Model): address = models.CharField(max_length=255) user = models.OneToOneField(OrderUser, on_delete=models.CASCADE, related_name='profile') class Order(models.Model): total_price = models.DecimalField(max_digits=8, decimal_places=5, default=0) user = models.ForeignKey( OrderUser, on_delete=models.CASCADE, related_name="orders" ) class OrderItem(models.Model): price = models.DecimalField(max_digits=8, decimal_places=5, default=0) quantity = models.IntegerField(default=0) order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name="items" ) class OrderItemDetail(models.Model): name = models.CharField(max_length=30) order_item = models.ForeignKey( OrderItem, on_delete=models.CASCADE, related_name="details" ) ``` ```python # schemas.py from djantic import ModelSchema from orders.models import OrderItemDetail, OrderItem, Order, OrderUserProfile class OrderItemDetailSchema(ModelSchema): class Config: model = OrderItemDetail class OrderItemSchema(ModelSchema): details: List[OrderItemDetailSchema] class Config: model = OrderItem class OrderSchema(ModelSchema): items: List[OrderItemSchema] class Config: model = Order class OrderUserProfileSchema(ModelSchema): class Config: model = OrderUserProfile class OrderUserSchema(ModelSchema): orders: List[OrderSchema] profile: OrderUserProfileSchema ``` Now let's assume you're interested in exporting the order and profile information for a particular user into a JSON format that contains the details accross all of the related item objects: ```python user = OrderUser.objects.first() print(OrderUserSchema.from_orm(user).json(ident=4)) ``` **Output:** ```json { "profile": { "id": 1, "address": "", "user": 1 }, "orders": [ { "items": [ { "details": [ { "id": 1, "name": "", "order_item": 1 } ], "id": 1, "price": 0.0, "quantity": 0, "order": 1 } ], "id": 1, "total_price": 0.0, "user": 1 } ], "id": 1, "email": "" } ``` The model schema definitions are composable and support customization of the output according to the auto-generated fields and any additional annotations. ### Including and excluding fields The fields exposed in the model instance may be configured using two options: `include` and `exclude`. These represent iterables that should contain a list of field name strings. Only one of these options may be set at the same time, and if neither are set then the default behaviour is to include all of the fields from the Django model. For example, to include all of the fields from a user model except a field named `email_address`, you would use the `exclude` option: ```python class UserSchema(ModelSchema): class Config: exclude = ["email_address"] ``` In addition to this, you may also limit the fields to only include annotations from the model schema class by setting the `include` option to a special string value: `"__annotations__"`. ```python class ProfileSchema(ModelSchema): website: str class Config: model = Profile include = "__annotations__" assert ProfileSchema.schema() == { "title": "ProfileSchema", "description": "A user's profile.", "type": "object", "properties": { "website": { "title": "Website", "type": "string" } }, "required": [ "website" ] } ``` djantic-0.7.0/bin/000077500000000000000000000000001422577170000137075ustar00rootroot00000000000000djantic-0.7.0/bin/lint000077500000000000000000000003351422577170000146040ustar00rootroot00000000000000#!/bin/bash export PREFIX="" if [ -d "venv" ] ; then export PREFIX="venv/bin/" fi set -ex ${PREFIX}poetry run black djantic tests --check ${PREFIX}poetry run flake8 djantic tests # ${PREFIX}poetry run mypy djantic djantic-0.7.0/bin/test000077500000000000000000000003171422577170000146150ustar00rootroot00000000000000#!/bin/bash export PREFIX="" if [ -d "venv" ] ; then export PREFIX="venv/bin/" fi set -ex ${PREFIX}poetry run pytest --ignore .venv --cov=djantic --cov-fail-under=100 --cov-report=term-missing "${@}" djantic-0.7.0/djantic/000077500000000000000000000000001422577170000145535ustar00rootroot00000000000000djantic-0.7.0/djantic/__init__.py000066400000000000000000000000711422577170000166620ustar00rootroot00000000000000from .main import ModelSchema __all__ = ["ModelSchema"] djantic-0.7.0/djantic/fields.py000066400000000000000000000115621422577170000164000ustar00rootroot00000000000000import logging from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum from typing import Any, Dict, List, Union from uuid import UUID from django.utils.functional import Promise from pydantic import IPvAnyAddress, Json from pydantic.fields import FieldInfo, Required, Undefined logger = logging.getLogger("djantic") INT_TYPES = [ "AutoField", "BigAutoField", "IntegerField", "SmallIntegerField", "BigIntegerField", "PositiveIntegerField", "PositiveSmallIntegerField", ] STR_TYPES = [ "CharField", "EmailField", "URLField", "SlugField", "TextField", "FilePathField", "FileField", ] FIELD_TYPES = { "GenericIPAddressField": IPvAnyAddress, "BooleanField": bool, "BinaryField": bytes, "DateField": date, "DateTimeField": datetime, "DurationField": timedelta, "TimeField": time, "DecimalField": Decimal, "FloatField": float, "UUIDField": UUID, "JSONField": Union[Json, dict, list], # TODO: Configure this using default "ArrayField": List, # "BigIntegerRangeField", # "CICharField", # "CIEmailField", # "CIText", # "CITextField", # "DateRangeField", # "DateTimeRangeField", # "DecimalRangeField", # "FloatRangeField", # "HStoreField", # "IntegerRangeField", # "RangeBoundary", # "RangeField", # "RangeOperators", } def ModelSchemaField(field: Any, schema_name: str) -> tuple: default = Required default_factory = None description = None title = None max_length = None python_type = None if field.is_relation: if not field.related_model: internal_type = field.model._meta.pk.get_internal_type() else: internal_type = field.related_model._meta.pk.get_internal_type() if not field.concrete and field.auto_created or field.null: default = None pk_type = FIELD_TYPES.get(internal_type, int) if field.one_to_many or field.many_to_many: python_type = List[Dict[str, pk_type]] else: python_type = pk_type if field.related_model: field = field.target_field else: if field.choices: enum_choices = {} for k, v in field.choices: if Promise in type(v).__mro__: v = str(v) enum_choices[v] = k if field.blank: enum_choices['_blank'] = '' enum_prefix = ( f"{schema_name.replace('_', '')}{field.name.title().replace('_', '')}" ) python_type = Enum( # type: ignore f"{enum_prefix}Enum", enum_choices, module=__name__, ) if field.has_default() and isinstance(field.default, Enum): default = field.default.value else: internal_type = field.get_internal_type() if internal_type in STR_TYPES: python_type = str if not field.choices: max_length = field.max_length elif internal_type in INT_TYPES: python_type = int elif internal_type in FIELD_TYPES: python_type = FIELD_TYPES[internal_type] else: # pragma: nocover for field_class in type(field).__mro__: get_internal_type = getattr(field_class, "get_internal_type", None) if get_internal_type: _internal_type = get_internal_type(field_class()) if _internal_type in FIELD_TYPES: python_type = FIELD_TYPES[_internal_type] break if python_type is None: logger.warning( "%s is currently unhandled, defaulting to str.", field.__class__ ) python_type = str deconstructed = field.deconstruct() field_options = deconstructed[3] or {} blank = field_options.pop("blank", False) null = field_options.pop("null", False) if default is Required and field.has_default(): if callable(field.default): default_factory = field.default default = Undefined else: default = field.default elif field.primary_key or blank or null: default = None if default is not None and field.null: python_type = Union[python_type, None] description = field.help_text title = field.verbose_name.title() if not description: description = field.name return ( python_type, FieldInfo( default, default_factory=default_factory, title=title, description=str(description), max_length=max_length, ), ) djantic-0.7.0/djantic/main.py000066400000000000000000000164331422577170000160600ustar00rootroot00000000000000from functools import reduce from itertools import chain from typing import Any, Dict, List, Optional, no_type_check from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Manager, Model from django.db.models.fields.files import ImageFieldFile from django.db.models.fields.reverse_related import (ForeignObjectRel, OneToOneRel) from django.utils.encoding import force_str from django.utils.functional import Promise from pydantic import BaseModel, ConfigError, create_model from pydantic.main import ModelMetaclass from pydantic.utils import GetterDict from .fields import ModelSchemaField _is_base_model_class_defined = False class ModelSchemaJSONEncoder(DjangoJSONEncoder): @no_type_check def default(self, obj): # pragma: nocover if isinstance(obj, Promise): return force_str(obj) return super().default(obj) def get_field_name(field) -> str: if issubclass(field.__class__, ForeignObjectRel) and not issubclass(field.__class__, OneToOneRel): return getattr(field, "related_name", None) or f"{field.name}_set" else: return getattr(field, "name", field) class ModelSchemaMetaclass(ModelMetaclass): @no_type_check def __new__( mcs, name: str, bases: tuple, namespace: dict, ): cls = super().__new__(mcs, name, bases, namespace) for base in reversed(bases): if ( _is_base_model_class_defined and issubclass(base, ModelSchema) and base == ModelSchema ): try: config = namespace["Config"] except KeyError as exc: raise ConfigError( f"{exc} (Is `Config` class defined?)" ) include = getattr(config, "include", None) exclude = getattr(config, "exclude", None) if include and exclude: raise ConfigError( "Only one of 'include' or 'exclude' should be set in " "configuration." ) annotations = namespace.get("__annotations__", {}) try: fields = config.model._meta.get_fields() except AttributeError as exc: raise ConfigError( f"{exc} (Is `Config.model` a valid Django model class?)" ) if include == '__annotations__': include = list(annotations.keys()) cls.__config__.include = include elif include is None and exclude is None: include = list(annotations.keys()) + [get_field_name(f) for f in fields] cls.__config__.include = include field_values = {} _seen = set() for field in chain(fields, annotations.copy()): field_name = get_field_name(field) if ( field_name in _seen or (include and field_name not in include) or (exclude and field_name in exclude) ): continue _seen.add(field_name) python_type = None pydantic_field = None if field_name in annotations and field_name in namespace: python_type = annotations.pop(field_name) pydantic_field = namespace[field_name] if ( hasattr(pydantic_field, "default_factory") and pydantic_field.default_factory ): pydantic_field = pydantic_field.default_factory() elif field_name in annotations: python_type = annotations.pop(field_name) pydantic_field = ( None if Optional[python_type] == python_type else Ellipsis ) else: python_type, pydantic_field = ModelSchemaField(field, name) field_values[field_name] = (python_type, pydantic_field) cls.__doc__ = namespace.get("__doc__", config.model.__doc__) cls.__fields__ = {} cls.__alias_map__ = {getattr(model_field[1], 'alias', None) or field_name: field_name for field_name, model_field in field_values.items()} model_schema = create_model( name, __base__=cls, __module__=cls.__module__, **field_values ) return model_schema return cls class ProxyGetterNestedObj(GetterDict): def __init__(self, obj: Any, schema_class): self._obj = obj self.schema_class = schema_class def get(self, key: Any, default: Any = None) -> Any: alias = self.schema_class.__alias_map__[key] outer_type_ = self.schema_class.__fields__[alias].outer_type_ if "__" in key: # Allow double underscores aliases: `first_name: str = Field(alias="user__first_name")` keys_map = key.split("__") attr = reduce(lambda a, b: getattr(a, b, default), keys_map, self._obj) else: attr = getattr(self._obj, key, None) is_manager = issubclass(attr.__class__, Manager) if is_manager and outer_type_ == List[Dict[str, int]]: attr = list(attr.all().values("id")) elif is_manager: attr = list(attr.all()) elif outer_type_ == int and issubclass(type(attr), Model): attr = attr.id elif issubclass(attr.__class__, ImageFieldFile) and issubclass(outer_type_, str): attr = attr.name return attr class ModelSchema(BaseModel, metaclass=ModelSchemaMetaclass): class Config: orm_mode = True @classmethod def schema_json( cls, *, by_alias: bool = True, encoder_cls: Any = ModelSchemaJSONEncoder, **dumps_kwargs: Any, ) -> str: return cls.__config__.json_dumps( cls.schema(by_alias=by_alias), cls=encoder_cls, **dumps_kwargs ) @classmethod @no_type_check def get_field_names(cls) -> List[str]: if hasattr(cls.__config__, "exclude"): django_model_fields = cls.__config__.model._meta.get_fields() all_fields = [f.name for f in django_model_fields] return [ name for name in all_fields if name not in cls.__config__.exclude ] return cls.__config__.include @classmethod def from_orm(cls, *args, **kwargs): return cls.from_django(*args, **kwargs) @classmethod def from_django(cls, objs, many=False, context={}): cls.context = context if many: result_objs = [] for obj in objs: cls.instance = obj result_objs.append(super().from_orm(ProxyGetterNestedObj(obj, cls))) return result_objs cls.instance = objs return super().from_orm(ProxyGetterNestedObj(objs, cls)) _is_base_model_class_defined = True djantic-0.7.0/djantic/py.typed000066400000000000000000000000001422577170000162400ustar00rootroot00000000000000djantic-0.7.0/docs/000077500000000000000000000000001422577170000140675ustar00rootroot00000000000000djantic-0.7.0/docs/index.md000066400000000000000000000177461422577170000155370ustar00rootroot00000000000000

Djantic

Pydantic model support for Django

GitHub Workflow Status (Test) PyPi package Supported Python versions Supported Django versions

--- **Documentation**: https://jordaneremieff.github.io/djantic/ --- Djantic is a library that provides a configurable utility class for automatically creating a Pydantic model instance for any Django model class. It is intended to support all of the underlying Pydantic model functionality such as JSON schema generation and introduces custom behaviour for exporting Django model instance data. ## Quickstart Install using pip: ```shell pip install djantic ``` Create a model schema: ```python from users.models import User from djantic import ModelSchema class UserSchema(ModelSchema): class Config: model = User print(UserSchema.schema()) ``` **Output:** ```python { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "profile": {"title": "Profile", "description": "None", "type": "integer"}, "id": {"title": "Id", "description": "id", "type": "integer"}, "first_name": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string", }, "last_name": { "title": "Last Name", "description": "last_name", "maxLength": 50, "type": "string", }, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "updated_at": { "title": "Updated At", "description": "updated_at", "type": "string", "format": "date-time", }, }, "required": ["first_name", "email", "created_at", "updated_at"], } ``` See https://pydantic-docs.helpmanual.io/usage/models/ for more. ### Loading and exporting model instances Use the `from_orm` method on the model schema to load a Django model instance for export: ```python user = User.objects.create( first_name="Jordan", last_name="Eremieff", email="jordan@eremieff.com" ) user_schema = UserSchema.from_orm(user) print(user_schema.json(indent=2)) ``` **Output:** ```json { "profile": null, "id": 1, "first_name": "Jordan", "last_name": "Eremieff", "email": "jordan@eremieff.com", "created_at": "2020-08-15T16:50:30.606345+00:00", "updated_at": "2020-08-15T16:50:30.606452+00:00" } ``` ### Using multiple level relations Djantic supports multiple level relations. This includes foreign keys, many-to-many, and one-to-one relationships. Consider the following example Django model and Djantic model schema definitions for a number of related database records: ```python # models.py from django.db import models class OrderUser(models.Model): email = models.EmailField(unique=True) class OrderUserProfile(models.Model): address = models.CharField(max_length=255) user = models.OneToOneField(OrderUser, on_delete=models.CASCADE, related_name='profile') class Order(models.Model): total_price = models.DecimalField(max_digits=8, decimal_places=5, default=0) user = models.ForeignKey( OrderUser, on_delete=models.CASCADE, related_name="orders" ) class OrderItem(models.Model): price = models.DecimalField(max_digits=8, decimal_places=5, default=0) quantity = models.IntegerField(default=0) order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name="items" ) class OrderItemDetail(models.Model): name = models.CharField(max_length=30) order_item = models.ForeignKey( OrderItem, on_delete=models.CASCADE, related_name="details" ) ``` ```python # schemas.py from djantic import ModelSchema from orders.models import OrderItemDetail, OrderItem, Order, OrderUserProfile class OrderItemDetailSchema(ModelSchema): class Config: model = OrderItemDetail class OrderItemSchema(ModelSchema): details: List[OrderItemDetailSchema] class Config: model = OrderItem class OrderSchema(ModelSchema): items: List[OrderItemSchema] class Config: model = Order class OrderUserProfileSchema(ModelSchema): class Config: model = OrderUserProfile class OrderUserSchema(ModelSchema): orders: List[OrderSchema] profile: OrderUserProfileSchema ``` Now let's assume you're interested in exporting the order and profile information for a particular user into a JSON format that contains the details accross all of the related item objects: ```python user = OrderUser.objects.first() print(OrderUserSchema.from_orm(user).json(ident=4)) ``` **Output:** ```json { "profile": { "id": 1, "address": "", "user": 1 }, "orders": [ { "items": [ { "details": [ { "id": 1, "name": "", "order_item": 1 } ], "id": 1, "price": 0.0, "quantity": 0, "order": 1 } ], "id": 1, "total_price": 0.0, "user": 1 } ], "id": 1, "email": "" } ``` The model schema definitions are composable and support customization of the output according to the auto-generated fields and any additional annotations. ### Including and excluding fields The fields exposed in the model instance may be configured using two options: `include` and `exclude`. These represent iterables that should contain a list of field name strings. Only one of these options may be set at the same time, and if neither are set then the default behaviour is to include all of the fields from the Django model. For example, to include all of the fields from a user model except a field named `email_address`, you would use the `exclude` option: ```python class UserSchema(ModelSchema): class Config: exclude = ["email_address"] ``` In addition to this, you may also limit the fields to only include annotations from the model schema class by setting the `include` option to a special string value: `"__annotations__"`. ```python class ProfileSchema(ModelSchema): website: str class Config: model = Profile include = "__annotations__" assert ProfileSchema.schema() == { "title": "ProfileSchema", "description": "A user's profile.", "type": "object", "properties": { "website": { "title": "Website", "type": "string" } }, "required": [ "website" ] } ``` djantic-0.7.0/docs/usage.md000066400000000000000000000323021422577170000155150ustar00rootroot00000000000000# Usage The main functionality this library intends to provide is a means to automatically generate Pydantic models based on Django ORM model definitions. Most of the Pydantic [model properties](https://pydantic-docs.helpmanual.io/usage/models/#model-properties) are expected to work with the generated model schemas. In addition to this, the model schemas provide a `from_orm` method for loading Django object instance data to be used with Pydantic's [model export](https://pydantic-docs.helpmanual.io/usage/exporting_models/) methods. ## Creating a model schema The `ModelSchema` class can be used to generate a Pydantic model that maps to a Django model's fields automatically, and they also support customization using type annotations and field configurations. Consider the following model definition for a user in Django: ```python from django.db import models class User(models.Model): """ A user of the application. """ first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50, null=True, blank=True) email = models.EmailField(unique=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) ``` A custom `ModelSchema` class could then be configured for this model: ```python from djantic import ModelSchema from myapp.models import User class UserSchema(ModelSchema): class Config: model = User ``` Once defined, the `UserSchema` can be used to perform various functions on the underlying Django model object, such as generating JSON schemas or exporting serialized instance data. ### Basic schema usage The `UserSchema` above can be used to generate a JSON schema using Pydantic's [schema](https://pydantic-docs.helpmanual.io/usage/schema/) method: ```python print(UserSchema.schema()) ``` Output: ```python { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "profile": {"title": "Profile", "description": "None", "type": "integer"}, "id": {"title": "Id", "description": "id", "type": "integer"}, "first_name": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string", }, "last_name": { "title": "Last Name", "description": "last_name", "maxLength": 50, "type": "string", }, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "updated_at": { "title": "Updated At", "description": "updated_at", "type": "string", "format": "date-time", }, }, "required": ["first_name", "email", "created_at", "updated_at"], } ``` By default, all of the fields in a Django model will be included in the model schema produced using the details of each field's configuration. ### Customizing the schema By default, the docstrings and help text of the Django model definition is used to populate the various titles and descriptive text and constraints in the schema outputs. However, the model schema class itself can be used to override this behaviour: ```python from pydantic import Field, constr from djantic import ModelSchema from myapp.models import User class UserSchema(ModelSchema): """ My custom model schema. """ first_name: str = Field( None, title="The user's first name", description="This is the user's first name", ) last_name: constr(strip_whitespace=True) class Config: model = User title = "My user schema" ``` Output: ```python { "title": "My user schema", "description": "My custom model schema.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "first_name": { "title": "The user's first name", "description": "This is the user's first name", "type": "string", }, "last_name": {"title": "Last Name", "type": "string"}, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "updated_at": { "title": "Updated At", "description": "updated_at", "type": "string", "format": "date-time", }, }, "required": ["first_name", "last_name", "email", "created_at", "updated_at"], } ``` Model schemas also support using standard Python type annotations and field inclusion/exclusion configurations to customize the schemas beyond the definitions inferred from the Django model. For example, the `last_name` field in the Django model is considered optional because of the `null=True` and `blank=True` parameters in the field definition, and the `first_name` field is required. These details can be modified by defining specific field rules using type annotations, and the schema fields can limited using the `include` (or `exclude`) configuration setting: ```python class UserSchema(ModelSchema): first_name: Optional[str] last_name: str class Config: model = User include = ["first_name", "last_name"] ``` Output: ```python { "description": "A user of the application.", "properties": { "first_name": {"title": "First Name", "type": "string"}, "last_name": {"title": "Last Name", "type": "string"}, }, "required": ["last_name"], "title": "UserSchema", "type": "object", } ``` ## Handling related objects Database relations (many to one, one to one, many to many) are also supported in the schema definition. Generic relations are also supported, but not extensively tested. Consider the initial `User` model in [creating a schema](/schemas/#creating-a-schema), but with the addition of a `Profile` model containing a one to one relationship: ```python from django.db import models class User(models.Model): """ A user of the application. """ first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50, null=True, blank=True) email = models.EmailField(unique=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Profile(models.Model): """ A user's profile. """ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") website = models.URLField(default="", blank=True) location = models.CharField(max_length=100, default="", blank=True) ``` The new `Profile` relationship would be available to the generated model schema: ```python from djantic import ModelSchema from myapp.models import User class UserSchema(ModelSchema): class Config: model = User include = ["id", "email", "profile"] print(UserSchema.schema()) ``` Output: ```python { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "profile": {"title": "Profile", "description": "id", "type": "integer"}, "id": {"title": "Id", "description": "id", "type": "integer"}, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, }, "required": ["email"], } ``` ***Note***: The initial `UserSchema` example in [creating a schema](/schemas/#creating-a-schema) could be used without modification. The `include` list here is used to reduce the example output and is not required for relations support. #### Related schema models The auto-generated `profile` definition can be expanded using an additional model schema set on the user schema: ```python class ProfileSchema(ModelSchema): class Config: model = Profile class UserSchema(ModelSchema): profile: ProfileSchema class Config: model = User include = ["id", "profile"] print(UserSchema.schema()) ``` Output: ```python { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "profile": {"$ref": "#/definitions/ProfileSchema"}, "id": {"title": "Id", "description": "id", "type": "integer"}, }, "required": ["profile"], "definitions": { "ProfileSchema": { "title": "ProfileSchema", "description": "A user's profile.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "user": {"title": "User", "description": "id", "type": "integer"}, "website": { "title": "Website", "description": "website", "default": "", "maxLength": 200, "type": "string", }, "location": { "title": "Location", "description": "location", "default": "", "maxLength": 100, "type": "string", }, }, "required": ["user"], } }, } ``` These schema relationships also work in reverse: ```python class UserSchema(ModelSchema): class Config: model = User include = ["id", "email"] class ProfileSchemaWithUser(ModelSchema): user: UserSchema class Config: model = Profile include = ["id", "user"] print(ProfileSchemaWithUser.schema()) ``` Output: ```python { "title": "ProfileSchemaWithUser", "description": "A user's profile.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "user": {"$ref": "#/definitions/UserSchema"}, }, "required": ["user"], "definitions": { "UserSchema": { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, }, "required": ["email"], } }, } ``` The above behaviour works similarly to one to many and many to many relations. You can see more examples in the [tests](https://github.com/jordaneremieff/djantic/blob/main/tests/test_relations.py). ## Exporting model data Model schemas support a `from_orm` method that allows loading Django model instances for export using the generated schema. This method is similar to Pydantic's builtin [from_orm](https://pydantic-docs.helpmanual.io/usage/models/#orm-mode-aka-arbitrary-class-instances), but very specific to Django's ORM. It is intended to provide support for all of Pydantic's [model export](https://pydantic-docs.helpmanual.io/usage/exporting_models/) methods. ### Basic export usage Create one or more Django model instances to be used when populating the model schema: ```python user = User.objects.create( first_name="Jordan", last_name="Eremieff", email="jordan@eremieff.com" ) profile = Profile.objects.create(user=user, website="https://github.com", location="AU") ``` Then use the `from_orm` method to load this object: ```python from djantic import ModelSchema from myapp.models import User class ProfileSchema(ModelSchema): class Config: model = Profile exclude = ["user"] class UserSchema(ModelSchema): profile: ProfileSchema class Config: model = User user = User.objects.get(id=1) obj = UserSchema.from_orm(user) ``` Now that the instance is loaded, it can be used with the various export methods to produce different outputs according to the schema definition. These outputs will be validated against the schema rules: #### model.dict() ```python print(obj.dict()) ``` Output: ```python { "profile": {"id": 1, "website": "https://github.com", "location": "AU"}, "id": 1, "first_name": "Jordan", "last_name": "Eremieff", "email": "jordan@eremieff.com", "created_at": datetime.datetime(2021, 4, 4, 8, 47, 39, 567410, tzinfo=), "updated_at": datetime.datetime(2021, 4, 4, 8, 47, 39, 567455, tzinfo=) } ``` #### model.json() ```python print(obj.json(indent=2)) ``` Output: ```json { "profile": { "id": 1, "website": "https://github.com", "location": "AU" }, "id": 1, "first_name": "Jordan", "last_name": "Eremieff", "email": "jordan@eremieff.com", "created_at": "2021-04-04T08:47:39.567410+00:00", "updated_at": "2021-04-04T08:47:39.567455+00:00" } ``` djantic-0.7.0/mkdocs.yml000066400000000000000000000015161422577170000151450ustar00rootroot00000000000000site_name: Djantic site_description: Pydantic model schemas for Django ORM site_url: https://github.com/jordaneremieff/djantic theme: name: material palette: # Light mode - media: "(prefers-color-scheme: light)" scheme: default primary: deeppurple accent: deeppurple toggle: icon: material/toggle-switch name: Switch to dark mode # Dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: blue accent: blue toggle: icon: material/toggle-switch-off-outline name: Switch to light mode repo_name: jordaneremieff/djantic repo_url: https://github.com/jordaneremieff/djantic edit_uri: "" nav: - Introduction: 'index.md' - Usage: 'usage.md' markdown_extensions: - mkautodoc - pymdownx.highlight - pymdownx.superfencesdjantic-0.7.0/poetry.lock000066400000000000000000002520301422577170000153350ustar00rootroot00000000000000[[package]] name = "appdirs" version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = "*" [[package]] name = "asgiref" version = "3.4.1" description = "ASGI specs, helper code, and adapters" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 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 = "black" version = "21.7b0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] appdirs = "*" click = ">=7.1.2" mypy-extensions = ">=0.4.3" pathspec = ">=0.8.1,<1" regex = ">=2020.1.8" tomli = ">=0.2.6,<2.0.0" typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] python2 = ["typed-ast (>=1.4.2)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" version = "3.3.1" description = "An easy safelist-based HTML-sanitizing tool." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] packaging = "*" six = ">=1.9.0" webencodings = "*" [[package]] name = "bump2version" version = "1.0.1" description = "Version-bump your software with a single command!" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "certifi" version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = "*" [[package]] name = "cffi" version = "1.14.6" description = "Foreign Function Interface for Python calling C code." category = "dev" optional = false python-versions = "*" [package.dependencies] pycparser = "*" [[package]] name = "charset-normalizer" version = "2.0.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false python-versions = ">=3.5.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "click" version = "8.0.1" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "codecov" version = "2.1.11" description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] coverage = "*" requests = ">=2.7.9" [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" category = "dev" optional = false python-versions = "*" [package.extras] test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" version = "5.5" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] toml = ["toml"] [[package]] name = "cryptography" version = "3.4.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "django" version = "3.2.12" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] asgiref = ">=3.3.2,<4" pytz = "*" sqlparse = ">=0.2.2" [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-stubs" version = "1.8.0" description = "Mypy stubs for Django" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] django = "*" django-stubs-ext = "*" mypy = ">=0.790" typing-extensions = "*" [[package]] name = "django-stubs-ext" version = "0.2.0" description = "Monkey-patching and extensions for django-stubs" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] django = "*" [[package]] name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "factory-boy" version = "3.2.1" description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] Faker = ">=0.7.0" [package.extras] dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] [[package]] name = "faker" version = "9.5.0" description = "Faker is a Python package that generates fake data for you." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.4" text-unidecode = "1.3" [[package]] name = "flake8" version = "3.9.2" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "ghp-import" version = "2.0.1" description = "Copy your docs directly to the gh-pages branch." category = "dev" optional = false python-versions = "*" [package.dependencies] python-dateutil = ">=2.8.1" [package.extras] dev = ["twine", "markdown", "flake8"] [[package]] name = "idna" version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "importlib-metadata" version = "4.6.1" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] 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 = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = "*" [[package]] name = "jeepney" version = "0.7.0" description = "Low-level, pure Python DBus protocol wrapper." category = "dev" optional = false python-versions = ">=3.6" [package.extras] test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio"] trio = ["trio", "async-generator"] [[package]] name = "jinja2" version = "3.0.1" description = "A very fast and expressive template engine." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" version = "23.0.1" description = "Store and access your passwords safely." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] importlib-metadata = ">=3.6" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [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 = "markdown" version = "3.3.4" description = "Python implementation of Markdown." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = "*" [[package]] name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "mkautodoc" version = "0.1.0" description = "AutoDoc for MarkDown" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "mkdocs" version = "1.2.3" description = "Project documentation with Markdown." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] click = ">=3.3" ghp-import = ">=1.0" importlib-metadata = ">=3.10" Jinja2 = ">=2.10.1" Markdown = ">=3.2.1" mergedeep = ">=1.3.4" packaging = ">=20.5" PyYAML = ">=3.10" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] [[package]] name = "mkdocs-material" version = "7.1.10" description = "A Material Design theme for MkDocs" category = "dev" optional = false python-versions = "*" [package.dependencies] markdown = ">=3.2" mkdocs = ">=1.1" mkdocs-material-extensions = ">=1.0" Pygments = ">=2.4" pymdown-extensions = ">=7.0" [[package]] name = "mkdocs-material-extensions" version = "1.0.1" description = "Extension pack for Python Markdown." category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] mkdocs-material = ">=5.0.0" [[package]] name = "mypy" version = "0.910" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" toml = "*" typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "dev" optional = false python-versions = "*" [[package]] name = "packaging" version = "21.0" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" [[package]] name = "pathspec" version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pkginfo" version = "1.7.1" description = "Query metadatdata from sdists / bdists / installed packages." category = "dev" optional = false python-versions = "*" [package.extras] testing = ["nose", "coverage"] [[package]] name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] [[package]] name = "psycopg2" version = "2.9.1" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycodestyle" version = "2.7.0" description = "Python style guide checker" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycparser" version = "2.20" description = "C parser in Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" version = "1.8.2" description = "Data validation and settings management using python 3.6 type hinting" category = "main" optional = false python-versions = ">=3.6.1" [package.dependencies] typing-extensions = ">=3.7.4.3" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" version = "2.3.1" description = "passive checker of Python programs" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" version = "2.9.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "pymdown-extensions" version = "8.2" description = "Extension pack for Python Markdown." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] Markdown = ">=3.2" [[package]] name = "pyparsing" version = "2.4.7" description = "Python parsing module" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" version = "6.2.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<1.0.0a1" py = ">=1.8.2" toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-cov" version = "2.12.1" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] coverage = ">=5.2.1" pytest = ">=4.6" toml = "*" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-django" version = "4.4.0" description = "A Django plugin for pytest." category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] pytest = ">=5.4.0" [package.extras] docs = ["sphinx", "sphinx-rtd-theme"] testing = ["django", "django-configurations (>=2.0)"] [[package]] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" [[package]] name = "pytz" version = "2021.1" description = "World timezone definitions, modern and historical" category = "main" optional = false python-versions = "*" [[package]] name = "pywin32-ctypes" version = "0.2.0" description = "" category = "dev" optional = false python-versions = "*" [[package]] name = "pyyaml" version = "5.4.1" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pyyaml = "*" [[package]] name = "readme-renderer" version = "29.0" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" category = "dev" optional = false python-versions = "*" [package.dependencies] bleach = ">=2.1.0" docutils = ">=0.13.1" Pygments = ">=2.5.1" six = "*" [package.extras] md = ["cmarkgfm (>=0.5.0,<0.6.0)"] [[package]] name = "regex" version = "2021.7.6" description = "Alternative regular expression module, to replace re." category = "dev" optional = false python-versions = "*" [[package]] name = "requests" version = "2.26.0" description = "Python HTTP for Humans." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "requests-toolbelt" version = "0.9.1" description = "A utility belt for advanced users of python-requests" category = "dev" optional = false python-versions = "*" [package.dependencies] requests = ">=2.0.1,<3.0.0" [[package]] name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" category = "dev" optional = false python-versions = "*" [package.extras] idna2008 = ["idna"] [[package]] name = "rich" version = "10.6.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "dev" optional = false python-versions = ">=3.6,<4.0" [package.dependencies] colorama = ">=0.4.0,<0.5.0" commonmark = ">=0.9.0,<0.10.0" pygments = ">=2.6.0,<3.0.0" typing-extensions = {version = ">=3.7.4,<4.0.0", markers = "python_version < \"3.8\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "secretstorage" version = "3.3.1" description = "Python bindings to FreeDesktop.org Secret Service API" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] cryptography = ">=2.0" jeepney = ">=0.6" [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "sqlparse" version = "0.4.2" description = "A non-validating SQL parser." category = "main" optional = false python-versions = ">=3.5" [[package]] name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" category = "dev" optional = false python-versions = "*" [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" version = "1.0.4" description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "tqdm" version = "4.61.2" description = "Fast, Extensible Progress Meter" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] dev = ["py-make (>=0.1.0)", "twine", "wheel"] notebook = ["ipywidgets (>=6)"] telegram = ["requests"] [[package]] name = "twine" version = "3.4.1" description = "Collection of utilities for publishing packages on PyPI" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] colorama = ">=0.4.3" importlib-metadata = ">=3.6" keyring = ">=15.1" pkginfo = ">=1.4.2" readme-renderer = ">=21.0" requests = ">=2.20" requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" rfc3986 = ">=1.4.0" tqdm = ">=4.14" [[package]] name = "typed-ast" version = "1.4.3" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = "*" [[package]] name = "typing-extensions" version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false python-versions = "*" [[package]] name = "urllib3" version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] 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 = "watchdog" version = "2.1.3" description = "Filesystem events monitoring" category = "dev" optional = false python-versions = ">=3.6" [package.extras] watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] [[package]] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" category = "dev" optional = false python-versions = "*" [[package]] name = "zipp" version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.6" [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)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "^3.7" content-hash = "6aa47dc094d4bde2b0670981e0a8d4f30accd4181d4f35ec1b0051a06aba80e9" [metadata.files] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] asgiref = [ {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] black = [ {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, ] bleach = [ {file = "bleach-3.3.1-py2.py3-none-any.whl", hash = "sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78"}, {file = "bleach-3.3.1.tar.gz", hash = "sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa"}, ] bump2version = [ {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, ] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] cffi = [ {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, ] charset-normalizer = [ {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"}, {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"}, ] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, ] codecov = [ {file = "codecov-2.1.11-py2.py3-none-any.whl", hash = "sha256:ba8553a82942ce37d4da92b70ffd6d54cf635fc1793ab0a7dc3fecd6ebfb3df8"}, {file = "codecov-2.1.11-py3.8.egg", hash = "sha256:e95901d4350e99fc39c8353efa450050d2446c55bac91d90fcfd2354e19a6aef"}, {file = "codecov-2.1.11.tar.gz", hash = "sha256:6cde272454009d27355f9434f4e49f238c0273b216beda8472a65dc4957f473b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] coverage = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] cryptography = [ {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, ] django = [ {file = "Django-3.2.12-py3-none-any.whl", hash = "sha256:9b06c289f9ba3a8abea16c9c9505f25107809fb933676f6c891ded270039d965"}, {file = "Django-3.2.12.tar.gz", hash = "sha256:9772e6935703e59e993960832d66a614cf0233a1c5123bc6224ecc6ad69e41e2"}, ] django-stubs = [ {file = "django-stubs-1.8.0.tar.gz", hash = "sha256:717967d7fee0a6af0746724a0be80d72831a982a40fa8f245a6a46f4cafd157b"}, {file = "django_stubs-1.8.0-py3-none-any.whl", hash = "sha256:bde9e44e3c4574c2454e74a3e607cc3bc23b0441bb7d1312cd677d5e30984b74"}, ] django-stubs-ext = [ {file = "django-stubs-ext-0.2.0.tar.gz", hash = "sha256:c14f297835a42c1122421ec7e2d06579996b29d33b8016002762afa5d78863af"}, {file = "django_stubs_ext-0.2.0-py3-none-any.whl", hash = "sha256:bd4a1e36ef2ba0ef15801933c85c68e59b383302c873795c6ecfc25950c7ecdb"}, ] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] factory-boy = [ {file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"}, {file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"}, ] faker = [ {file = "Faker-9.5.0-py3-none-any.whl", hash = "sha256:3410503876fcd0441c13c6aa66890b61a50e1fe8eb5cdbd18c69dad923bda57a"}, {file = "Faker-9.5.0.tar.gz", hash = "sha256:4650bbd3c3df3a5ad9b2506000589cd7360b3d4ae5553faf89f593d08a590e4c"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] ghp-import = [ {file = "ghp-import-2.0.1.tar.gz", hash = "sha256:753de2eace6e0f7d4edfb3cce5e3c3b98cd52aadb80163303d1d036bda7b4483"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] importlib-metadata = [ {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] jeepney = [ {file = "jeepney-0.7.0-py3-none-any.whl", hash = "sha256:71335e7a4e93817982f473f3507bffc2eff7a544119ab9b73e089c8ba1409ba3"}, {file = "jeepney-0.7.0.tar.gz", hash = "sha256:1237cd64c8f7ac3aa4b3f332c4d0fb4a8216f39eaa662ec904302d4d77de5a54"}, ] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, ] keyring = [ {file = "keyring-23.0.1-py3-none-any.whl", hash = "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48"}, {file = "keyring-23.0.1.tar.gz", hash = "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8"}, ] markdown = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] markupsafe = [ {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] mergedeep = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] mkautodoc = [ {file = "mkautodoc-0.1.0.tar.gz", hash = "sha256:7c2595f40276b356e576ce7e343338f8b4fa1e02ea904edf33fadf82b68ca67c"}, ] mkdocs = [ {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, ] mkdocs-material = [ {file = "mkdocs-material-7.1.10.tar.gz", hash = "sha256:890e9be00bfbe4d22ccccbcde1bf9bad67a3ba495f2a7d2422ea4acb5099f014"}, {file = "mkdocs_material-7.1.10-py2.py3-none-any.whl", hash = "sha256:92ff8c4a8e78555ef7b7ed0ba3043421d18971b48d066ea2cefb50e889fc66db"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, {file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"}, ] mypy = [ {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pkginfo = [ {file = "pkginfo-1.7.1-py2.py3-none-any.whl", hash = "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779"}, {file = "pkginfo-1.7.1.tar.gz", hash = "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] psycopg2 = [ {file = "psycopg2-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854"}, {file = "psycopg2-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56"}, {file = "psycopg2-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188"}, {file = "psycopg2-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25"}, {file = "psycopg2-2.9.1-cp38-cp38-win32.whl", hash = "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa"}, {file = "psycopg2-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62"}, {file = "psycopg2-2.9.1-cp39-cp39-win32.whl", hash = "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb"}, {file = "psycopg2-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf"}, {file = "psycopg2-2.9.1.tar.gz", hash = "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pycparser = [ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] pydantic = [ {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, ] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, ] pymdown-extensions = [ {file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"}, {file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] pytest-django = [ {file = "pytest-django-4.4.0.tar.gz", hash = "sha256:b5171e3798bf7e3fc5ea7072fe87324db67a4dd9f1192b037fed4cc3c1b7f455"}, {file = "pytest_django-4.4.0-py3-none-any.whl", hash = "sha256:65783e78382456528bd9d79a35843adde9e6a47347b20464eb2c885cb0f1f606"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] readme-renderer = [ {file = "readme_renderer-29.0-py2.py3-none-any.whl", hash = "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c"}, {file = "readme_renderer-29.0.tar.gz", hash = "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"}, ] regex = [ {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] rich = [ {file = "rich-10.6.0-py3-none-any.whl", hash = "sha256:d3f72827cd5df13b2ef7f1a97f81ec65548d4fdeb92cef653234f227580bbb2a"}, {file = "rich-10.6.0.tar.gz", hash = "sha256:128261b3e2419a4ef9c97066ccc2abbfb49fa7c5e89c3fe4056d00aa5e9c1e65"}, ] secretstorage = [ {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] sqlparse = [ {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ {file = "tomli-1.0.4-py3-none-any.whl", hash = "sha256:0713b16ff91df8638a6a694e295c8159ab35ba93e3424a626dd5226d386057be"}, {file = "tomli-1.0.4.tar.gz", hash = "sha256:be670d0d8d7570fd0ea0113bd7bb1ba3ac6706b4de062cc4c952769355c9c268"}, ] tqdm = [ {file = "tqdm-4.61.2-py2.py3-none-any.whl", hash = "sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64"}, {file = "tqdm-4.61.2.tar.gz", hash = "sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a"}, ] twine = [ {file = "twine-3.4.1-py3-none-any.whl", hash = "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83"}, {file = "twine-3.4.1.tar.gz", hash = "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b"}, ] typed-ast = [ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] watchdog = [ {file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"}, {file = "watchdog-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acc4e2d5be6f140f02ee8590e51c002829e2c33ee199036fcd61311d558d89f4"}, {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b851237cf3533fabbc034ffcd84d0fa52014b3121454e5f8b86974b531560c"}, {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a12539ecf2478a94e4ba4d13476bb2c7a2e0a2080af2bb37df84d88b1b01358a"}, {file = "watchdog-2.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6fe9c8533e955c6589cfea6f3f0a1a95fb16867a211125236c82e1815932b5d7"}, {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d9456f0433845e7153b102fffeb767bde2406b76042f2216838af3b21707894e"}, {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd8c595d5a93abd441ee7c5bb3ff0d7170e79031520d113d6f401d0cf49d7c8f"}, {file = "watchdog-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0bcfe904c7d404eb6905f7106c54873503b442e8e918cc226e1828f498bdc0ca"}, {file = "watchdog-2.1.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf84bd94cbaad8f6b9cbaeef43080920f4cb0e61ad90af7106b3de402f5fe127"}, {file = "watchdog-2.1.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b8ddb2c9f92e0c686ea77341dcb58216fa5ff7d5f992c7278ee8a392a06e86bb"}, {file = "watchdog-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8805a5f468862daf1e4f4447b0ccf3acaff626eaa57fbb46d7960d1cf09f2e6d"}, {file = "watchdog-2.1.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:3e305ea2757f81d8ebd8559d1a944ed83e3ab1bdf68bcf16ec851b97c08dc035"}, {file = "watchdog-2.1.3-py3-none-manylinux2014_i686.whl", hash = "sha256:431a3ea70b20962e6dee65f0eeecd768cd3085ea613ccb9b53c8969de9f6ebd2"}, {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:e4929ac2aaa2e4f1a30a36751160be391911da463a8799460340901517298b13"}, {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:201cadf0b8c11922f54ec97482f95b2aafca429c4c3a4bb869a14f3c20c32686"}, {file = "watchdog-2.1.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:3a7d242a7963174684206093846537220ee37ba9986b824a326a8bb4ef329a33"}, {file = "watchdog-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:54e057727dd18bd01a3060dbf5104eb5a495ca26316487e0f32a394fd5fe725a"}, {file = "watchdog-2.1.3-py3-none-win32.whl", hash = "sha256:b5fc5c127bad6983eecf1ad117ab3418949f18af9c8758bd10158be3647298a9"}, {file = "watchdog-2.1.3-py3-none-win_amd64.whl", hash = "sha256:44acad6f642996a2b50bb9ce4fb3730dde08f23e79e20cd3d8e2a2076b730381"}, {file = "watchdog-2.1.3-py3-none-win_ia64.whl", hash = "sha256:0bcdf7b99b56a3ae069866c33d247c9994ffde91b620eaf0306b27e099bd1ae0"}, {file = "watchdog-2.1.3.tar.gz", hash = "sha256:e5236a8e8602ab6db4b873664c2d356c365ab3cac96fbdec4970ad616415dd45"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] zipp = [ {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, ] djantic-0.7.0/pyproject.toml000066400000000000000000000014151422577170000160540ustar00rootroot00000000000000[tool.poetry] name = "djantic" version = "0.7.0" description = "Pydantic models for Django" authors = ["Jordan Eremieff "] license = "MIT" [tool.poetry.dependencies] python = "^3.7" pydantic = "^1.8.2" Django = "~3" [tool.poetry.dev-dependencies] black = "^21.7b0" setuptools = "^57.2.0" twine = "^3.4.1" wheel = "^0.36.2" flake8 = "^3.9.2" mypy = "^0.910" pytest = "^6.2.4" pytest-cov = "^2.12.1" codecov = "^2.1.11" mkdocs = "^1.2.3" mkdocs-material = "^7.1.10" mkautodoc = "^0.1.0" Pygments = "^2.9.0" pymdown-extensions = "^8.2" rich = "^10.6.0" django-stubs = "^1.8.0" pytest-django = "^4.4.0" psycopg2 = "^2.9.1" bump2version = "^1.0.1" factory-boy = "^3.2.1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" djantic-0.7.0/setup.cfg000066400000000000000000000017731422577170000147700ustar00rootroot00000000000000[flake8] max-line-length = 88 ignore = E203, W503 exclude = .git .venv __pycache__ .eggs *.egg settings.py [mypy] disallow_any_generics = True disallow_any_explicit = True allow_redefinition = False check_untyped_defs = True disallow_untyped_decorators = False disallow_untyped_calls = True ignore_errors = False ignore_missing_imports = True implicit_reexport = False local_partial_types = True strict_optional = True strict_equality = True no_implicit_optional = True warn_unused_ignores = True warn_redundant_casts = True warn_unused_configs = True warn_unreachable = True warn_no_return = True [tool:pytest] DJANGO_SETTINGS_MODULE = tests.testapp.settings norecursedirs = data static node_modules bin dist build docs .mypy_cache .pytest_cache .secret .txt .idea .git .venv *.egg .eggs .git .github .poetry __pycache__ addopts = -v --durations=10 -p no:logging -s --ignore .venvdjantic-0.7.0/setup.py000066400000000000000000000022021422577170000146450ustar00rootroot00000000000000from setuptools import find_packages, setup __version__ = "0.7.0" def get_long_description(): return open("README.md", "r", encoding="utf8").read() setup( name="djantic", version=__version__, packages=find_packages(), license="MIT", url="https://github.com/jordaneremieff/djantic/", description="Pydantic model support for Django ORM", long_description=get_long_description(), python_requires=">=3.7", package_data={"djantic": ["py.typed"]}, long_description_content_type="text/markdown", author="Jordan Eremieff", author_email="jordan@eremieff.com", classifiers=[ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Framework :: Django", "Framework :: Django :: 3.0", "Framework :: Django :: 3.1", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "Programming Language :: Python", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], ) djantic-0.7.0/tests/000077500000000000000000000000001422577170000143015ustar00rootroot00000000000000djantic-0.7.0/tests/__init__.py000066400000000000000000000000001422577170000164000ustar00rootroot00000000000000djantic-0.7.0/tests/test_fields.py000066400000000000000000000222451422577170000171650ustar00rootroot00000000000000import pytest from testapp.models import Configuration, Listing, Preference, Record, Searchable from djantic import ModelSchema @pytest.mark.django_db def test_unhandled_field_type(): class SearchableSchema(ModelSchema): class Config: model = Searchable assert SearchableSchema.schema() == { "title": "SearchableSchema", "description": "Searchable(id, title, search_vector)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "title": { "title": "Title", "description": "title", "maxLength": 255, "type": "string", }, "search_vector": { "title": "Search Vector", "description": "search_vector", "type": "string", }, }, "required": ["title"], } searchable = Searchable.objects.create(title="My content") assert SearchableSchema.from_django(searchable).dict() == { "id": 1, "title": "My content", "search_vector": None, } @pytest.mark.django_db def test_custom_field(): """ Test a model using custom field subclasses. """ class RecordSchema(ModelSchema): class Config: model = Record include = ["id", "title", "items"] assert RecordSchema.schema() == { "title": "RecordSchema", "description": "A generic record model.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "title": { "title": "Title", "description": "title", "maxLength": 20, "type": "string", }, "items": { "title": "Items", "description": "items", "anyOf": [ {"type": "string", "format": "json-string"}, {"type": "object"}, {"type": "array", "items": {}}, ], }, }, "required": ["title"], } @pytest.mark.django_db def test_postgres_json_field(): """ Test generating a schema for multiple Postgres JSON fields. """ class ConfigurationSchema(ModelSchema): class Config: model = Configuration include = ["permissions", "changelog", "metadata"] assert ConfigurationSchema.schema() == { "title": "ConfigurationSchema", "description": "A configuration container.", "type": "object", "properties": { "permissions": { "title": "Permissions", "description": "permissions", "anyOf": [ {"type": "string", "format": "json-string"}, {"type": "object"}, {"type": "array", "items": {}}, ], }, "changelog": { "title": "Changelog", "description": "changelog", "anyOf": [ {"type": "string", "format": "json-string"}, {"type": "object"}, {"type": "array", "items": {}}, ], }, "metadata": { "title": "Metadata", "description": "metadata", "anyOf": [ {"type": "string", "format": "json-string"}, {"type": "object"}, {"type": "array", "items": {}}, ], }, }, } @pytest.mark.django_db def test_lazy_choice_field(): """ Test generating a dynamic enum choice field. """ class RecordSchema(ModelSchema): class Config: model = Record include = ["record_type", "record_status"] assert RecordSchema.schema() == { "title": "RecordSchema", "description": "A generic record model.", "type": "object", "properties": { "record_type": { "title": "Record Type", "description": "record_type", "default": "NEW", "allOf": [{"$ref": "#/definitions/RecordSchemaRecordTypeEnum"}], }, "record_status": { "title": "Record Status", "description": "record_status", "default": 0, "allOf": [{"$ref": "#/definitions/RecordSchemaRecordStatusEnum"}], }, }, "definitions": { "RecordSchemaRecordTypeEnum": { "title": "RecordSchemaRecordTypeEnum", "description": "An enumeration.", "enum": ["NEW", "OLD"], }, "RecordSchemaRecordStatusEnum": { "title": "RecordSchemaRecordStatusEnum", "description": "An enumeration.", "enum": [0, 1, 2], }, }, } @pytest.mark.django_db def test_enum_choices(): class PreferenceSchema(ModelSchema): class Config: model = Preference use_enum_values = True assert PreferenceSchema.schema() == { "title": "PreferenceSchema", "description": "Preference(id, name, preferred_food, preferred_group, preferred_sport, preferred_musician)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "name": { "title": "Name", "description": "name", "maxLength": 128, "type": "string", }, "preferred_food": { "title": "Preferred Food", "description": "preferred_food", "default": "ba", "allOf": [{"$ref": "#/definitions/PreferenceSchemaPreferredFoodEnum"}], }, "preferred_group": { "title": "Preferred Group", "description": "preferred_group", "default": 1, "allOf": [{"$ref": "#/definitions/PreferenceSchemaPreferredGroupEnum"}], }, "preferred_sport": { "title": "Preferred Sport", "description": "preferred_sport", "allOf": [{"$ref": "#/definitions/PreferenceSchemaPreferredSportEnum"}], }, "preferred_musician": { "allOf": [{"$ref": "#/definitions/PreferenceSchemaPreferredMusicianEnum"}], 'default': '', "description": "preferred_musician", "title": "Preferred Musician" }, }, "required": ["name"], "definitions": { "PreferenceSchemaPreferredFoodEnum": { "title": "PreferenceSchemaPreferredFoodEnum", "description": "An enumeration.", "enum": ["ba", "ap"], }, "PreferenceSchemaPreferredGroupEnum": { "title": "PreferenceSchemaPreferredGroupEnum", "description": "An enumeration.", "enum": [1, 2], }, "PreferenceSchemaPreferredSportEnum": { "title": "PreferenceSchemaPreferredSportEnum", "description": "An enumeration.", "enum": ["football", "basketball", ""], }, "PreferenceSchemaPreferredMusicianEnum": { "title": "PreferenceSchemaPreferredMusicianEnum", "description": "An enumeration.", "enum": ["tom_jobim", "sinatra", ""], } }, } preference = Preference.objects.create(name="Jordan", preferred_sport="", preferred_musician=None) assert PreferenceSchema.from_django(preference).dict() == { "id": 1, "name": "Jordan", "preferred_food": "ba", "preferred_group": 1, "preferred_sport": "", 'preferred_musician': None } @pytest.mark.django_db def test_enum_choices_generates_unique_enums(): class PreferenceSchema(ModelSchema): class Config: model = Preference use_enum_values = True class PreferenceSchema2(ModelSchema): class Config: model = Preference use_enum_values = True assert str(PreferenceSchema2.__fields__["preferred_food"].type_) != str( PreferenceSchema.__fields__["preferred_food"].type_ ) @pytest.mark.django_db def test_listing(): class ListingSchema(ModelSchema): class Config: model = Listing use_enum_values = True assert ListingSchema.schema() == { "title": "ListingSchema", "description": "Listing(id, items)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "items": { "description": "items", "items": {}, "title": "Items", "type": "array", }, }, "required": ["items"], } preference = Listing(items=["a", "b"]) assert ListingSchema.from_django(preference).dict() == { "id": None, "items": ["a", "b"], } djantic-0.7.0/tests/test_files.py000066400000000000000000000024651422577170000170230ustar00rootroot00000000000000from tempfile import NamedTemporaryFile import pytest from testapp.models import Attachment from djantic import ModelSchema @pytest.mark.django_db def test_image_field_schema(): class AttachmentSchema(ModelSchema): class Config: model = Attachment image_file = NamedTemporaryFile(suffix=".jpg") attachment = Attachment.objects.create( description="My image upload", image=image_file.name, ) assert AttachmentSchema.schema() == { "title": "AttachmentSchema", "description": "Attachment(id, description, image)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "description": { "title": "Description", "description": "description", "maxLength": 255, "type": "string", }, "image": { "title": "Image", "description": "image", "maxLength": 100, "type": "string", }, }, "required": ["description"], } assert AttachmentSchema.from_django(attachment).dict() == { "id": attachment.id, "description": attachment.description, "image": attachment.image.name, } djantic-0.7.0/tests/test_main.py000066400000000000000000000027211422577170000166400ustar00rootroot00000000000000import pytest from pydantic import ConfigError from testapp.models import User from djantic import ModelSchema @pytest.mark.django_db def test_config_errors(): """ Test the model config error exceptions. """ with pytest.raises( ConfigError, match="(Is `Config` class defined?)" ): class InvalidModelErrorSchema(ModelSchema): pass with pytest.raises( ConfigError, match="(Is `Config.model` a valid Django model class?)" ): class InvalidModelErrorSchema(ModelSchema): class Config: model = "Ok" with pytest.raises( ConfigError, match="Only one of 'include' or 'exclude' should be set in configuration.", ): class IncludeExcludeErrorSchema(ModelSchema): class Config: model = User include = ["id"] exclude = ["first_name"] @pytest.mark.django_db def test_get_field_names(): """ Test retrieving the field names for a model. """ class UserSchema(ModelSchema): class Config: model = User include = ["id"] assert UserSchema.get_field_names() == ["id"] class UserSchema(ModelSchema): class Config: model = User exclude = ["id"] assert UserSchema.get_field_names() == [ "profile", "first_name", "last_name", "email", "created_at", "updated_at", ] djantic-0.7.0/tests/test_multiple_level_relations.py000066400000000000000000000305601422577170000230200ustar00rootroot00000000000000 from decimal import Decimal from typing import List, Optional import pytest from pydantic import validator from testapp.order import Order, OrderItem, OrderItemDetail, OrderUser, OrderUserFactory, OrderUserProfile from djantic import ModelSchema @pytest.mark.django_db def test_multiple_level_relations(): class OrderItemDetailSchema(ModelSchema): class Config: model = OrderItemDetail class OrderItemSchema(ModelSchema): details: List[OrderItemDetailSchema] class Config: model = OrderItem class OrderSchema(ModelSchema): items: List[OrderItemSchema] class Config: model = Order class OrderUserProfileSchema(ModelSchema): class Config: model = OrderUserProfile class OrderUserSchema(ModelSchema): orders: List[OrderSchema] profile: OrderUserProfileSchema user_cache: Optional[dict] class Config: model = OrderUser include = ('id', 'first_name', 'last_name', 'email', 'profile', 'orders', 'user_cache') @validator('user_cache', pre=True, always=True) def get_user_cache(cls, _): return { 'has_order': True } user = OrderUserFactory.create() assert OrderUserSchema.from_django(user).dict() == { 'id': 1, 'first_name': '', 'last_name': None, 'email': '', 'user_cache': {'has_order': True}, 'profile': { 'id': 1, 'address': '', 'user': 1 }, 'orders': [ { 'id': 1, 'total_price': Decimal('0.00000'), 'shipping_address': '', 'user': 1, 'items': [ { 'id': 1, 'name': '', 'price': Decimal('0.00000'), 'quantity': 0, 'order': 1, 'details': [ { 'id': 1, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 1 }, { 'id': 2, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 1 } ] }, { 'details': [ { 'id': 3, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 2 }, { 'id': 4, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 2 } ], 'id': 2, 'name': '', 'price': Decimal('0.00000'), 'quantity': 0, 'order': 1 } ], }, { 'id': 2, 'total_price': Decimal('0.00000'), 'shipping_address': '', 'user': 1, 'items': [ { 'id': 3, 'name': '', 'price': Decimal('0.00000'), 'quantity': 0, 'order': 2, 'details': [ { 'id': 5, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 3}, { 'id': 6, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 3} ] }, { 'id': 4, 'name': '', 'price': Decimal('0.00000'), 'quantity': 0, 'order': 2, 'details': [ { 'id': 7, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 4}, { 'id': 8, 'name': '', 'value': 0, 'quantity': 0, 'order_item': 4}] } ] } ] } assert OrderUserSchema.schema() == { "title": "OrderUserSchema", "description": "OrderUser(id, first_name, last_name, email)", "type": "object", "properties": { "profile": { "$ref": "#/definitions/OrderUserProfileSchema" }, "orders": { "title": "Orders", "type": "array", "items": { "$ref": "#/definitions/OrderSchema" } }, "id": { "title": "Id", "description": "id", "type": "integer" }, "first_name": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string" }, "last_name": { "title": "Last Name", "description": "last_name", "maxLength": 50, "type": "string" }, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string" }, "user_cache": { "title": "User Cache", "type": "object" } }, "required": [ "profile", "orders", "first_name", "email" ], "definitions": { "OrderUserProfileSchema": { "title": "OrderUserProfileSchema", "description": "OrderUserProfile(id, address, user)", "type": "object", "properties": { "id": { "title": "Id", "description": "id", "type": "integer" }, "address": { "title": "Address", "description": "address", "maxLength": 255, "type": "string" }, "user": { "title": "User", "description": "id", "type": "integer" } }, "required": [ "address", "user" ] }, "OrderItemDetailSchema": { "title": "OrderItemDetailSchema", "description": "OrderItemDetail(id, name, value, quantity, order_item)", "type": "object", "properties": { "id": { "title": "Id", "description": "id", "type": "integer" }, "name": { "title": "Name", "description": "name", "maxLength": 30, "type": "string" }, "value": { "title": "Value", "description": "value", "default": 0, "type": "integer" }, "quantity": { "title": "Quantity", "description": "quantity", "default": 0, "type": "integer" }, "order_item": { "title": "Order Item", "description": "id", "type": "integer" } }, "required": [ "name", "order_item" ] }, "OrderItemSchema": { "title": "OrderItemSchema", "description": "OrderItem(id, name, price, quantity, order)", "type": "object", "properties": { "details": { "title": "Details", "type": "array", "items": { "$ref": "#/definitions/OrderItemDetailSchema" } }, "id": { "title": "Id", "description": "id", "type": "integer" }, "name": { "title": "Name", "description": "name", "maxLength": 30, "type": "string" }, "price": { "title": "Price", "description": "price", "default": 0, "type": "number" }, "quantity": { "title": "Quantity", "description": "quantity", "default": 0, "type": "integer" }, "order": { "title": "Order", "description": "id", "type": "integer" } }, "required": [ "details", "name", "order" ] }, "OrderSchema": { "title": "OrderSchema", "description": "Order(id, total_price, shipping_address, user)", "type": "object", "properties": { "items": { "title": "Items", "type": "array", "items": { "$ref": "#/definitions/OrderItemSchema" } }, "id": { "title": "Id", "description": "id", "type": "integer" }, "total_price": { "title": "Total Price", "description": "total_price", "default": 0, "type": "number" }, "shipping_address": { "title": "Shipping Address", "description": "shipping_address", "maxLength": 255, "type": "string" }, "user": { "title": "User", "description": "id", "type": "integer" } }, "required": [ "items", "shipping_address", "user" ] } } } djantic-0.7.0/tests/test_queries.py000066400000000000000000000162421422577170000173740ustar00rootroot00000000000000from typing import List import pytest from testapp.models import Bookmark, Message, Profile, Tagged, Thread, User from djantic import ModelSchema @pytest.mark.django_db def test_get_instance(): """ Test retrieving an existing Django object to populate the schema model. """ user = User.objects.create( first_name="Jordan", last_name="Eremieff", email="jordan@eremieff.com" ) class UserSchema(ModelSchema): class Config: model = User include = ["id", "first_name"] assert UserSchema.from_django(user).dict() == {"first_name": "Jordan", "id": 1} @pytest.mark.django_db def test_get_instance_with_generic_foreign_key(): bookmark = Bookmark.objects.create(url="https://www.djangoproject.com/") Tagged.objects.create(content_object=bookmark, slug="django") class TaggedSchema(ModelSchema): class Config: model = Tagged class BookmarkWithTaggedSchema(ModelSchema): tags: List[TaggedSchema] class Config: model = Bookmark bookmark_with_tagged_schema = BookmarkWithTaggedSchema.from_django(bookmark) assert bookmark_with_tagged_schema.dict() == { "id": 1, "tags": [ { 'content_object': 1, "content_type": 20, "id": 1, "object_id": 1, "slug": "django", } ], "url": "https://www.djangoproject.com/", } @pytest.mark.django_db def test_get_queryset_with_reverse_one_to_one(): """ Test retrieving a Django queryset with reverse one-to-one relationships. """ user_data = [ {"first_name": "Jordan", "email": "jordan@eremieff.com"}, {"first_name": "Sara", "email": "sara@example.com"}, ] for kwargs in user_data: user = User.objects.create(**kwargs) Profile.objects.create(user=user, location="Australia") class UserSchema(ModelSchema): class Config: model = User include = ["id", "email", "first_name", "profile"] users = User.objects.all() user_schema_qs = UserSchema.from_django(users, many=True) assert user_schema_qs == [ { "email": "jordan@eremieff.com", "first_name": "Jordan", "id": 1, "profile": 1, }, {"email": "sara@example.com", "first_name": "Sara", "id": 2, "profile": 2}, ] # Test when using a declared sub-model class ProfileSchema(ModelSchema): class Config: model = Profile include = ["id", "location"] class UserWithProfileSchema(ModelSchema): profile: ProfileSchema class Config: model = User exclude = ["created_at", "updated_at", "last_name"] users = User.objects.all() user_with_profile_schema_qs = UserWithProfileSchema.from_django(users, many=True) assert user_with_profile_schema_qs == [ { "email": "jordan@eremieff.com", "first_name": "Jordan", "id": 1, "profile": {"id": 1, "location": "Australia"}, }, { "email": "sara@example.com", "first_name": "Sara", "id": 2, "profile": {"id": 2, "location": "Australia"}, }, ] @pytest.mark.django_db def test_get_queryset_with_foreign_key(): """ Test retrieving a Django queryset with foreign-key relationships. """ thread = Thread.objects.create(title="My thread topic") thread2 = Thread.objects.create(title="Another topic") for content in ("I agree.", "I disagree!", "lol"): message_one = Message.objects.create(content=content, thread=thread) Message.objects.create(content=content, thread=thread2) class MessageSchema(ModelSchema): class Config: model = Message exclude = ["created_at"] schema = MessageSchema.from_django(message_one) assert schema.dict() == {"id": 5, "content": "lol", "thread": 1} class ThreadSchema(ModelSchema): class Config: model = Thread exclude = ["created_at"] class MessageWithThreadSchema(ModelSchema): thread: ThreadSchema class Config: model = Message exclude = ["created_at"] schema = MessageWithThreadSchema.from_django(message_one) assert schema.dict() == { "id": 5, "content": "lol", "thread": { "messages": [{"id": 1}, {"id": 3}, {"id": 5}], "id": 1, "title": "My thread topic", }, } @pytest.mark.django_db def test_get_queryset_with_reverse_foreign_key(): """ Test retrieving a Django queryset with reverse foreign-key relationships. """ thread = Thread.objects.create(title="My thread topic") thread2 = Thread.objects.create(title="Another topic") for content in ("I agree.", "I disagree!", "lol"): Message.objects.create(content=content, thread=thread) Message.objects.create(content=content, thread=thread2) threads = Thread.objects.all() class MessageSchema(ModelSchema): class Config: model = Message include = ["id", "content"] class ThreadSchema(ModelSchema): class Config: model = Thread thread_schema_qs = ThreadSchema.from_django(threads, many=True) thread_schemas = [t.dict() for t in thread_schema_qs] assert thread_schemas == [ { "messages": [{"id": 2}, {"id": 4}, {"id": 6}], "id": 2, "title": "Another topic", }, { "messages": [{"id": 1}, {"id": 3}, {"id": 5}], "id": 1, "title": "My thread topic", }, ] # Test when using a declared sub-model class ThreadWithMessageListSchema(ModelSchema): messages: List[MessageSchema] class Config: model = Thread exclude = ["created_at", "updated_at"] thread_with_message_list_schema_qs = ThreadWithMessageListSchema.from_django( threads, many=True ) assert thread_with_message_list_schema_qs == [ { "messages": [ {"id": 2, "content": "I agree."}, {"id": 4, "content": "I disagree!"}, {"id": 6, "content": "lol"}, ], "id": 2, "title": "Another topic", }, { "messages": [ {"id": 1, "content": "I agree."}, {"id": 3, "content": "I disagree!"}, {"id": 5, "content": "lol"}, ], "id": 1, "title": "My thread topic", }, ] @pytest.mark.django_db def test_get_queryset_with_generic_foreign_key(): bookmark = Bookmark.objects.create(url="https://github.com") bookmark.tags.create(slug="tag-1") bookmark.tags.create(slug="tag-2") class TaggedSchema(ModelSchema): class Config: model = Tagged class BookmarkSchema(ModelSchema): class Config: model = Bookmark schema = BookmarkSchema.from_django(bookmark) schema.dict() == { "id": 1, "url": "https://github.com", "tags": [{"id": 1}, {"id": 2}], } djantic-0.7.0/tests/test_relations.py000066400000000000000000000714651422577170000177270ustar00rootroot00000000000000import datetime from typing import Dict, List, Optional import pytest from pydantic import Field from testapp.models import ( Article, Bookmark, Case, Expert, Item, Message, Profile, Publication, Tagged, Thread, User ) from djantic import ModelSchema @pytest.mark.django_db def test_m2m(): """ Test forward m2m relationships. """ class ArticleSchema(ModelSchema): class Config: model = Article assert ArticleSchema.schema() == { "title": "ArticleSchema", "description": "A news article.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "headline": { "title": "Headline", "description": "headline", "maxLength": 100, "type": "string", }, "pub_date": { "title": "Pub Date", "description": "pub_date", "type": "string", "format": "date", }, "publications": { "title": "Publications", "description": "id", "type": "array", "items": { "type": "object", "additionalProperties": {"type": "integer"}, }, }, }, "required": ["headline", "pub_date", "publications"], } class PublicationSchema(ModelSchema): class Config: model = Publication class ArticleWithPublicationListSchema(ModelSchema): publications: List[PublicationSchema] class Config: model = Article assert ArticleWithPublicationListSchema.schema() == { "title": "ArticleWithPublicationListSchema", "description": "A news article.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "headline": { "title": "Headline", "description": "headline", "maxLength": 100, "type": "string", }, "pub_date": { "title": "Pub Date", "description": "pub_date", "type": "string", "format": "date", }, "publications": { "title": "Publications", "type": "array", "items": {"$ref": "#/definitions/PublicationSchema"}, }, }, "required": ["headline", "pub_date", "publications"], "definitions": { "PublicationSchema": { "title": "PublicationSchema", "description": "A news publication.", "type": "object", "properties": { "article_set": { "title": "Article Set", "description": "id", "type": "array", "items": { "type": "object", "additionalProperties": {"type": "integer"}, }, }, "id": {"title": "Id", "description": "id", "type": "integer"}, "title": { "title": "Title", "description": "title", "maxLength": 30, "type": "string", }, }, "required": ["title"], } }, } article = Article.objects.create( headline="My Headline", pub_date=datetime.date(2021, 3, 20) ) publication = Publication.objects.create(title="My Publication") article.publications.add(publication) schema = ArticleWithPublicationListSchema.from_django(article) assert schema.dict() == { "id": 1, "headline": "My Headline", "pub_date": datetime.date(2021, 3, 20), "publications": [{"article_set": [{'id': 1}], "id": 1, "title": "My Publication"}], } @pytest.mark.django_db def test_foreign_key(): """ Test forward foreign-key relationships. """ class ThreadSchema(ModelSchema): class Config: model = Thread class MessageSchema(ModelSchema): class Config: model = Message assert MessageSchema.schema() == { "title": "MessageSchema", "description": "A message posted in a thread.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "content": {"title": "Content", "description": "content", "type": "string"}, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "thread": {"title": "Thread", "description": "id", "type": "integer"}, }, "required": ["content", "created_at", "thread"], } class MessageWithThreadSchema(ModelSchema): thread: ThreadSchema class Config: model = Message assert MessageWithThreadSchema.schema() == { "title": "MessageWithThreadSchema", "description": "A message posted in a thread.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "content": {"title": "Content", "description": "content", "type": "string"}, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "thread": {"$ref": "#/definitions/ThreadSchema"}, }, "required": ["content", "created_at", "thread"], "definitions": { "ThreadSchema": { "title": "ThreadSchema", "description": "A thread of messages.", "type": "object", "properties": { "messages": { "title": "Messages", "description": "id", "type": "array", "items": { "type": "object", "additionalProperties": {"type": "integer"}, }, }, "id": {"title": "Id", "description": "id", "type": "integer"}, "title": { "title": "Title", "description": "title", "maxLength": 30, "type": "string", }, }, "required": ["title"], } }, } class ThreadWithMessageListSchema(ModelSchema): messages: List[MessageSchema] class Config: model = Thread assert ThreadWithMessageListSchema.schema() == { "title": "ThreadWithMessageListSchema", "description": "A thread of messages.", "type": "object", "properties": { "messages": { "title": "Messages", "type": "array", "items": {"$ref": "#/definitions/MessageSchema"}, }, "id": {"title": "Id", "description": "id", "type": "integer"}, "title": { "title": "Title", "description": "title", "maxLength": 30, "type": "string", }, }, "required": ["messages", "title"], "definitions": { "MessageSchema": { "title": "MessageSchema", "description": "A message posted in a thread.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "content": { "title": "Content", "description": "content", "type": "string", }, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "thread": { "title": "Thread", "description": "id", "type": "integer", }, }, "required": ["content", "created_at", "thread"], } }, } @pytest.mark.django_db def test_one_to_one(): """ Test forward one-to-one relationships. """ class UserSchema(ModelSchema): class Config: model = User class ProfileSchema(ModelSchema): class Config: model = Profile assert ProfileSchema.schema() == { "title": "ProfileSchema", "description": "A user's profile.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "user": {"title": "User", "description": "id", "type": "integer"}, "website": { "title": "Website", "description": "website", "default": "", "maxLength": 200, "type": "string", }, "location": { "title": "Location", "description": "location", "default": "", "maxLength": 100, "type": "string", }, }, "required": ["user"], } class ProfileWithUserSchema(ModelSchema): user: UserSchema class Config: model = Profile assert ProfileWithUserSchema.schema() == { "title": "ProfileWithUserSchema", "description": "A user's profile.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "user": {"$ref": "#/definitions/UserSchema"}, "website": { "title": "Website", "description": "website", "default": "", "maxLength": 200, "type": "string", }, "location": { "title": "Location", "description": "location", "default": "", "maxLength": 100, "type": "string", }, }, "required": ["user"], "definitions": { "UserSchema": { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "profile": { "title": "Profile", "description": "id", "type": "integer", }, "id": {"title": "Id", "description": "id", "type": "integer"}, "first_name": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string", }, "last_name": { "title": "Last Name", "description": "last_name", "maxLength": 50, "type": "string", }, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "updated_at": { "title": "Updated At", "description": "updated_at", "type": "string", "format": "date-time", }, }, "required": ["first_name", "email", "created_at", "updated_at"], } }, } @pytest.mark.django_db def test_one_to_one_reverse(): """ Test reverse one-to-one relationships. """ class ProfileSchema(ModelSchema): class Config: model = Profile class UserSchema(ModelSchema): class Config: model = User assert ProfileSchema.schema() == { "title": "ProfileSchema", "description": "A user's profile.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "user": {"title": "User", "description": "id", "type": "integer"}, "website": { "title": "Website", "description": "website", "default": "", "maxLength": 200, "type": "string", }, "location": { "title": "Location", "description": "location", "default": "", "maxLength": 100, "type": "string", }, }, "required": ["user"], } class UserWithProfileSchema(ModelSchema): profile: ProfileSchema class Config: model = User assert UserWithProfileSchema.schema() == { "title": "UserWithProfileSchema", "description": "A user of the application.", "type": "object", "properties": { "profile": {"$ref": "#/definitions/ProfileSchema"}, "id": {"title": "Id", "description": "id", "type": "integer"}, "first_name": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string", }, "last_name": { "title": "Last Name", "description": "last_name", "maxLength": 50, "type": "string", }, "email": { "title": "Email", "description": "email", "maxLength": 254, "type": "string", }, "created_at": { "title": "Created At", "description": "created_at", "type": "string", "format": "date-time", }, "updated_at": { "title": "Updated At", "description": "updated_at", "type": "string", "format": "date-time", }, }, "required": ["profile", "first_name", "email", "created_at", "updated_at"], "definitions": { "ProfileSchema": { "title": "ProfileSchema", "description": "A user's profile.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "user": {"title": "User", "description": "id", "type": "integer"}, "website": { "title": "Website", "description": "website", "default": "", "maxLength": 200, "type": "string", }, "location": { "title": "Location", "description": "location", "default": "", "maxLength": 100, "type": "string", }, }, "required": ["user"], } }, } @pytest.mark.django_db def test_generic_relation(): """ Test generic foreign-key relationships. """ class TaggedSchema(ModelSchema): class Config: model = Tagged assert TaggedSchema.schema() == { "title": "TaggedSchema", "description": "Tagged(id, slug, content_type, object_id)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "slug": { "title": "Slug", "description": "slug", "maxLength": 50, "type": "string", }, "content_type": { "title": "Content Type", "description": "id", "type": "integer", }, "object_id": { "title": "Object Id", "description": "object_id", "type": "integer", }, "content_object": { "title": "Content Object", "description": "content_object", "type": "integer", }, }, "required": ["slug", "content_type", "object_id", "content_object"], } class BookmarkSchema(ModelSchema): # FIXME: I added this because for some reason in 2.2 the GenericRelation field # ends up required, but in 3 it does not. tags: List[Dict[str, int]] = None class Config: model = Bookmark assert BookmarkSchema.schema() == { "title": "BookmarkSchema", "description": "Bookmark(id, url)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "url": { "title": "Url", "description": "url", "maxLength": 200, "type": "string", }, "tags": { "title": "Tags", "type": "array", "items": { "type": "object", "additionalProperties": {"type": "integer"}, }, }, }, "required": ["url"], } class BookmarkWithTaggedSchema(ModelSchema): tags: List[TaggedSchema] class Config: model = Bookmark assert BookmarkWithTaggedSchema.schema() == { "title": "BookmarkWithTaggedSchema", "description": "Bookmark(id, url)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "url": { "title": "Url", "description": "url", "maxLength": 200, "type": "string", }, "tags": { "title": "Tags", "type": "array", "items": {"$ref": "#/definitions/TaggedSchema"}, }, }, "required": ["url", "tags"], "definitions": { "TaggedSchema": { "title": "TaggedSchema", "description": "Tagged(id, slug, content_type, object_id)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "slug": { "title": "Slug", "description": "slug", "maxLength": 50, "type": "string", }, "content_type": { "title": "Content Type", "description": "id", "type": "integer", }, "object_id": { "title": "Object Id", "description": "object_id", "type": "integer", }, "content_object": { "title": "Content Object", "description": "content_object", "type": "integer", }, }, "required": ["slug", "content_type", "object_id", "content_object"], } }, } class ItemSchema(ModelSchema): tags: List[TaggedSchema] class Config: model = Item # Test without defining a GenericRelation on the model assert ItemSchema.schema() == { "title": "ItemSchema", "description": "Item(id, name, item_list)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "name": { "title": "Name", "description": "name", "maxLength": 100, "type": "string", }, "item_list": { "title": "Item List", "description": "id", "type": "integer", }, "tags": { "title": "Tags", "type": "array", "items": {"$ref": "#/definitions/TaggedSchema"}, }, }, "required": ["name", "item_list", "tags"], "definitions": { "TaggedSchema": { "title": "TaggedSchema", "description": "Tagged(id, slug, content_type, object_id)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "slug": { "title": "Slug", "description": "slug", "maxLength": 50, "type": "string", }, "content_type": { "title": "Content Type", "description": "id", "type": "integer", }, "object_id": { "title": "Object Id", "description": "object_id", "type": "integer", }, "content_object": { "title": "Content Object", "description": "content_object", "type": "integer", }, }, "required": ["slug", "content_type", "object_id", "content_object"], } }, } @pytest.mark.django_db def test_m2m_reverse(): class ExpertSchema(ModelSchema): class Config: model = Expert class CaseSchema(ModelSchema): class Config: model = Case assert ExpertSchema.schema() == { "title": "ExpertSchema", "description": "Expert(id, name)", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "name": { "title": "Name", "description": "name", "maxLength": 128, "type": "string", }, "cases": { "title": "Cases", "description": "id", "type": "array", "items": { "type": "object", "additionalProperties": {"type": "integer"}, }, }, }, "required": ["name", "cases"], } assert CaseSchema.schema() == { "title": "CaseSchema", "description": "Case(id, name, details)", "type": "object", "properties": { "related_experts": { "title": "Related Experts", "description": "id", "type": "array", "items": { "type": "object", "additionalProperties": {"type": "integer"}, }, }, "id": {"title": "Id", "description": "id", "type": "integer"}, "name": { "title": "Name", "description": "name", "maxLength": 128, "type": "string", }, "details": {"title": "Details", "description": "details", "type": "string"}, }, "required": ["name", "details"], } case = Case.objects.create(name="My Case", details="Some text data.") expert = Expert.objects.create(name="My Expert") case_schema = CaseSchema.from_django(case) expert_schema = ExpertSchema.from_django(expert) assert case_schema.dict() == { "related_experts": [], "id": 1, "name": "My Case", "details": "Some text data.", } assert expert_schema.dict() == {"id": 1, "name": "My Expert", "cases": []} expert.cases.add(case) case_schema = CaseSchema.from_django(case) expert_schema = ExpertSchema.from_django(expert) assert case_schema.dict() == { "related_experts": [{"id": 1}], "id": 1, "name": "My Case", "details": "Some text data.", } assert expert_schema.dict() == {"id": 1, "name": "My Expert", "cases": [{"id": 1}]} class CustomExpertSchema(ModelSchema): """Custom schema""" name: Optional[str] class Config: model = Expert class CaseSchema(ModelSchema): related_experts: List[CustomExpertSchema] class Config: model = Case assert CaseSchema.schema() == { "title": "CaseSchema", "description": "Case(id, name, details)", "type": "object", "properties": { "related_experts": { "title": "Related Experts", "type": "array", "items": {"$ref": "#/definitions/CustomExpertSchema"}, }, "id": {"title": "Id", "description": "id", "type": "integer"}, "name": { "title": "Name", "description": "name", "maxLength": 128, "type": "string", }, "details": {"title": "Details", "description": "details", "type": "string"}, }, "required": ["related_experts", "name", "details"], "definitions": { "CustomExpertSchema": { "title": "CustomExpertSchema", "description": "Custom schema", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "cases": { "title": "Cases", "description": "id", "type": "array", "items": { "type": "object", "additionalProperties": {"type": "integer"}, }, }, }, "required": ["cases"], } }, } case_schema = CaseSchema.from_django(case) assert case_schema.dict() == { "related_experts": [{"id": 1, "name": "My Expert", "cases": [{'id': 1}]}], "id": 1, "name": "My Case", "details": "Some text data.", } @pytest.mark.django_db def test_alias(): class ProfileSchema(ModelSchema): first_name: str = Field(alias='user__first_name') class Config: model = Profile assert ProfileSchema.schema() == { 'title': 'ProfileSchema', 'description': "A user's profile.", 'type': 'object', 'properties': { 'id': { 'title': 'Id', 'description': 'id', 'type': 'integer' }, 'user': { 'title': 'User', 'description': 'id', 'type': 'integer' }, 'website': { 'title': 'Website', 'description': 'website', 'default': '', 'maxLength': 200, 'type': 'string' }, 'location': { 'title': 'Location', 'description': 'location', 'default': '', 'maxLength': 100, 'type': 'string' }, 'user__first_name': { 'title': 'User First Name', 'type': 'string' } }, 'required': ['user', 'user__first_name'] } user = User.objects.create(first_name="Jack") profile = Profile.objects.create( user=user, website='www.github.com', location='Europe') assert ProfileSchema.from_django(profile).dict() == {'first_name': 'Jack', 'id': 1, 'location': 'Europe', 'user': 1, 'website': 'www.github.com'} djantic-0.7.0/tests/test_schemas.py000066400000000000000000000234041422577170000173400ustar00rootroot00000000000000import datetime from typing import Optional import pytest from pydantic import BaseModel, Field from testapp.models import User, Profile, Configuration from djantic import ModelSchema @pytest.mark.django_db def test_description(): """ Test setting the schema description to the docstring of the Pydantic model. """ class ProfileSchema(ModelSchema): """ Pydantic profile docstring. """ class Config: model = Profile assert ProfileSchema.schema()["description"] == "Pydantic profile docstring." class UserSchema(ModelSchema): """ Pydantic user docstring. """ class Config: model = User assert UserSchema.schema()["description"] == "Pydantic user docstring." # Default will be the model docstring class UserSchema(ModelSchema): class Config: model = User assert UserSchema.schema()["description"] == "A user of the application." @pytest.mark.django_db def test_cache(): """ Test the schema cache. """ class UserSchema(ModelSchema): class Config: model = User include = ["id", "first_name"] expected = { "title": "UserSchema", "description": "A user of the application.", "type": "object", "properties": { "id": {"title": "Id", "description": "id", "type": "integer"}, "first_name": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string", }, }, "required": ["first_name"], } assert True not in UserSchema.__schema_cache__ assert False not in UserSchema.__schema_cache__ assert UserSchema.schema() == expected assert UserSchema.__schema_cache__.keys() == {(True, "#/definitions/{model}")} assert UserSchema.schema() == expected @pytest.mark.django_db def test_include_exclude(): """ Test include and exclude rules in the model config. """ all_user_fields = [field.name for field in User._meta.get_fields()] class UserSchema(ModelSchema): """ All fields are included by default. """ class Config: model = User assert set(UserSchema.schema()["properties"].keys()) == set(all_user_fields) class UserSchema(ModelSchema): """ All fields are included explicitly. """ class Config: model = User assert set(UserSchema.schema()["properties"].keys()) == set(all_user_fields) class UserSchema(ModelSchema): """ Only 'first_name' and 'email' are included. """ last_name: str # Fields annotations follow the same config rules class Config: model = User include = ["first_name", "email"] included = UserSchema.schema()["properties"].keys() assert set(included) == set(UserSchema.__config__.include) assert set(included) == set(["first_name", "email"]) class UserSchema(ModelSchema): """ Only 'id' and 'profile' are not excluded. """ first_name: str last_name: str class Config: model = User exclude = ["first_name", "last_name", "email", "created_at", "updated_at"] not_excluded = UserSchema.schema()["properties"].keys() assert set(not_excluded) == set( [ field for field in all_user_fields if field not in UserSchema.__config__.exclude ] ) assert set(not_excluded) == set(["profile", "id"]) @pytest.mark.django_db def test_annotations(): """ Test annotating fields. """ class UserSchema(ModelSchema): """ Test required, optional, and function fields. 'first_name' is required in Django model, but optional in schema 'last_name' is optional in Django model, but required in schema """ first_name: Optional[str] last_name: str class Config: model = User include = ["first_name", "last_name"] assert UserSchema.schema()["required"] == ["last_name"] updated_at_dt = datetime.datetime(2020, 12, 31, 0, 0) class UserSchema(ModelSchema): """ Test field functions and factory defaults. """ first_name: str = Field(default="Hello") last_name: str = Field(..., min_length=1, max_length=50) email: str = Field(default_factory=lambda: "jordan@eremieff.com") created_at: datetime.datetime = Field(default_factory=datetime.datetime.now) updated_at: datetime.datetime = updated_at_dt class Config: model = User schema = UserSchema.schema() props = schema["properties"] assert "default" in props["created_at"] assert props["email"]["default"] == "jordan@eremieff.com" assert props["first_name"]["default"] == "Hello" assert props["updated_at"]["default"] == updated_at_dt.strftime("%Y-%m-%dT00:00:00") assert set(schema["required"]) == set(["last_name"]) def test_by_alias_generator(): class UserSchema(ModelSchema): """ Test alias generator. """ class Config: model = User include = ["first_name", "last_name"] @staticmethod def alias_generator(x): return x.upper() assert UserSchema.schema() == { "title": "UserSchema", "description": "Test alias generator.", "type": "object", "properties": { "FIRST_NAME": { "title": "First Name", "description": "first_name", "maxLength": 50, "type": "string", }, "LAST_NAME": { "title": "Last Name", "description": "last_name", "maxLength": 50, "type": "string", }, }, "required": ["FIRST_NAME"], } assert set(UserSchema.schema()["properties"].keys()) == set( ["FIRST_NAME", "LAST_NAME"] ) assert set(UserSchema.schema(by_alias=False)["properties"].keys()) == set( ["first_name", "last_name"] ) def test_sub_model(): """ Test compatability with normal Pydantic models. """ class SignUp(BaseModel): """ Pydantic model as the sub-model. """ referral_code: Optional[str] class ProfileSchema(ModelSchema): """ Django model relation as a sub-model. """ class Config: model = Profile include = ["id"] class UserSchema(ModelSchema): sign_up: SignUp profile: ProfileSchema class Config: model = User include = ["id", "sign_up", "profile"] assert set(UserSchema.schema()["definitions"].keys()) == set( ["ProfileSchema", "SignUp"] ) class Notification(BaseModel): """ Pydantic model as the main model. """ user: UserSchema content: str sent_at: datetime.datetime = Field(default_factory=datetime.datetime.now) assert set(Notification.schema()["properties"].keys()) == set( ["user", "content", "sent_at"] ) assert set(Notification.schema()["definitions"].keys()) == set( ["ProfileSchema", "SignUp", "UserSchema"] ) @pytest.mark.django_db def test_json(): class ConfigurationSchema(ModelSchema): """ Test JSON schema. """ class Config: model = Configuration expected = """{ "title": "ConfigurationSchema", "description": "Test JSON schema.", "type": "object", "properties": { "id": { "title": "Id", "description": "id", "type": "integer" }, "config_id": { "title": "Config Id", "description": "Unique id of the configuration.", "type": "string", "format": "uuid" }, "name": { "title": "Name", "description": "name", "maxLength": 100, "type": "string" }, "permissions": { "title": "Permissions", "description": "permissions", "anyOf": [ { "type": "string", "format": "json-string" }, { "type": "object" }, { "type": "array", "items": {} } ] }, "changelog": { "title": "Changelog", "description": "changelog", "anyOf": [ { "type": "string", "format": "json-string" }, { "type": "object" }, { "type": "array", "items": {} } ] }, "metadata": { "title": "Metadata", "description": "metadata", "anyOf": [ { "type": "string", "format": "json-string" }, { "type": "object" }, { "type": "array", "items": {} } ] }, "version": { "title": "Version", "description": "version", "default": "0.0.1", "maxLength": 5, "type": "string" } }, "required": [ "name" ] }""" assert ConfigurationSchema.schema_json(indent=2) == expected @pytest.mark.django_db def test_include_from_annotations(): """ Test include="__annotations__" config. """ class ProfileSchema(ModelSchema): website: str class Config: model = Profile include = "__annotations__" assert ProfileSchema.schema() == { "title": "ProfileSchema", "description": "A user's profile.", "type": "object", "properties": { "website": { "title": "Website", "type": "string" } }, "required": [ "website" ] } djantic-0.7.0/tests/testapp/000077500000000000000000000000001422577170000157615ustar00rootroot00000000000000djantic-0.7.0/tests/testapp/__init__.py000066400000000000000000000000001422577170000200600ustar00rootroot00000000000000djantic-0.7.0/tests/testapp/apps.py000066400000000000000000000001311422577170000172710ustar00rootroot00000000000000from django.apps import AppConfig class TestAppConfig(AppConfig): name = "testapp" djantic-0.7.0/tests/testapp/fields.py000066400000000000000000000012101422577170000175730ustar00rootroot00000000000000from django.contrib.postgres.fields import JSONField from django.db import models class CustomFieldMixin: pass class RestrictedCharField(models.CharField): def __init__(self, *args, **kwargs): kwargs["max_length"] = 20 super().__init__(*args, **kwargs) class NotNullRestrictedCharField(RestrictedCharField): def __init__(self, *args, **kwargs): kwargs["null"] = False kwargs["blank"] = False super().__init__(*args, **kwargs) class ListField(CustomFieldMixin, JSONField): def __init__(self, *args, **kwargs): kwargs["default"] = list super().__init__(*args, **kwargs) djantic-0.7.0/tests/testapp/manage.py000077500000000000000000000011631422577170000175670ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() djantic-0.7.0/tests/testapp/models.py000066400000000000000000000156231422577170000176250ustar00rootroot00000000000000import uuid import os.path from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils.text import slugify from django.contrib.postgres.fields import JSONField, ArrayField from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVectorField from django.utils.translation import gettext_lazy as _ from .fields import ListField, NotNullRestrictedCharField class Thread(models.Model): """ A thread of messages. """ title = models.CharField(max_length=30) class Meta: ordering = ["title"] def __str__(self): return self.title class Message(models.Model): """ A message posted in a thread. """ content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) thread = models.ForeignKey( Thread, on_delete=models.CASCADE, related_name="messages" ) def __str__(self): return f"Message created in {self.thread} @ {self.created_at.isoformat()}" class Publication(models.Model): """ A news publication. """ title = models.CharField(max_length=30) class Meta: ordering = ["title"] def __str__(self): return self.title class Article(models.Model): """ A news article. """ headline = models.CharField(max_length=100) pub_date = models.DateField() publications = models.ManyToManyField(Publication) class Meta: ordering = ["headline"] def __str__(self): return self.headline class Group(models.Model): """ A group of users. """ title = models.TextField() slug = models.SlugField(unique=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): self.slug = slugify(self.title, self.created_at) super().save(*args, **kwargs) class User(models.Model): """ A user of the application. """ first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50, null=True, blank=True) email = models.EmailField(unique=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Profile(models.Model): """ A user's profile. """ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") website = models.URLField(default="", blank=True) location = models.CharField(max_length=100, default="", blank=True) class Configuration(models.Model): """ A configuration container. """ config_id = models.UUIDField( default=uuid.uuid4, help_text=_("Unique id of the configuration.") ) name = models.CharField(max_length=100) permissions = JSONField(default=dict, blank=True) changelog = JSONField(default=list, blank=True) metadata = JSONField(blank=True) version = models.CharField(default="0.0.1", max_length=5) class RequestLog(models.Model): """ A log entry for a server request. """ request_id = models.UUIDField( default=uuid.uuid4, help_text=_("Unique id of the request.") ) response_time = models.DurationField() ip_address = models.GenericIPAddressField(blank=True) host_ipv4_address = models.GenericIPAddressField(protocol="ipv4", blank=True) host_ipv6_address = models.GenericIPAddressField(protocol="ipv6", blank=True) metadata = JSONField(blank=True) class Record(models.Model): """ A generic record model. """ NEW = "NEW" OLD = "OLD" PENDING = 0 CANCELLED = 1 CONFIRMED = 2 RECORD_TYPE_CHOICES = ((NEW, "New"), (OLD, "Old")) RECORD_STATUS_CHOICES = ( (PENDING, "Pending"), (CANCELLED, "Cancelled"), (CONFIRMED, "Confirmed"), ) title = NotNullRestrictedCharField() items = ListField() record_type = models.CharField( default=NEW, max_length=5, choices=RECORD_TYPE_CHOICES ) record_status = models.PositiveSmallIntegerField( default=PENDING, choices=RECORD_STATUS_CHOICES ) class ItemList(models.Model): name = models.CharField(max_length=100) class Item(models.Model): name = models.CharField(max_length=100) item_list = models.ForeignKey( ItemList, on_delete=models.CASCADE, related_name="items" ) class Tagged(models.Model): slug = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") def __str__(self): return self.slug class Bookmark(models.Model): url = models.URLField() tags = GenericRelation(Tagged) def upload_image_handler(instance, filename): base_name, ext = os.path.splitext(filename) return f"{base_name}{uuid.uuid4()}{ext}" class Attachment(models.Model): description = models.CharField(max_length=255) image = models.ImageField(blank=True, null=True, upload_to=upload_image_handler) class FoodChoices(models.TextChoices): BANANA = "ba", _("A delicious yellow Banana") APPLE = "ap", _("A delicious red Apple") class GroupChoices(models.IntegerChoices): GROUP_1 = 1, "First group" GROUP_2 = 2, "Second group" class SportChoices(models.TextChoices): FOOTBALL = "football", _("I prefer to use my foots.") BASKETBALL = "basketball", _("I prefer to use my hands.") class MusicianChoices(models.TextChoices): TOM = "tom_jobim", _("Antônio Carlos Jobim.") SINATRA = "sinatra", _("Francis Albert Sinatra.") class Preference(models.Model): name = models.CharField(max_length=128) preferred_food = models.CharField( max_length=2, choices=FoodChoices.choices, default=FoodChoices.BANANA ) preferred_group = models.IntegerField( choices=GroupChoices.choices, default=GroupChoices.GROUP_1 ) preferred_sport = models.CharField( max_length=255, choices=SportChoices.choices, default=SportChoices.FOOTBALL, blank=True ) preferred_musician = models.CharField( max_length=255, choices=MusicianChoices.choices, null=True, blank=True, default="" ) class Searchable(models.Model): title = models.CharField(max_length=255) search_vector = SearchVectorField(null=True) def __str__(self): return self.title class Meta: indexes = [GinIndex(fields=["search_vector"], name="search_vector_idx")] class ExtendedModel(models.Model): name = models.CharField(max_length=128) class Meta: abstract = True class Expert(ExtendedModel): cases = models.ManyToManyField("Case", related_name="related_experts") class Case(ExtendedModel): details = models.TextField() class Listing(models.Model): items = ArrayField(models.TextField(), size=4) djantic-0.7.0/tests/testapp/order.py000066400000000000000000000055731422577170000174600ustar00rootroot00000000000000import factory from django.db import models from factory.django import DjangoModelFactory class OrderUser(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50, null=True, blank=True) email = models.EmailField(unique=True) class OrderUserProfile(models.Model): address = models.CharField(max_length=255) user = models.OneToOneField(OrderUser, on_delete=models.CASCADE, related_name='profile') class Order(models.Model): total_price = models.DecimalField(max_digits=8, decimal_places=5, default=0) shipping_address = models.CharField(max_length=255) user = models.ForeignKey( OrderUser, on_delete=models.CASCADE, related_name="orders" ) class Meta: ordering = ["total_price"] def __str__(self): return f'{self.order.id} - {self.name}' class OrderItem(models.Model): name = models.CharField(max_length=30) price = models.DecimalField(max_digits=8, decimal_places=5, default=0) quantity = models.IntegerField(default=0) order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name="items" ) class Meta: ordering = ["order"] def __str__(self): return f'{self.order.id} - {self.name}' class OrderItemDetail(models.Model): name = models.CharField(max_length=30) value = models.IntegerField(default=0) quantity = models.IntegerField(default=0) order_item = models.ForeignKey( OrderItem, on_delete=models.CASCADE, related_name="details" ) class Meta: ordering = ["order_item"] def __str__(self): return f'{self.order_item.id} - {self.name}' class OrderItemDetailFactory(DjangoModelFactory): class Meta: model = OrderItemDetail class OrderItemFactory(DjangoModelFactory): class Meta: model = OrderItem @factory.post_generation def details(self, create, details, **kwargs): if details is None: details = [OrderItemDetailFactory.create(order_item=self, **kwargs) for i in range(0, 2)] class OrderFactory(DjangoModelFactory): class Meta: model = Order @factory.post_generation def items(self, create, items, **kwargs): if items is None: items = [OrderItemFactory.create(order=self, **kwargs) for i in range(0, 2)] class OrderUserProfileFactory(DjangoModelFactory): class Meta: model = OrderUserProfile class OrderUserFactory(DjangoModelFactory): class Meta: model = OrderUser @factory.post_generation def orders(self, create, orders, **kwargs): if orders is None: orders = [OrderFactory.create(user=self, **kwargs) for i in range(0, 2)] @factory.post_generation def profile(self, create, profile, **kwargs): if profile is None: profile = OrderUserProfileFactory.create(user=self, **kwargs) djantic-0.7.0/tests/testapp/project/000077500000000000000000000000001422577170000174275ustar00rootroot00000000000000djantic-0.7.0/tests/testapp/project/__init__.py000066400000000000000000000000001422577170000215260ustar00rootroot00000000000000djantic-0.7.0/tests/testapp/project/asgi.py000066400000000000000000000006071422577170000207270ustar00rootroot00000000000000""" ASGI config for project project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") application = get_asgi_application() djantic-0.7.0/tests/testapp/project/settings.py000066400000000000000000000060051422577170000216420ustar00rootroot00000000000000""" Django settings for project project. Generated by 'django-admin startproject' using Django 3.0.8. For more information on this file, see https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "s++59o#e_*oh#uxyo6mmevgg(0(9qutfs0o(c&6j6f%=9rz1*s" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "testapp.apps.TestappConfig", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "project.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] }, } ] WSGI_APPLICATION = "project.wsgi.application" # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = "/static/" djantic-0.7.0/tests/testapp/project/urls.py000066400000000000000000000013551422577170000207720ustar00rootroot00000000000000"""project URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.0/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path urlpatterns = [ path("admin/", admin.site.urls), ] djantic-0.7.0/tests/testapp/project/wsgi.py000066400000000000000000000006071422577170000207550ustar00rootroot00000000000000""" WSGI config for project project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") application = get_wsgi_application() djantic-0.7.0/tests/testapp/settings.py000066400000000000000000000061751422577170000202040ustar00rootroot00000000000000""" Django settings for project project. Generated by 'django-admin startproject' using Django 3.0.8. For more information on this file, see https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os import sys PROJECT_ROOT = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) sys.path.insert(0, PROJECT_ROOT) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "s++59o#e_*oh#uxyo6mmevgg(0(9qutfs0o(c&6j6f%=9rz1*s" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "testapp.apps.TestAppConfig", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "project.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] }, } ] WSGI_APPLICATION = "project.wsgi.application" # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = "/static/" djantic-0.7.0/tox.ini000066400000000000000000000010621422577170000144510ustar00rootroot00000000000000[tox] isolated_build = True envlist = py37-django{30,31,32} py38-django{30,31,32} py39-django{30,31,32} py310-django{32} [gh-actions] python = 3.7: py37 3.8: py38 3.9: py39 3.10: py310 [testenv] deps = pytest pytest-cov pytest-django coverage psycopg2 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 factory-boy setenv = PYTHONPATH = {toxinidir} commands = python -m pytest -vv --cov=djantic --cov-fail-under=100 --cov-report=term-missing {posargs}