aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--backend/authentication/backend.py11
-rw-r--r--backend/authentication/user.py5
-rw-r--r--backend/models/form.py12
-rw-r--r--backend/models/form_response.py2
-rw-r--r--backend/models/question.py8
-rw-r--r--backend/route.py10
-rw-r--r--backend/route_manager.py60
-rw-r--r--poetry.lock163
-rw-r--r--pyproject.toml1
9 files changed, 205 insertions, 67 deletions
diff --git a/backend/authentication/backend.py b/backend/authentication/backend.py
index 38668eb..f1d2ece 100644
--- a/backend/authentication/backend.py
+++ b/backend/authentication/backend.py
@@ -1,6 +1,5 @@
import jwt
import typing as t
-from abc import ABC
from starlette import authentication
from starlette.requests import Request
@@ -10,11 +9,11 @@ from backend import constants
from .user import User
-class JWTAuthenticationBackend(authentication.AuthenticationBackend, ABC):
+class JWTAuthenticationBackend(authentication.AuthenticationBackend):
"""Custom Starlette authentication backend for JWT."""
@staticmethod
- def get_token_from_header(header: str) -> t.Optional[str]:
+ def get_token_from_header(header: str) -> str:
"""Parse JWT token from header value."""
try:
prefix, token = header.split()
@@ -32,10 +31,10 @@ class JWTAuthenticationBackend(authentication.AuthenticationBackend, ABC):
async def authenticate(
self, request: Request
- ) -> t.Optional[t.Tuple[authentication.AuthCredentials, authentication.BaseUser]]:
+ ) -> t.Optional[tuple[authentication.AuthCredentials, authentication.BaseUser]]:
"""Handles JWT authentication process."""
if "Authorization" not in request.headers:
- return
+ return None
auth = request.headers["Authorization"]
token = self.get_token_from_header(auth)
@@ -47,7 +46,7 @@ class JWTAuthenticationBackend(authentication.AuthenticationBackend, ABC):
scopes = ["authenticated"]
- if payload.get("admin", False) is True:
+ if payload.get("admin") is True:
scopes.append("admin")
return authentication.AuthCredentials(scopes), User(token, payload)
diff --git a/backend/authentication/user.py b/backend/authentication/user.py
index afa243f..722c348 100644
--- a/backend/authentication/user.py
+++ b/backend/authentication/user.py
@@ -1,13 +1,12 @@
import typing as t
-from abc import ABC
from starlette.authentication import BaseUser
-class User(BaseUser, ABC):
+class User(BaseUser):
"""Starlette BaseUser implementation for JWT authentication."""
- def __init__(self, token: str, payload: t.Dict) -> None:
+ def __init__(self, token: str, payload: dict[str, t.Any]) -> None:
self.token = token
self.payload = payload
diff --git a/backend/models/form.py b/backend/models/form.py
index 2cf8486..cb58065 100644
--- a/backend/models/form.py
+++ b/backend/models/form.py
@@ -12,8 +12,8 @@ class Form(BaseModel):
"""Schema model for form."""
id: str = Field(alias="_id")
- features: t.List[str]
- questions: t.List[Question]
+ features: list[str]
+ questions: list[Question]
name: str
description: str
@@ -21,12 +21,12 @@ class Form(BaseModel):
allow_population_by_field_name = True
@validator("features")
- def validate_features(cls, value: t.List[str]) -> t.Optional[t.List[str]]:
+ def validate_features(cls, value: list[str]) -> t.Optional[list[str]]:
"""Validates is all features in allowed list."""
# Uppercase everything to avoid mixed case in DB
value = [v.upper() for v in value]
- allowed_values = list(v.value for v in FormFeatures.__members__.values())
- if not all(v in allowed_values for v in value):
+ allowed_values = [v.value for v in FormFeatures.__members__.values()]
+ if any(v not in allowed_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
@@ -34,7 +34,7 @@ class Form(BaseModel):
return value
- def dict(self, admin: bool = True, **kwargs: t.Dict) -> t.Dict[str, t.Any]:
+ def dict(self, admin: bool = True, **kwargs: t.Any) -> dict[str, t.Any]:
"""Wrapper for original function to exclude private data for public access."""
data = super().dict(**kwargs)
diff --git a/backend/models/form_response.py b/backend/models/form_response.py
index bea070f..f3296cd 100644
--- a/backend/models/form_response.py
+++ b/backend/models/form_response.py
@@ -12,7 +12,7 @@ class FormResponse(BaseModel):
id: str = Field(alias="_id")
user: t.Optional[DiscordUser]
antispam: t.Optional[AntiSpam]
- response: t.Dict[str, t.Any]
+ response: dict[str, t.Any]
form_id: str
class Config:
diff --git a/backend/models/question.py b/backend/models/question.py
index 1a012ff..3b98024 100644
--- a/backend/models/question.py
+++ b/backend/models/question.py
@@ -11,7 +11,7 @@ class Question(BaseModel):
id: str = Field(alias="_id")
name: str
type: str
- data: t.Dict[str, t.Any]
+ data: dict[str, t.Any]
class Config:
allow_population_by_field_name = True
@@ -31,14 +31,14 @@ class Question(BaseModel):
@root_validator
def validate_question_data(
cls,
- value: t.Dict[str, t.Any]
- ) -> t.Optional[t.Dict[str, t.Any]]:
+ value: dict[str, t.Any]
+ ) -> t.Optional[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 value.get("type") not in REQUIRED_QUESTION_TYPE_DATA:
return value
- for key, data_type in REQUIRED_QUESTION_TYPE_DATA[value.get("type")].items():
+ for key, data_type in REQUIRED_QUESTION_TYPE_DATA[value["type"]].items():
if key not in value.get("data", {}):
raise ValueError(f"Required question data key '{key}' not provided.")
diff --git a/backend/route.py b/backend/route.py
index eb69ebc..d8c38fc 100644
--- a/backend/route.py
+++ b/backend/route.py
@@ -5,13 +5,13 @@ from starlette.endpoints import HTTPEndpoint
class Route(HTTPEndpoint):
- name: str = None
- path: str = None
+ name: str
+ path: str
@classmethod
- def check_parameters(cls) -> "Route":
- if cls.name is None:
+ def check_parameters(cls):
+ if not hasattr(cls, "name"):
raise ValueError(f"Route {cls.__name__} has not defined a name")
- if cls.path is None:
+ if not hasattr(cls, "path"):
raise ValueError(f"Route {cls.__name__} has not defined a path")
diff --git a/backend/route_manager.py b/backend/route_manager.py
index bac275a..c48ea8e 100644
--- a/backend/route_manager.py
+++ b/backend/route_manager.py
@@ -4,15 +4,17 @@ Module to dynamically generate a Starlette routing map based on a directory tree
import importlib
import inspect
+import typing as t
+
from pathlib import Path
-from starlette.routing import Route as StarletteRoute, Mount
+from starlette.routing import Route as StarletteRoute, BaseRoute, Mount
from nested_dict import nested_dict
from backend.route import Route
-def construct_route_map_from_dict(route_dict: dict) -> list:
+def construct_route_map_from_dict(route_dict: dict) -> list[BaseRoute]:
route_map = []
for mount, item in route_dict.items():
if inspect.isclass(item):
@@ -26,35 +28,39 @@ def construct_route_map_from_dict(route_dict: dict) -> list:
return route_map
-def create_route_map() -> list:
- routes_directory = Path("backend") / "routes"
-
- route_dict = nested_dict()
-
- for file in routes_directory.rglob("*.py"):
- import_name = f"{'.'.join(file.parent.parts)}.{file.stem}"
+def is_route_class(member: t.Any) -> bool:
+ return inspect.isclass(member) and issubclass(member, Route) and member != Route
- route = importlib.import_module(import_name)
- for _member_name, member in inspect.getmembers(route):
- if inspect.isclass(member):
- if issubclass(member, Route) and member != Route:
- member.check_parameters()
+def route_classes() -> t.Iterator[tuple[Path, type[Route]]]:
+ routes_directory = Path("backend") / "routes"
- levels = file.parent.parts[2:]
+ for module_path in routes_directory.rglob("*.py"):
+ import_name = f"{'.'.join(file.parent.parts)}.{file.stem}"
+ route_module = importlib.import_module(import_name)
+ for _member_name, member in inspect.getmembers(route_module):
+ if is_route_class(member):
+ member.check_parameters()
+ yield (module_path, member)
- current_level = None
- for level in levels:
- if current_level is None:
- current_level = route_dict[f"/{level}"]
- else:
- current_level = current_level[f"/{level}"]
- if current_level is not None:
- current_level[member.path] = member
- else:
- route_dict[member.path] = member
+def create_route_map() -> list[BaseRoute]:
+ route_dict = nested_dict()
- route_map = construct_route_map_from_dict(route_dict.to_dict())
+ for module_path, member in route_classes():
+ # module_path == Path("backend/routes/foo/bar/baz/bin.py")
+ # => levels == ["foo", "bar", "baz"]
+ levels = file.parent.parts[2:]
+ current_level = None
+ for level in levels:
+ if current_level is None:
+ current_level = route_dict[f"/{level}"]
+ else:
+ current_level = current_level[f"/{level}"]
+
+ if current_level is not None:
+ current_level[member.path] = member
+ else:
+ route_dict[member.path] = member
- return route_map
+ return construct_route_map_from_dict(route_dict.to_dict())
diff --git a/poetry.lock b/poetry.lock
index d3ee7b5..c93f2f6 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -10,6 +10,28 @@ python-versions = "*"
pycares = ">=3.0.0"
[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "20.3.0"
+description = "Classes Without Boilerplate"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
+
+[[package]]
name = "certifi"
version = "2020.12.5"
description = "Python package for providing Mozilla's CA Bundle."
@@ -77,10 +99,10 @@ optional = false
python-versions = ">=3.4"
[package.extras]
-tornado = ["tornado (>=0.2)"]
+eventlet = ["eventlet (>=0.9.7)"]
gevent = ["gevent (>=0.13)"]
setproctitle = ["setproctitle"]
-eventlet = ["eventlet (>=0.9.7)"]
+tornado = ["tornado (>=0.2)"]
[[package]]
name = "h11"
@@ -143,6 +165,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
@@ -170,6 +200,36 @@ optional = false
python-versions = "*"
[[package]]
+name = "packaging"
+version = "20.8"
+description = "Core utilities for Python packages"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+
+[[package]]
+name = "pluggy"
+version = "0.13.1"
+description = "plugin and hook calling mechanisms for python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+
+[[package]]
+name = "py"
+version = "1.10.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
name = "pycares"
version = "3.1.1"
description = "Python interface for c-ares"
@@ -209,8 +269,8 @@ 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)"]
+typing_extensions = ["typing-extensions (>=3.7.2)"]
[[package]]
name = "pydnsbl"
@@ -221,8 +281,8 @@ optional = false
python-versions = "*"
[package.dependencies]
-idna = ">=2.9,<3"
aiodns = ">=1.1.1,<=2.0"
+idna = ">=2.9,<3"
[[package]]
name = "pyflakes"
@@ -241,9 +301,9 @@ optional = false
python-versions = "*"
[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]]
name = "pymongo"
@@ -254,14 +314,43 @@ optional = false
python-versions = "*"
[package.extras]
-tls = ["ipaddress"]
-encryption = ["pymongocrypt (<2.0.0)"]
aws = ["pymongo-auth-aws (<2.0.0)"]
+encryption = ["pymongocrypt (<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]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pytest"
+version = "6.2.0"
+description = "pytest: simple powerful testing with Python"
+category = "main"
+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\""}
+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 = "python-dotenv"
@@ -280,7 +369,7 @@ version = "5.3.1"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "rfc3986"
@@ -316,6 +405,14 @@ python-versions = ">=3.6"
full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"]
[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
name = "uvicorn"
version = "0.12.3"
description = "The lightning-fast ASGI server."
@@ -324,14 +421,14 @@ optional = false
python-versions = "*"
[package.dependencies]
-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\""}
+click = ">=7.0.0,<8.0.0"
+colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
h11 = ">=0.8"
-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"
+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\""}
+watchgod = {version = ">=0.6,<0.7", optional = true, markers = "extra == \"standard\""}
websockets = {version = ">=8.0.0,<9.0.0", optional = true, markers = "extra == \"standard\""}
[package.extras]
@@ -364,13 +461,21 @@ python-versions = ">=3.6.1"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
-content-hash = "9b3e9077c7cfab780a3333a213bbfee2eaa406da1f165a7a66cf1a83ac23011b"
+content-hash = "8edb3e972a11289bbffe636a012e3ec8bdda1f58a127f1c0b5e273acf8a8a83b"
[metadata.files]
aiodns = [
{file = "aiodns-2.0.0-py2.py3-none-any.whl", hash = "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de"},
{file = "aiodns-2.0.0.tar.gz", hash = "sha256:815fdef4607474295d68da46978a54481dd1e7be153c7d60f9e72773cd38d77d"},
]
+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-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
+ {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
+]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
@@ -463,6 +568,10 @@ idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
+iniconfig = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
@@ -474,6 +583,18 @@ motor = [
nested-dict = [
{file = "nested_dict-1.61.tar.gz", hash = "sha256:de0fb5bac82ba7bcc23736f09373f18628ea57f92bbaa13480d23f261c41e771"},
]
+packaging = [
+ {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
+ {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
+]
+pluggy = [
+ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+py = [
+ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+ {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
pycares = [
{file = "pycares-3.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:81edb016d9e43dde7473bc3999c29cdfee3a6b67308fed1ea21049f458e83ae0"},
{file = "pycares-3.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1917b82494907a4a342db420bc4dd5bac355a5fa3984c35ba9bf51422b020b48"},
@@ -615,6 +736,14 @@ pymongo = [
{file = "pymongo-3.11.2-py2.7-macosx-10.14-intel.egg", hash = "sha256:c1d1992bbdf363b22b5a9543ab7d7c6f27a1498826d50d91319b803ddcf1142e"},
{file = "pymongo-3.11.2.tar.gz", hash = "sha256:c2b67881392a9e85aa108e75f62cdbe372d5a3f17ea5f8d3436dcf4662052f14"},
]
+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.0-py3-none-any.whl", hash = "sha256:d69e1a80b34fe4d596c9142f35d9e523d98a2838976f1a68419a8f051b24cec6"},
+ {file = "pytest-6.2.0.tar.gz", hash = "sha256:b12e09409c5bdedc28d308469e156127004a436b41e9b44f9bff6446cbab9152"},
+]
python-dotenv = [
{file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"},
{file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"},
@@ -646,6 +775,10 @@ starlette = [
{file = "starlette-0.13.8-py3-none-any.whl", hash = "sha256:40afea6ffa830849800cc4efdf006a86ad579d6ba6b64cb1925a1897b020ba6e"},
{file = "starlette-0.13.8.tar.gz", hash = "sha256:82df29b2149437ad828a883674bf031788600c876dae50835e98398bd1706183"},
]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
uvicorn = [
{file = "uvicorn-0.12.3-py3-none-any.whl", hash = "sha256:562ef6aaa8fa723ab6b82cf9e67a774088179d0ec57cb17e447b15d58b603bcf"},
{file = "uvicorn-0.12.3.tar.gz", hash = "sha256:5836edaf4d278fe67ba0298c0537bdb6398cf359eb644f79e6500ca1aad232b3"},
diff --git a/pyproject.toml b/pyproject.toml
index 41e5165..6130f73 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,6 +17,7 @@ httpx = "^0.16.1"
gunicorn = "^20.0.4"
pydantic = "^1.7.2"
pydnsbl = "^1.1"
+pytest = "^6.2.0"
[tool.poetry.dev-dependencies]
flake8 = "^3.8.4"