pax_global_header 0000666 0000000 0000000 00000000064 14225771700 0014517 g ustar 00root root 0000000 0000000 52 comment=d23070db6d2aaabf629781749327b44bff974ce3
djantic-0.7.0/ 0000775 0000000 0000000 00000000000 14225771700 0013137 5 ustar 00root root 0000000 0000000 djantic-0.7.0/.bumpversion.cfg 0000664 0000000 0000000 00000000422 14225771700 0016245 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 14225771700 0014477 5 ustar 00root root 0000000 0000000 djantic-0.7.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14225771700 0016534 5 ustar 00root root 0000000 0000000 djantic-0.7.0/.github/workflows/publish.yml 0000664 0000000 0000000 00000001662 14225771700 0020732 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001522 14225771700 0020236 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000003464 14225771700 0015136 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000002060 14225771700 0014142 0 ustar 00root root 0000000 0000000 MIT 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.md 0000664 0000000 0000000 00000017746 14225771700 0014435 0 ustar 00root root 0000000 0000000
Djantic
Pydantic model support for Django
---
**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/ 0000775 0000000 0000000 00000000000 14225771700 0013707 5 ustar 00root root 0000000 0000000 djantic-0.7.0/bin/lint 0000775 0000000 0000000 00000000335 14225771700 0014604 0 ustar 00root root 0000000 0000000 #!/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/test 0000775 0000000 0000000 00000000317 14225771700 0014615 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14225771700 0014553 5 ustar 00root root 0000000 0000000 djantic-0.7.0/djantic/__init__.py 0000664 0000000 0000000 00000000071 14225771700 0016662 0 ustar 00root root 0000000 0000000 from .main import ModelSchema
__all__ = ["ModelSchema"]
djantic-0.7.0/djantic/fields.py 0000664 0000000 0000000 00000011562 14225771700 0016400 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000016433 14225771700 0016060 0 ustar 00root root 0000000 0000000 from 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.typed 0000664 0000000 0000000 00000000000 14225771700 0016240 0 ustar 00root root 0000000 0000000 djantic-0.7.0/docs/ 0000775 0000000 0000000 00000000000 14225771700 0014067 5 ustar 00root root 0000000 0000000 djantic-0.7.0/docs/index.md 0000664 0000000 0000000 00000017746 14225771700 0015537 0 ustar 00root root 0000000 0000000
Djantic
Pydantic model support for Django
---
**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.md 0000664 0000000 0000000 00000032302 14225771700 0015515 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000001516 14225771700 0015145 0 ustar 00root root 0000000 0000000 site_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.superfences djantic-0.7.0/poetry.lock 0000664 0000000 0000000 00000252030 14225771700 0015335 0 ustar 00root root 0000000 0000000 [[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.toml 0000664 0000000 0000000 00000001415 14225771700 0016054 0 ustar 00root root 0000000 0000000 [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.cfg 0000664 0000000 0000000 00000001773 14225771700 0014770 0 ustar 00root root 0000000 0000000 [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 .venv djantic-0.7.0/setup.py 0000664 0000000 0000000 00000002202 14225771700 0014645 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14225771700 0014301 5 ustar 00root root 0000000 0000000 djantic-0.7.0/tests/__init__.py 0000664 0000000 0000000 00000000000 14225771700 0016400 0 ustar 00root root 0000000 0000000 djantic-0.7.0/tests/test_fields.py 0000664 0000000 0000000 00000022245 14225771700 0017165 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000002465 14225771700 0017023 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002721 14225771700 0016640 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000030560 14225771700 0023020 0 ustar 00root root 0000000 0000000
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.py 0000664 0000000 0000000 00000016242 14225771700 0017374 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000071465 14225771700 0017727 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000023404 14225771700 0017340 0 ustar 00root root 0000000 0000000 import 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/ 0000775 0000000 0000000 00000000000 14225771700 0015761 5 ustar 00root root 0000000 0000000 djantic-0.7.0/tests/testapp/__init__.py 0000664 0000000 0000000 00000000000 14225771700 0020060 0 ustar 00root root 0000000 0000000 djantic-0.7.0/tests/testapp/apps.py 0000664 0000000 0000000 00000000131 14225771700 0017271 0 ustar 00root root 0000000 0000000 from django.apps import AppConfig
class TestAppConfig(AppConfig):
name = "testapp"
djantic-0.7.0/tests/testapp/fields.py 0000664 0000000 0000000 00000001210 14225771700 0017573 0 ustar 00root root 0000000 0000000 from 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.py 0000775 0000000 0000000 00000001163 14225771700 0017567 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000015623 14225771700 0017625 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000005573 14225771700 0017460 0 ustar 00root root 0000000 0000000 import 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/ 0000775 0000000 0000000 00000000000 14225771700 0017427 5 ustar 00root root 0000000 0000000 djantic-0.7.0/tests/testapp/project/__init__.py 0000664 0000000 0000000 00000000000 14225771700 0021526 0 ustar 00root root 0000000 0000000 djantic-0.7.0/tests/testapp/project/asgi.py 0000664 0000000 0000000 00000000607 14225771700 0020727 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000006005 14225771700 0021642 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000001355 14225771700 0020772 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000000607 14225771700 0020755 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000006175 14225771700 0020204 0 ustar 00root root 0000000 0000000 """
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.ini 0000664 0000000 0000000 00000001062 14225771700 0014451 0 ustar 00root root 0000000 0000000 [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}