Integration tests #3
15 changed files with 527 additions and 21 deletions
3
.flake8
3
.flake8
|
@ -1,3 +0,0 @@
|
|||
[flake8]
|
||||
max-line-length = 88
|
||||
extend-ignore = E203, B008
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
.env
|
||||
.*env
|
||||
.idea/
|
||||
.nvim.lua
|
||||
.python-version
|
||||
|
|
|
@ -15,7 +15,10 @@ repos:
|
|||
rev: 7.1.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: [--config, .flake8]
|
||||
args:
|
||||
- --max-line-length=88
|
||||
- --extend-ignore=E203,B008
|
||||
- --per-file-ignores=tests/factories.py:E701
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
|
|
364
poetry.lock
generated
364
poetry.lock
generated
|
@ -32,6 +32,21 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
|
|||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "asttokens"
|
||||
version = "3.0.0"
|
||||
description = "Annotate AST trees with source code positions"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
|
||||
{file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
astroid = ["astroid (>=2,<4)"]
|
||||
test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "5.0.1"
|
||||
|
@ -79,6 +94,17 @@ files = [
|
|||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.1.1"
|
||||
description = "Decorators for Humans"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
|
||||
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.7.0"
|
||||
|
@ -114,6 +140,35 @@ files = [
|
|||
dnspython = ">=2.0.0"
|
||||
idna = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.1.0"
|
||||
description = "Get the currently executing AST node of a frame, and other information"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"},
|
||||
{file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "33.1.0"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Faker-33.1.0-py3-none-any.whl", hash = "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d"},
|
||||
{file = "faker-33.1.0.tar.gz", hash = "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.4"
|
||||
typing-extensions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.6"
|
||||
|
@ -402,6 +457,73 @@ files = [
|
|||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "8.31.0"
|
||||
description = "IPython: Productive Interactive Computing"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6"},
|
||||
{file = "ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
decorator = "*"
|
||||
jedi = ">=0.16"
|
||||
matplotlib-inline = "*"
|
||||
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
|
||||
prompt_toolkit = ">=3.0.41,<3.1.0"
|
||||
pygments = ">=2.4.0"
|
||||
stack_data = "*"
|
||||
traitlets = ">=5.13.0"
|
||||
typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
|
||||
|
||||
[package.extras]
|
||||
all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"]
|
||||
black = ["black"]
|
||||
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"]
|
||||
kernel = ["ipykernel"]
|
||||
matplotlib = ["matplotlib"]
|
||||
nbconvert = ["nbconvert"]
|
||||
nbformat = ["nbformat"]
|
||||
notebook = ["ipywidgets", "notebook"]
|
||||
parallel = ["ipyparallel"]
|
||||
qtconsole = ["qtconsole"]
|
||||
test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
|
||||
test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
|
||||
|
||||
[[package]]
|
||||
name = "jedi"
|
||||
version = "0.19.2"
|
||||
description = "An autocompletion tool for Python that can be used for text editors."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
|
||||
{file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
parso = ">=0.8.4,<0.9.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
|
||||
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
|
||||
testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.5"
|
||||
|
@ -513,6 +635,20 @@ files = [
|
|||
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib-inline"
|
||||
version = "0.1.7"
|
||||
description = "Inline Matplotlib backend for Jupyter"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
|
||||
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
traitlets = "*"
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
|
@ -524,6 +660,124 @@ files = [
|
|||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parso"
|
||||
version = "0.8.4"
|
||||
description = "A Python Parser"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
|
||||
{file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
|
||||
testing = ["docopt", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "pexpect"
|
||||
version = "4.9.0"
|
||||
description = "Pexpect allows easy control of interactive console applications."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
|
||||
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
ptyprocess = ">=0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "polyfactory"
|
||||
version = "2.18.1"
|
||||
description = "Mock data generation factories"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8"
|
||||
files = [
|
||||
{file = "polyfactory-2.18.1-py3-none-any.whl", hash = "sha256:1a2b0715e08bfe9f14abc838fc013ab8772cb90e66f2e601e15e1127f0bc1b18"},
|
||||
{file = "polyfactory-2.18.1.tar.gz", hash = "sha256:17c9db18afe4fb8d7dd8e5ba296e69da0fcf7d0f3b63d1840eb10d135aed5aad"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
faker = "*"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[package.extras]
|
||||
attrs = ["attrs (>=22.2.0)"]
|
||||
beanie = ["beanie", "pydantic[email]"]
|
||||
full = ["attrs", "beanie", "msgspec", "odmantic", "pydantic", "sqlalchemy"]
|
||||
msgspec = ["msgspec"]
|
||||
odmantic = ["odmantic (<1.0.0)", "pydantic[email]"]
|
||||
pydantic = ["pydantic[email]"]
|
||||
sqlalchemy = ["sqlalchemy (>=1.4.29)"]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.48"
|
||||
description = "Library for building powerful interactive command lines in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"},
|
||||
{file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
wcwidth = "*"
|
||||
|
||||
[[package]]
|
||||
name = "ptyprocess"
|
||||
version = "0.7.0"
|
||||
description = "Run a subprocess in a pseudo terminal"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pure-eval"
|
||||
version = "0.2.3"
|
||||
description = "Safely evaluate AST nodes without side effects"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
|
||||
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
tests = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.4"
|
||||
|
@ -690,6 +944,58 @@ files = [
|
|||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2"
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.25.0"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
|
||||
{file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=8.2,<9"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
|
||||
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
files = [
|
||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
|
@ -840,6 +1146,17 @@ files = [
|
|||
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
files = [
|
||||
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
|
@ -851,6 +1168,25 @@ files = [
|
|||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stack-data"
|
||||
version = "0.6.3"
|
||||
description = "Extract data from python stack frames and tracebacks for informative displays"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
|
||||
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
asttokens = ">=2.1.0"
|
||||
executing = ">=1.2.0"
|
||||
pure-eval = "*"
|
||||
|
||||
[package.extras]
|
||||
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.41.3"
|
||||
|
@ -868,6 +1204,21 @@ anyio = ">=3.4.0,<5"
|
|||
[package.extras]
|
||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.14.3"
|
||||
description = "Traitlets Python configuration system"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
|
||||
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
|
||||
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.1"
|
||||
|
@ -1055,6 +1406,17 @@ files = [
|
|||
[package.dependencies]
|
||||
anyio = ">=3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
|
||||
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "14.1"
|
||||
|
@ -1139,4 +1501,4 @@ hiredis = ["hiredis"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "1f2ca7562492fce7198a033828bce3cd8b9ed4cd38fc9e5c35784113bb816827"
|
||||
content-hash = "9f0f96c653fdc92b7a1ed18be32ff88814ac5188b207fda1ea7e672a7cd19d6c"
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
from fastapi import FastAPI
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.exceptions import HTTPException
|
||||
from redis.asyncio import Redis
|
||||
|
||||
from pssecret.models import Secret, SecretSaveResult
|
||||
from pssecret.redis_db import redis
|
||||
from pssecret.utils import get_new_key
|
||||
from pssecret.redis_db import get_redis
|
||||
from pssecret.utils import save_secret_data
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
RedisDep = Annotated[Redis, Depends(get_redis)]
|
||||
|
||||
|
||||
@app.post(
|
||||
"/secret",
|
||||
|
@ -18,12 +23,9 @@ app = FastAPI()
|
|||
),
|
||||
response_model=SecretSaveResult,
|
||||
)
|
||||
async def set_secret(data: Secret):
|
||||
new_key = await get_new_key()
|
||||
await redis.setex(new_key, 60 * 60 * 24, data.data)
|
||||
|
||||
async def set_secret(data: Secret, redis: RedisDep) -> dict[str, str]:
|
||||
return {
|
||||
"key": new_key,
|
||||
"key": await save_secret_data(data, redis),
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,8 +40,8 @@ async def set_secret(data: Secret):
|
|||
response_model=Secret,
|
||||
responses={404: {"description": "The item was not found"}},
|
||||
)
|
||||
async def get_secret(secret_key: str):
|
||||
data: str | None = await redis.getdel(secret_key)
|
||||
async def get_secret(secret_key: str, redis: RedisDep) -> dict[str, bytes]:
|
||||
data: bytes | None = await redis.getdel(secret_key)
|
||||
|
||||
if data is None:
|
||||
raise HTTPException(404)
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
from pssecret.settings import settings
|
||||
from pssecret.settings import Settings, get_settings
|
||||
|
||||
redis = aioredis.from_url(str(settings.redis_url))
|
||||
|
||||
def get_redis(settings: Annotated[Settings, Depends(get_settings)]) -> aioredis.Redis:
|
||||
return aioredis.from_url(str(settings.redis_url))
|
||||
|
|
|
@ -6,4 +6,5 @@ class Settings(BaseSettings):
|
|||
redis_url: RedisDsn = RedisDsn("redis://localhost")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
from uuid import uuid4
|
||||
|
||||
from pssecret.redis_db import redis
|
||||
from redis.asyncio import Redis
|
||||
|
||||
from pssecret.models import Secret
|
||||
|
||||
|
||||
async def get_new_key() -> str:
|
||||
async def get_new_key(redis: Redis) -> str:
|
||||
"""Returns free Redis key"""
|
||||
while True:
|
||||
new_key = str(uuid4())
|
||||
|
||||
if not await redis.exists(new_key):
|
||||
return new_key
|
||||
|
||||
|
||||
async def save_secret(data: Secret, redis: Redis) -> str:
|
||||
"""Save passed data, returns retrieval key"""
|
||||
new_key = await get_new_key(redis)
|
||||
await redis.setex(new_key, 60 * 60 * 24, data.data)
|
||||
|
||||
return new_key
|
||||
|
|
|
@ -33,7 +33,20 @@ hiredis = { version = "3.1.0", optional = true }
|
|||
hiredis = ["hiredis"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = ">=8.3.4,<9"
|
||||
pytest-asyncio = "*"
|
||||
polyfactory = ">=2.18,<3"
|
||||
|
||||
[tool.poetry.group.dev-extra.dependencies]
|
||||
ipython = "^8.31.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.basedpyright]
|
||||
reportUnknownMemberType = "none"
|
||||
reportUnusedCallResult = "none"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
38
tests/conftest.py
Normal file
38
tests/conftest.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
from collections.abc import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic_settings import SettingsConfigDict
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
from pssecret.main import app
|
||||
from pssecret.settings import Settings, get_settings
|
||||
|
||||
|
||||
class TestSettings(Settings):
|
||||
model_config = SettingsConfigDict(env_file=".test.env")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings() -> Settings:
|
||||
return TestSettings()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def redis_server(settings: Settings) -> AsyncGenerator[aioredis.Redis]:
|
||||
redis = await aioredis.from_url(str(settings.redis_url))
|
||||
yield redis
|
||||
await redis.flushdb()
|
||||
|
||||
|
||||
def get_test_settings() -> Settings:
|
||||
return TestSettings()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def client() -> TestClient:
|
||||
client_ = TestClient(app)
|
||||
|
||||
app.dependency_overrides[get_settings] = get_test_settings
|
||||
|
||||
return client_
|
6
tests/factories.py
Normal file
6
tests/factories.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from polyfactory.factories.pydantic_factory import ModelFactory
|
||||
|
||||
from pssecret.models import Secret
|
||||
|
||||
|
||||
class SecretFactory(ModelFactory[Secret]): ...
|
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
33
tests/integration/test_api.py
Normal file
33
tests/integration/test_api.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.factories import SecretFactory
|
||||
|
||||
|
||||
def test_store_secret_returns_key(client: TestClient):
|
||||
response = client.post("/secret", json=dict(SecretFactory().build()))
|
||||
|
||||
assert response.json()["key"]
|
||||
|
||||
|
||||
def test_stored_secret_could_be_retrieved(client: TestClient):
|
||||
secret = SecretFactory().build()
|
||||
response = client.post("/secret", json=dict(secret))
|
||||
|
||||
retrieval_key: str = response.json()["key"]
|
||||
|
||||
response = client.get(f"/secret/{retrieval_key}")
|
||||
|
||||
assert response.json()["data"] == secret.data
|
||||
|
||||
|
||||
def test_stored_secret_could_be_retrieved_only_once(client: TestClient):
|
||||
secret = SecretFactory().build()
|
||||
response = client.post("/secret", json=dict(secret))
|
||||
|
||||
retrieval_key: str = response.json()["key"]
|
||||
|
||||
client.get(f"/secret/{retrieval_key}")
|
||||
response = client.get(f"/secret/{retrieval_key}")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert secret.data not in response.text
|
35
tests/integration/test_utils.py
Normal file
35
tests/integration/test_utils.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from redis.asyncio import Redis
|
||||
|
||||
from pssecret.utils import get_new_key, save_secret_data
|
||||
|
||||
from ..factories import SecretFactory
|
||||
|
||||
|
||||
async def test_get_new_key_returns_key(redis_server: Redis) -> None:
|
||||
assert isinstance(await get_new_key(redis_server), str)
|
||||
|
||||
|
||||
async def test_get_new_key_returns_free_key(redis_server: Redis) -> None:
|
||||
new_key = await get_new_key(redis_server)
|
||||
res = not await redis_server.exists(new_key)
|
||||
assert res
|
||||
|
||||
|
||||
@patch("pssecret.utils.uuid4", side_effect=("used", "free"))
|
||||
async def test_get_new_key_skips_used_keys(_, redis_server: Redis) -> None:
|
||||
await redis_server.set("used", "")
|
||||
|
||||
assert await get_new_key(redis_server) == "free"
|
||||
|
||||
|
||||
async def test_save_secret_data(redis_server: Redis) -> None:
|
||||
secret = SecretFactory.build()
|
||||
|
||||
key = await save_secret_data(secret, redis_server)
|
||||
|
||||
redis_data: bytes | None = await redis_server.get(key)
|
||||
|
||||
assert redis_data is not None
|
||||
assert redis_data.decode() == secret.data
|
Loading…
Reference in a new issue