diff options
| -rw-r--r-- | backend/constants.py | 41 | ||||
| -rw-r--r-- | backend/models/__init__.py | 4 | ||||
| -rw-r--r-- | backend/models/form.py | 27 | ||||
| -rw-r--r-- | backend/models/question.py | 54 | ||||
| -rw-r--r-- | poetry.lock | 266 | ||||
| -rw-r--r-- | pyproject.toml | 1 | 
6 files changed, 263 insertions, 130 deletions
| diff --git a/backend/constants.py b/backend/constants.py index 746e277..3b8bec8 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -3,6 +3,8 @@ load_dotenv()  import os  # noqa  import binascii  # noqa +from enum import Enum  # noqa +  DATABASE_URL = os.getenv("DATABASE_URL")  MONGO_DATABASE = os.getenv("MONGO_DATABASE", "pydis_forms") @@ -15,3 +17,42 @@ OAUTH2_REDIRECT_URI = os.getenv(  )  SECRET_KEY = os.getenv("SECRET_KEY", binascii.hexlify(os.urandom(30)).decode()) + +QUESTION_TYPES = [ +    "radio", +    "checkbox", +    "select", +    "short_text", +    "textarea", +    "code", +    "range", +    "section", +] + +REQUIRED_QUESTION_TYPE_DATA = { +    "radio": { +        "options": list, +    }, +    "select": { +        "options": list, +    }, +    "code": { +        "language": str, +    }, +    "range": { +        "options": list, +    }, +    "section": { +        "text": str, +    }, +} + + +class FormFeatures(Enum): +    """Lists form features. Read more in SCHEMA.md.""" + +    DISCOVERABLE = "DISCOVERABLE" +    REQUIRES_LOGIN = "REQUIRES_LOGIN" +    OPEN = "OPEN" +    COLLECT_EMAIL = "COLLECT_EMAIL" +    DISABLE_ANTISPAM = "DISABLE_ANTISPAM" diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..80abf6f --- /dev/null +++ b/backend/models/__init__.py @@ -0,0 +1,4 @@ +from .form import Form +from .question import Question + +__all__ = ["Form", "Question"] diff --git a/backend/models/form.py b/backend/models/form.py new file mode 100644 index 0000000..d0f0a3c --- /dev/null +++ b/backend/models/form.py @@ -0,0 +1,27 @@ +import typing as t + +from pydantic import BaseModel, Field, validator + +from backend.constants import FormFeatures +from backend.models import Question + + +class Form(BaseModel): +    """Schema model for form.""" + +    id: str = Field(alias="_id") +    features: t.List[str] +    questions: t.List[Question] + +    @validator("features") +    def validate_features(self, value: t.List[str]) -> t.Optional[t.List[str]]: +        """Validates is all features in allowed list.""" +        # Uppercase everything to avoid mixed case in DB +        value = [v.upper() for v in value] +        if not all(v in FormFeatures.__members__.values() for v in value): +            raise ValueError("Form features list contains one or more invalid values.") + +        if FormFeatures.COLLECT_EMAIL in value and FormFeatures.REQUIRES_LOGIN not in value:  # noqa +            raise ValueError("COLLECT_EMAIL feature require REQUIRES_LOGIN feature.") + +        return value diff --git a/backend/models/question.py b/backend/models/question.py new file mode 100644 index 0000000..2324a47 --- /dev/null +++ b/backend/models/question.py @@ -0,0 +1,54 @@ +import typing as t + +from pydantic import BaseModel, Field, validator + +from backend.constants import QUESTION_TYPES, REQUIRED_QUESTION_TYPE_DATA + + +class Question(BaseModel): +    """Schema model for form question.""" + +    id: str = Field(alias="_id") +    name: str +    type: str +    data: t.Dict[str, t.Any] + +    @validator("type", pre=True) +    def validate_question_type(self, value: str) -> t.Optional[str]: +        """Checks if question type in currently allowed types list.""" +        value = value.lower() +        if value not in QUESTION_TYPES: +            raise ValueError( +                f"{value} is not valid question type. " +                f"Allowed question types: {QUESTION_TYPES}." +            ) + +        return value + +    @validator("data") +    def validate_question_data( +            self, +            value: t.Dict[str, t.Any] +    ) -> t.Optional[t.Dict[str, t.Any]]: +        """Check does required data exists for question type and remove other data.""" +        # When question type don't need data, don't add anything to keep DB clean. +        if self.type not in REQUIRED_QUESTION_TYPE_DATA: +            return {} + +        # Required keys (and values) will be stored to here +        # to remove all unnecessary stuff +        result = {} + +        for key, data_type in REQUIRED_QUESTION_TYPE_DATA[self.type].items(): +            if key not in value: +                raise ValueError(f"Required question data key '{key}' not provided.") + +            if not isinstance(value[key], data_type): +                raise ValueError( +                    f"Question data key '{key}' expects {data_type.__name__}, " +                    f"got {type(value[key]).__name__} instead." +                ) + +            result[key] = value[key] + +        return result diff --git a/poetry.lock b/poetry.lock index 4ff5822..665cb24 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,35 +1,34 @@  [[package]] -category = "main" -description = "Python package for providing Mozilla's CA Bundle."  name = "certifi" +version = "2020.11.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main"  optional = false  python-versions = "*" -version = "2020.11.8"  [[package]] -category = "main" -description = "Composable command line interface toolkit"  name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main"  optional = false  python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2"  [[package]] -category = "main" -description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\""  name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main"  optional = false  python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.4"  [[package]] -category = "dev" -description = "the modular source code checker: pep8 pyflakes and co"  name = "flake8" +version = "3.8.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev"  optional = false  python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.4"  [package.dependencies]  mccabe = ">=0.6.0,<0.7.0" @@ -37,48 +36,45 @@ pycodestyle = ">=2.6.0a1,<2.7.0"  pyflakes = ">=2.2.0,<2.3.0"  [[package]] -category = "dev" -description = "Flake8 Type Annotation Checks"  name = "flake8-annotations" +version = "2.4.1" +description = "Flake8 Type Annotation Checks" +category = "dev"  optional = false  python-versions = ">=3.6.1,<4.0.0" -version = "2.4.1"  [package.dependencies]  flake8 = ">=3.7,<3.9"  [[package]] -category = "main" -description = "WSGI HTTP Server for UNIX"  name = "gunicorn" +version = "20.0.4" +description = "WSGI HTTP Server for UNIX" +category = "main"  optional = false  python-versions = ">=3.4" -version = "20.0.4" - -[package.dependencies] -setuptools = ">=3.0"  [package.extras] -eventlet = ["eventlet (>=0.9.7)"] +tornado = ["tornado (>=0.2)"]  gevent = ["gevent (>=0.13)"]  setproctitle = ["setproctitle"] -tornado = ["tornado (>=0.2)"] +eventlet = ["eventlet (>=0.9.7)"]  [[package]] -category = "main" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"  name = "h11" +version = "0.11.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main"  optional = false  python-versions = "*" -version = "0.11.0"  [[package]] -category = "main" -description = "A minimal low-level HTTP client."  name = "httpcore" +version = "0.12.2" +description = "A minimal low-level HTTP client." +category = "main"  optional = false  python-versions = ">=3.6" -version = "0.12.2"  [package.dependencies]  h11 = "<1.0.0" @@ -88,234 +84,220 @@ sniffio = ">=1.0.0,<2.0.0"  http2 = ["h2 (>=3,<5)"]  [[package]] -category = "main" -description = "A collection of framework independent HTTP protocol utils." -marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""  name = "httptools" +version = "0.1.1" +description = "A collection of framework independent HTTP protocol utils." +category = "main"  optional = false  python-versions = "*" -version = "0.1.1"  [package.extras] -test = ["Cython (0.29.14)"] +test = ["Cython (==0.29.14)"]  [[package]] -category = "main" -description = "The next generation HTTP client."  name = "httpx" +version = "0.16.1" +description = "The next generation HTTP client." +category = "main"  optional = false  python-versions = ">=3.6" -version = "0.16.1"  [package.dependencies]  certifi = "*"  httpcore = ">=0.12.0,<0.13.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}  sniffio = "*" -[package.dependencies.rfc3986] -extras = ["idna2008"] -version = ">=1.3,<2" -  [package.extras]  brotli = ["brotlipy (>=0.7.0,<0.8.0)"]  http2 = ["h2 (>=3.0.0,<4.0.0)"]  [[package]] -category = "main" -description = "Internationalized Domain Names in Applications (IDNA)"  name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main"  optional = false  python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10"  [[package]] -category = "dev" -description = "McCabe checker, plugin for flake8"  name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev"  optional = false  python-versions = "*" -version = "0.6.1"  [[package]] -category = "main" -description = "Python dictionary with automatic and arbitrary levels of nestedness"  name = "nested-dict" +version = "1.61" +description = "Python dictionary with automatic and arbitrary levels of nestedness" +category = "main"  optional = false  python-versions = "*" -version = "1.61"  [[package]] -category = "dev" -description = "Python style guide checker"  name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev"  optional = false  python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.6.0"  [[package]] -category = "dev" -description = "passive checker of Python programs" +name = "pydantic" +version = "1.7.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +typing_extensions = ["typing-extensions (>=3.7.2)"] +email = ["email-validator (>=1.0.3)"] + +[[package]]  name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev"  optional = false  python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.2.0"  [[package]] -category = "main" -description = "JSON Web Token implementation in Python"  name = "pyjwt" +version = "1.7.1" +description = "JSON Web Token implementation in Python" +category = "main"  optional = false  python-versions = "*" -version = "1.7.1"  [package.extras] +test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"]  crypto = ["cryptography (>=1.4)"]  flake8 = ["flake8", "flake8-import-order", "pep8-naming"] -test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"]  [[package]] -category = "main" -description = "Python driver for MongoDB <http://www.mongodb.org>"  name = "pymongo" +version = "3.11.1" +description = "Python driver for MongoDB <http://www.mongodb.org>" +category = "main"  optional = false  python-versions = "*" -version = "3.11.1"  [package.extras] -aws = ["pymongo-auth-aws (<2.0.0)"] +tls = ["ipaddress"]  encryption = ["pymongocrypt (<2.0.0)"] +aws = ["pymongo-auth-aws (<2.0.0)"]  gssapi = ["pykerberos"] -ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]  snappy = ["python-snappy"]  srv = ["dnspython (>=1.16.0,<1.17.0)"] -tls = ["ipaddress"]  zstd = ["zstandard"] +ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]  [[package]] -category = "main" -description = "Add .env support to your django/flask apps in development and deployments"  name = "python-dotenv" +version = "0.14.0" +description = "Add .env support to your django/flask apps in development and deployments" +category = "main"  optional = false  python-versions = "*" -version = "0.14.0"  [package.extras]  cli = ["click (>=5.0)"]  [[package]] -category = "main" -description = "YAML parser and emitter for Python"  name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "main"  optional = false  python-versions = "*" -version = "5.3.1"  [[package]] -category = "main" -description = "Validating URI References per RFC 3986"  name = "rfc3986" +version = "1.4.0" +description = "Validating URI References per RFC 3986" +category = "main"  optional = false  python-versions = "*" -version = "1.4.0"  [package.dependencies] -[package.dependencies.idna] -optional = true -version = "*" +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}  [package.extras]  idna2008 = ["idna"]  [[package]] -category = "main" -description = "Sniff out which async library your code is running under"  name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main"  optional = false  python-versions = ">=3.5" -version = "1.2.0"  [[package]] -category = "main" -description = "The little ASGI library that shines."  name = "starlette" +version = "0.13.8" +description = "The little ASGI library that shines." +category = "main"  optional = false  python-versions = ">=3.6" -version = "0.13.8"  [package.extras]  full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"]  [[package]] -category = "main" -description = "The lightning-fast ASGI server."  name = "uvicorn" +version = "0.12.3" +description = "The lightning-fast ASGI server." +category = "main"  optional = false  python-versions = "*" -version = "0.12.2"  [package.dependencies] -click = ">=7.0.0,<8.0.0" +PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}  h11 = ">=0.8" - -[package.dependencies.PyYAML] -optional = true -version = ">=5.1" - -[package.dependencies.colorama] -optional = true -version = ">=0.4" - -[package.dependencies.httptools] -optional = true -version = ">=0.1.0,<0.2.0" - -[package.dependencies.python-dotenv] -optional = true -version = ">=0.13" - -[package.dependencies.uvloop] -optional = true -version = ">=0.14.0" - -[package.dependencies.watchgod] -optional = true -version = ">=0.6,<0.7" - -[package.dependencies.websockets] -optional = true -version = ">=8.0.0,<9.0.0" +watchgod = {version = ">=0.6,<0.7", optional = true, markers = "extra == \"standard\""} +httptools = {version = ">=0.1.0,<0.2.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +click = ">=7.0.0,<8.0.0" +websockets = {version = ">=8.0.0,<9.0.0", optional = true, markers = "extra == \"standard\""}  [package.extras]  standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6,<0.7)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "httptools (>=0.1.0,<0.2.0)", "uvloop (>=0.14.0)", "colorama (>=0.4)"]  [[package]] -category = "main" -description = "Fast implementation of asyncio event loop on top of libuv" -marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""  name = "uvloop" +version = "0.14.0" +description = "Fast implementation of asyncio event loop on top of libuv" +category = "main"  optional = false  python-versions = "*" -version = "0.14.0"  [[package]] -category = "main" -description = "Simple, modern file watching and code reload in python."  name = "watchgod" +version = "0.6" +description = "Simple, modern file watching and code reload in python." +category = "main"  optional = false  python-versions = ">=3.5" -version = "0.6"  [[package]] -category = "main" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"  name = "websockets" +version = "8.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main"  optional = false  python-versions = ">=3.6.1" -version = "8.1"  [metadata] -content-hash = "9f434cb3b530e607b34f6b26c5c0314b40597d76b899316d35f4122c3d675eec" +lock-version = "1.1"  python-versions = "^3.9" +content-hash = "4b12ca2ac95ff86f810d6bd0546b24af82e638f552fa563d7d288fb8284b0872"  [metadata.files]  certifi = [ @@ -383,6 +365,30 @@ pycodestyle = [      {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},      {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},  ] +pydantic = [ +    {file = "pydantic-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dfaa6ed1d509b5aef4142084206584280bb6e9014f01df931ec6febdad5b200a"}, +    {file = "pydantic-1.7.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2182ba2a9290964b278bcc07a8d24207de709125d520efec9ad6fa6f92ee058d"}, +    {file = "pydantic-1.7.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:0fe8b45d31ae53d74a6aa0bf801587bd49970070eac6a6326f9fa2a302703b8a"}, +    {file = "pydantic-1.7.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:01f0291f4951580f320f7ae3f2ecaf0044cdebcc9b45c5f882a7e84453362420"}, +    {file = "pydantic-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4ba6b903e1b7bd3eb5df0e78d7364b7e831ed8b4cd781ebc3c4f1077fbcb72a4"}, +    {file = "pydantic-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b11fc9530bf0698c8014b2bdb3bbc50243e82a7fa2577c8cfba660bcc819e768"}, +    {file = "pydantic-1.7.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a3c274c49930dc047a75ecc865e435f3df89715c775db75ddb0186804d9b04d0"}, +    {file = "pydantic-1.7.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:c68b5edf4da53c98bb1ccb556ae8f655575cb2e676aef066c12b08c724a3f1a1"}, +    {file = "pydantic-1.7.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:95d4410c4e429480c736bba0db6cce5aaa311304aea685ebcf9ee47571bfd7c8"}, +    {file = "pydantic-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a2fc7bf77ed4a7a961d7684afe177ff59971828141e608f142e4af858e07dddc"}, +    {file = "pydantic-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9572c0db13c8658b4a4cb705dcaae6983aeb9842248b36761b3fbc9010b740f"}, +    {file = "pydantic-1.7.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f83f679e727742b0c465e7ef992d6da4a7e5268b8edd8fdaf5303276374bef52"}, +    {file = "pydantic-1.7.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:e5fece30e80087d9b7986104e2ac150647ec1658c4789c89893b03b100ca3164"}, +    {file = "pydantic-1.7.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce2d452961352ba229fe1e0b925b41c0c37128f08dddb788d0fd73fd87ea0f66"}, +    {file = "pydantic-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:fc21a37ff3f545de80b166e1735c4172b41b017948a3fb2d5e2f03c219eac50a"}, +    {file = "pydantic-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c9760d1556ec59ff745f88269a8f357e2b7afc75c556b3a87b8dda5bc62da8ba"}, +    {file = "pydantic-1.7.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c1673633ad1eea78b1c5c420a47cd48717d2ef214c8230d96ca2591e9e00958"}, +    {file = "pydantic-1.7.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:388c0c26c574ff49bad7d0fd6ed82fbccd86a0473fa3900397d3354c533d6ebb"}, +    {file = "pydantic-1.7.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ab1d5e4d8de00575957e1c982b951bffaedd3204ddd24694e3baca3332e53a23"}, +    {file = "pydantic-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:f045cf7afb3352a03bc6cb993578a34560ac24c5d004fa33c76efec6ada1361a"}, +    {file = "pydantic-1.7.2-py3-none-any.whl", hash = "sha256:6665f7ab7fbbf4d3c1040925ff4d42d7549a8c15fe041164adfe4fc2134d4cce"}, +    {file = "pydantic-1.7.2.tar.gz", hash = "sha256:c8200aecbd1fb914e1bd061d71a4d1d79ecb553165296af0c14989b89e90d09b"}, +]  pyflakes = [      {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},      {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, @@ -489,8 +495,8 @@ starlette = [      {file = "starlette-0.13.8.tar.gz", hash = "sha256:82df29b2149437ad828a883674bf031788600c876dae50835e98398bd1706183"},  ]  uvicorn = [ -    {file = "uvicorn-0.12.2-py3-none-any.whl", hash = "sha256:e5dbed4a8a44c7b04376021021d63798d6a7bcfae9c654a0b153577b93854fba"}, -    {file = "uvicorn-0.12.2.tar.gz", hash = "sha256:8ff7495c74b8286a341526ff9efa3988ebab9a4b2f561c7438c3cb420992d7dd"}, +    {file = "uvicorn-0.12.3-py3-none-any.whl", hash = "sha256:562ef6aaa8fa723ab6b82cf9e67a774088179d0ec57cb17e447b15d58b603bcf"}, +    {file = "uvicorn-0.12.3.tar.gz", hash = "sha256:5836edaf4d278fe67ba0298c0537bdb6398cf359eb644f79e6500ca1aad232b3"},  ]  uvloop = [      {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, diff --git a/pyproject.toml b/pyproject.toml index 999f45c..bddfb0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ python-dotenv = "^0.14.0"  pyjwt = "^1.7.1"  httpx = "^0.16.1"  gunicorn = "^20.0.4" +pydantic = "^1.7.2"  [tool.poetry.dev-dependencies]  flake8 = "^3.8.4" | 
