diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3685375..6374072 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks default_language_version: - python: python3.11 + python: python3.12 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/docker-compose.yml b/docker-compose.yml index b322a27..972fa79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,5 +7,6 @@ services: - 27017:27017 restart: on-failure environment: - - MONGODB_ADVERTISED_HOSTNAME=localhost - - ALLOW_EMPTY_PASSWORD=yes + - MONGODB_ADVERTISED_HOSTNAME=${MONGO_HOST} + - MONGODB_ROOT_USER=${MONGO_ROOT_USER} + - MONGODB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD} diff --git a/poetry.lock b/poetry.lock index 04d1e02..21548a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,7 @@ version = "0.6.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, @@ -17,6 +18,7 @@ version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, @@ -28,15 +30,32 @@ sniffio = ">=1.1" [package.extras] doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4) ; python_version < \"3.8\"", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; python_version < \"3.12\" and platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (<0.22)"] +[[package]] +name = "asttokens" +version = "3.0.0" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +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 = "certifi" version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, @@ -48,6 +67,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -59,6 +79,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -73,10 +94,24 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "decorator" +version = "5.2.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, +] [[package]] name = "distlib" @@ -84,6 +119,7 @@ version = "0.3.7" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, @@ -95,6 +131,7 @@ version = "2.4.2" description = "DNS toolkit" optional = false python-versions = ">=3.8,<4.0" +groups = ["main"] files = [ {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, @@ -108,12 +145,28 @@ idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.23)"] wmi = ["wmi (>=1.5.1,<2.0.0)"] +[[package]] +name = "executing" +version = "2.2.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, + {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] + [[package]] name = "fastapi" version = "0.104.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, @@ -134,6 +187,7 @@ version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, @@ -142,7 +196,7 @@ files = [ [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] +typing = ["typing-extensions (>=4.8) ; python_version < \"3.11\""] [[package]] name = "h11" @@ -150,6 +204,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -161,6 +216,7 @@ version = "1.0.2" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, @@ -182,6 +238,7 @@ version = "0.25.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"}, {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"}, @@ -195,7 +252,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -206,6 +263,7 @@ version = "2.5.31" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "identify-2.5.31-py2.py3-none-any.whl", hash = "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"}, {file = "identify-2.5.31.tar.gz", hash = "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75"}, @@ -220,6 +278,7 @@ version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, @@ -231,17 +290,117 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["main"] 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 = "ipdb" +version = "0.13.13" +description = "IPython-enabled pdb" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, + {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, +] + +[package.dependencies] +decorator = {version = "*", markers = "python_version >= \"3.11\""} +ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} + +[[package]] +name = "ipython" +version = "9.4.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.11" +groups = ["dev"] +files = [ + {file = "ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066"}, + {file = "ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +ipython-pygments-lexers = "*" +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" + +[package.extras] +all = ["ipython[doc,matplotlib,test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"] +matplotlib = ["matplotlib"] +test = ["packaging", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +description = "Defines a variety of Pygments lexers for highlighting IPython code." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, + {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, +] + +[package.dependencies] +pygments = "*" + +[[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" +groups = ["dev"] +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 = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +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 = "motor" version = "3.3.2" description = "Non-blocking MongoDB driver for Tornado or asyncio" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "motor-3.3.2-py3-none-any.whl", hash = "sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953"}, {file = "motor-3.3.2.tar.gz", hash = "sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a"}, @@ -266,6 +425,7 @@ version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +groups = ["main"] files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, @@ -280,17 +440,51 @@ version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +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 = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +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 = "platformdirs" version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, @@ -306,6 +500,7 @@ version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, @@ -321,6 +516,7 @@ version = "3.5.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, @@ -333,12 +529,56 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +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 = "*" +groups = ["dev"] +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.5.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pydantic-2.5.1-py3-none-any.whl", hash = "sha256:dc5244a8939e0d9a68f1f1b5f550b2e1c879912033b1becbedb315accc75441b"}, {file = "pydantic-2.5.1.tar.gz", hash = "sha256:0b8be5413c06aadfbe56f6dc1d45c9ed25fd43264414c571135c97dd77c2bedb"}, @@ -358,6 +598,7 @@ version = "2.14.3" description = "" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pydantic_core-2.14.3-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:ba44fad1d114539d6a1509966b20b74d2dec9a5b0ee12dd7fd0a1bb7b8785e5f"}, {file = "pydantic_core-2.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a70d23eedd88a6484aa79a732a90e36701048a1509078d1b59578ef0ea2cdf5"}, @@ -475,6 +716,7 @@ version = "2.1.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, @@ -484,12 +726,28 @@ files = [ pydantic = ">=2.3.0" python-dotenv = ">=0.21.0" +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pymongo" version = "4.6.0" description = "Python driver for MongoDB " optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pymongo-4.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c011bd5ad03cc096f99ffcfdd18a1817354132c1331bed7a837a25226659845f"}, {file = "pymongo-4.6.0-cp310-cp310-manylinux1_i686.whl", hash = "sha256:5e63146dbdb1eac207464f6e0cfcdb640c9c5ff0f57b754fa96fe252314a1dc6"}, @@ -521,6 +779,7 @@ files = [ {file = "pymongo-4.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab6bcc8e424e07c1d4ba6df96f7fb963bcb48f590b9456de9ebd03b88084fe8"}, {file = "pymongo-4.6.0-cp312-cp312-win32.whl", hash = "sha256:47aa128be2e66abd9d1a9b0437c62499d812d291f17b55185cb4aa33a5f710a4"}, {file = "pymongo-4.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:014e7049dd019a6663747ca7dae328943e14f7261f7c1381045dfc26a04fa330"}, + {file = "pymongo-4.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e24025625bad66895b1bc3ae1647f48f0a92dd014108fb1be404c77f0b69ca67"}, {file = "pymongo-4.6.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:288c21ab9531b037f7efa4e467b33176bc73a0c27223c141b822ab4a0e66ff2a"}, {file = "pymongo-4.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:747c84f4e690fbe6999c90ac97246c95d31460d890510e4a3fa61b7d2b87aa34"}, {file = "pymongo-4.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:055f5c266e2767a88bb585d01137d9c7f778b0195d3dbf4a487ef0638be9b651"}, @@ -579,9 +838,9 @@ dnspython = ">=1.16.0,<3.0.0" [package.extras] aws = ["pymongo-auth-aws (<2.0.0)"] -encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] -gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] -ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +encryption = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] +gssapi = ["pykerberos ; os_name != \"nt\"", "winkerberos (>=0.5.0) ; os_name == \"nt\""] +ocsp = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] snappy = ["python-snappy"] test = ["pytest (>=7)"] zstd = ["zstandard"] @@ -592,6 +851,7 @@ version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, @@ -612,6 +872,7 @@ version = "0.21.1" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, @@ -630,6 +891,7 @@ version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, @@ -644,6 +906,7 @@ version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, @@ -663,6 +926,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -703,6 +967,7 @@ version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, @@ -710,7 +975,7 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -719,17 +984,39 @@ version = "1.3.0" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +groups = ["dev"] +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.27.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, @@ -741,12 +1028,29 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +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 = "typing-extensions" version = "4.8.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, @@ -758,6 +1062,7 @@ version = "0.24.0.post1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, @@ -768,7 +1073,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "virtualenv" @@ -776,6 +1081,7 @@ version = "20.24.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, @@ -788,9 +1094,21 @@ platformdirs = ">=3.9.1,<4" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] [metadata] -lock-version = "2.0" -python-versions = "^3.12" -content-hash = "00486b0cd88562c8de0e54b8782a827b1515d95a612016742349956c76b1eed1" +lock-version = "2.1" +python-versions = ">=3.12,<4.0" +content-hash = "511eb5a9b8f545e6ebedfe7f593fa518184d91a2e7bb01032d21b99f02335978" diff --git a/pyproject.toml b/pyproject.toml index b0a80ea..7aba624 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,13 @@ [tool.poetry] -name = "tdd project" +name = "store" version = "0.0.1" description = "" authors = ["Nayanna Nara "] readme = "README.md" +# package-mode = false [tool.poetry.dependencies] -python = "^3.12" +python = ">=3.12,<4.0" fastapi = "^0.104.1" uvicorn = "^0.24.0.post1" pydantic = "^2.5.1" @@ -17,13 +18,12 @@ pytest-asyncio = "^0.21.1" pre-commit = "^3.5.0" httpx = "^0.25.1" +[tool.poetry.group.dev.dependencies] +ipdb = "^0.13.13" + [tool.pytest.ini_options] asyncio_mode = "auto" -addopts = [ - "--strict-config", - "--strict-markers", - "--ignore=docs_src", -] +addopts = ["--strict-config", "--strict-markers", "--ignore=docs_src"] xfail_strict = true junit_family = "xunit2" diff --git a/store/controllers/product.py b/store/controllers/product.py index 39a8562..6c7d8c1 100644 --- a/store/controllers/product.py +++ b/store/controllers/product.py @@ -1,7 +1,8 @@ -from typing import List -from fastapi import APIRouter, Body, Depends, HTTPException, Path, status +from decimal import Decimal +from typing import List, Optional +from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status from pydantic import UUID4 -from store.core.exceptions import NotFoundException +from store.core.exceptions import InsertionException, NotFoundException from store.schemas.product import ProductIn, ProductOut, ProductUpdate, ProductUpdateOut from store.usecases.product import ProductUsecase @@ -13,7 +14,10 @@ async def post( body: ProductIn = Body(...), usecase: ProductUsecase = Depends() ) -> ProductOut: - return await usecase.create(body=body) + try: + return await usecase.create(body=body) + except InsertionException as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=exc.message) @router.get(path="/{id}", status_code=status.HTTP_200_OK) @@ -27,8 +31,12 @@ async def get( @router.get(path="/", status_code=status.HTTP_200_OK) -async def query(usecase: ProductUsecase = Depends()) -> List[ProductOut]: - return await usecase.query() +async def query( + min_price: Optional[Decimal] = Query(None, description="Minimum price filter"), + max_price: Optional[Decimal] = Query(None, description="Maximum price filter"), + usecase: ProductUsecase = Depends(), +) -> List[ProductOut]: + return await usecase.query(min_price=min_price, max_price=max_price) @router.patch(path="/{id}", status_code=status.HTTP_200_OK) @@ -36,8 +44,11 @@ async def patch( id: UUID4 = Path(alias="id"), body: ProductUpdate = Body(...), usecase: ProductUsecase = Depends(), -) -> ProductUpdateOut: - return await usecase.update(id=id, body=body) +) -> Optional[ProductUpdateOut]: + try: + return await usecase.update(id=id, body=body) + except NotFoundException as exc: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=exc.message) @router.delete(path="/{id}", status_code=status.HTTP_204_NO_CONTENT) diff --git a/store/core/config.py b/store/core/config.py index c812740..00b8e4c 100644 --- a/store/core/config.py +++ b/store/core/config.py @@ -5,9 +5,20 @@ class Settings(BaseSettings): PROJECT_NAME: str = "Store API" ROOT_PATH: str = "/" - DATABASE_URL: str + MONGO_HOST: str + MONGO_ROOT_USER: str + MONGO_ROOT_PASSWORD: str + MONGO_DB_PORT: int = 27017 + MONGO_DB_NAME: str = "banco_store" + + @property + def DATABASE_URL(self) -> str: + return ( + f"mongodb://{self.MONGO_ROOT_USER}:{self.MONGO_ROOT_PASSWORD}" + f"@{self.MONGO_HOST}:{self.MONGO_DB_PORT}/{self.MONGO_DB_NAME}?authSource=admin" + ) model_config = SettingsConfigDict(env_file=".env") -settings = Settings() +settings = Settings() # type: ignore diff --git a/store/core/exceptions.py b/store/core/exceptions.py index 72c969e..0dbafe7 100644 --- a/store/core/exceptions.py +++ b/store/core/exceptions.py @@ -8,3 +8,7 @@ def __init__(self, message: str | None = None) -> None: class NotFoundException(BaseException): message = "Not Found" + + +class InsertionException(BaseException): + message = "Falha ao inserir produto" diff --git a/store/db/mongo.py b/store/db/mongo.py index 2e96b45..f3733e1 100644 --- a/store/db/mongo.py +++ b/store/db/mongo.py @@ -5,9 +5,11 @@ class MongoClient: def __init__(self) -> None: - self.client: AsyncIOMotorClient = AsyncIOMotorClient(settings.DATABASE_URL) + self.client: "AsyncIOMotorClient" = AsyncIOMotorClient( # type: ignore + settings.DATABASE_URL, uuidRepresentation="standard" + ) - def get(self) -> AsyncIOMotorClient: + def get(self) -> "AsyncIOMotorClient": # type: ignore return self.client diff --git a/store/models/base.py b/store/models/base.py index 16d5b43..3246453 100644 --- a/store/models/base.py +++ b/store/models/base.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal from typing import Any import uuid @@ -8,8 +8,8 @@ class CreateBaseModel(BaseModel): id: UUID4 = Field(default_factory=uuid.uuid4) - created_at: datetime = Field(default_factory=datetime.utcnow) - updated_at: datetime = Field(default_factory=datetime.utcnow) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) @model_serializer def set_model(self) -> dict[str, Any]: diff --git a/store/schemas/base.py b/store/schemas/base.py index a2223aa..ed8a752 100644 --- a/store/schemas/base.py +++ b/store/schemas/base.py @@ -1,12 +1,14 @@ -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal from bson import Decimal128 from pydantic import UUID4, BaseModel, Field, model_validator class BaseSchemaMixin(BaseModel): - class Config: - from_attributes = True + model_config = { + "from_attributes": True, + "json_encoders": {Decimal128: lambda v: str(Decimal(str(v)))}, + } class OutSchema(BaseModel): @@ -19,5 +21,6 @@ def set_schema(cls, data): for key, value in data.items(): if isinstance(value, Decimal128): data[key] = Decimal(str(value)) - + elif isinstance(value, datetime) and value.tzinfo is None: + data[key] = value.replace(tzinfo=timezone.utc) return data diff --git a/store/schemas/product.py b/store/schemas/product.py index e6f6d26..aa57909 100644 --- a/store/schemas/product.py +++ b/store/schemas/product.py @@ -1,3 +1,4 @@ +from datetime import datetime from decimal import Decimal from typing import Annotated, Optional from bson import Decimal128 @@ -31,6 +32,7 @@ class ProductUpdate(BaseSchemaMixin): quantity: Optional[int] = Field(None, description="Product quantity") price: Optional[Decimal_] = Field(None, description="Product price") status: Optional[bool] = Field(None, description="Product status") + updated_at: Optional[datetime] = Field(None, description="Product update timestamp") class ProductUpdateOut(ProductOut): diff --git a/store/usecases/product.py b/store/usecases/product.py index 143bb78..7965ad2 100644 --- a/store/usecases/product.py +++ b/store/usecases/product.py @@ -1,20 +1,29 @@ -from typing import List +from decimal import Decimal +from typing import List, Optional from uuid import UUID +from datetime import datetime, timezone +from bson import Decimal128 from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase import pymongo from store.db.mongo import db_client from store.models.product import ProductModel from store.schemas.product import ProductIn, ProductOut, ProductUpdate, ProductUpdateOut -from store.core.exceptions import NotFoundException +from store.core.exceptions import InsertionException, NotFoundException class ProductUsecase: def __init__(self) -> None: - self.client: AsyncIOMotorClient = db_client.get() - self.database: AsyncIOMotorDatabase = self.client.get_database() + client = db_client.get() + database = client.get_database() + self.client: "AsyncIOMotorClient" = client # type: ignore + self.database: "AsyncIOMotorDatabase" = database # type: ignore self.collection = self.database.get_collection("products") async def create(self, body: ProductIn) -> ProductOut: + existing = await self.collection.find_one({"name": body.name}) + if existing: + raise InsertionException(f"Produto de nome '{body.name}' já existe.") + product_model = ProductModel(**body.model_dump()) await self.collection.insert_one(product_model.model_dump()) @@ -28,15 +37,41 @@ async def get(self, id: UUID) -> ProductOut: return ProductOut(**result) - async def query(self) -> List[ProductOut]: - return [ProductOut(**item) async for item in self.collection.find()] + async def query( + self, min_price: Optional[Decimal] = None, max_price: Optional[Decimal] = None + ) -> List[ProductOut]: + # Converte os valores Decimal para Decimal128 + query = {} + + if min_price is not None or max_price is not None: + price_query = {} + if min_price is not None: + price_query["$gte"] = Decimal128(str(min_price)) + if max_price is not None: + price_query["$lte"] = Decimal128(str(max_price)) + query["price"] = price_query + + # Executa a consulta diretamente + cursor = self.collection.find(query) + return [ProductOut(**item) async for item in cursor] async def update(self, id: UUID, body: ProductUpdate) -> ProductUpdateOut: + update_data = body.model_dump(exclude_none=True) + if "updated_at" in update_data: + if isinstance(update_data["updated_at"], str): + update_data["updated_at"] = datetime.fromisoformat( + update_data["updated_at"] + ) + else: + update_data["updated_at"] = datetime.now(timezone.utc) + result = await self.collection.find_one_and_update( filter={"id": id}, - update={"$set": body.model_dump(exclude_none=True)}, + update={"$set": update_data}, return_document=pymongo.ReturnDocument.AFTER, ) + if not result: + raise NotFoundException(message=f"Produto não encontrado com id : {id}") return ProductUpdateOut(**result) @@ -47,7 +82,7 @@ async def delete(self, id: UUID) -> bool: result = await self.collection.delete_one({"id": id}) - return True if result.deleted_count > 0 else False + return result.deleted_count > 0 product_usecase = ProductUsecase() diff --git a/tests/conftest.py b/tests/conftest.py index 1ea00e8..e253a6a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from store.schemas.product import ProductIn, ProductUpdate from store.usecases.product import product_usecase from tests.factories import product_data, products_data -from httpx import AsyncClient +import httpx @pytest.fixture(scope="session") @@ -33,11 +33,11 @@ async def clear_collections(mongo_client): @pytest.fixture -async def client() -> AsyncClient: +async def client() -> "httpx.AsyncClient": # type: ignore from store.main import app - async with AsyncClient(app=app, base_url="http://test") as ac: - yield ac + async with httpx.AsyncClient(app=app, base_url="http://test") as ac: + yield ac # pyright: ignore[reportReturnType] @pytest.fixture @@ -52,12 +52,12 @@ def product_id() -> UUID: @pytest.fixture def product_in(product_id): - return ProductIn(**product_data(), id=product_id) + return ProductIn(**product_data(), id=product_id) # type: ignore @pytest.fixture def product_up(product_id): - return ProductUpdate(**product_data(), id=product_id) + return ProductUpdate(**product_data(), id=product_id) # type: ignore @pytest.fixture diff --git a/tests/controllers/test_product.py b/tests/controllers/test_product.py index 150f725..8a0db75 100644 --- a/tests/controllers/test_product.py +++ b/tests/controllers/test_product.py @@ -1,5 +1,9 @@ +from decimal import Decimal +from random import randint from typing import List - +from datetime import datetime, timedelta, timezone +from uuid import UUID +from httpx import AsyncClient import pytest from tests.factories import product_data from fastapi import status @@ -7,7 +11,6 @@ async def test_controller_create_should_return_success(client, products_url): response = await client.post(products_url, json=product_data()) - content = response.json() del content["created_at"] @@ -23,6 +26,19 @@ async def test_controller_create_should_return_success(client, products_url): } +async def test_controller_create_should_return_fail( + client, products_url, product_inserted +): + product_repeated = product_data() + product_repeated["name"] = product_inserted.name + response = await client.post(products_url, json=product_repeated) + content = response.json() + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert content["detail"] == ( + f"Produto de nome '{product_repeated['name']}' já existe." + ) + + async def test_controller_get_should_return_success( client, products_url, product_inserted ): @@ -43,13 +59,13 @@ async def test_controller_get_should_return_success( } -async def test_controller_get_should_return_not_found(client, products_url): - response = await client.get(f"{products_url}4fd7cd35-a3a0-4c1f-a78d-d24aa81e7dca") +async def test_controller_get_should_return_not_found( + client: AsyncClient, products_url: str, product_id: UUID +): + response = await client.get(f"{products_url}{product_id}") assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json() == { - "detail": "Product not found with filter: 4fd7cd35-a3a0-4c1f-a78d-d24aa81e7dca" - } + assert response.json() == {"detail": f"Product not found with filter: {product_id}"} @pytest.mark.usefixtures("products_inserted") @@ -83,6 +99,100 @@ async def test_controller_patch_should_return_success( } +@pytest.mark.usefixtures("products_inserted") +async def test_controller_patch_should_return_not_found_exception( + client: AsyncClient, products_url: str, product_id: UUID +): + response = await client.patch( + f"{products_url}{product_id}", json={"price": "1.159"} + ) + + content = response.json() + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert content["detail"] == f"Produto não encontrado com id : {product_id}" + + +async def test_update_product_updated_at_auto(client, products_url, product_inserted): + response = await client.patch( + f"{products_url}{product_inserted.id}", json={"price": "7.500", "quantity": 20} + ) + + assert response.status_code == 200 + updated = response.json() + updated_at = datetime.fromisoformat(updated["updated_at"]) + if updated_at.tzinfo is None: + updated_at = updated_at.replace(tzinfo=timezone.utc) + + assert datetime.now(tz=timezone.utc) - updated_at < timedelta(seconds=5) + + +async def test_update_product_with_custom_updated_at( + client, products_url, product_inserted +): + new_updated_at = datetime( + randint(2020, 2025), + randint(1, 12), + randint(1, 28), + randint(0, 24), + randint(0, 59), + randint(0, 59), + tzinfo=timezone.utc, + ).isoformat() + + response = await client.patch( + f"{products_url}{product_inserted.id}", + json={ + "price": "8.99", + "quantity": 30, + "updated_at": new_updated_at, + }, + ) + + assert response.status_code == 200 + updated = response.json() + updated_at_resp_dt = datetime.fromisoformat( + updated["updated_at"].replace("Z", "+00:00") + ) + new_updated_at_dt = datetime.fromisoformat(new_updated_at) + assert updated_at_resp_dt == new_updated_at_dt + + +async def test_update_product_updated_at_behavior_fixed_date( + client, products_url, product_inserted +): + update_payload = {"price": "7500", "quantity": 15} + response = await client.patch( + f"{products_url}{product_inserted.id}", json=update_payload + ) + assert response.status_code == 200 + updated = response.json() + updated_at = datetime.fromisoformat(updated["updated_at"].replace("Z", "+00:00")) + now = datetime.now(timezone.utc) + assert abs((now - updated_at).total_seconds()) < 5 # Tolerância de 5 segundos + + # Atualização enviando updated_at fixo (deve aceitar) + fixed_updated_at = "2023-07-15T10:30:00+00:00" + update_payload_with_fixed = { + "price": "7999", + "quantity": 20, + "updated_at": fixed_updated_at, + } + response = await client.patch( + f"{products_url}{product_inserted.id}", json=update_payload_with_fixed + ) + assert response.status_code == 200 + updated = response.json() + + updated_at_resp = updated["updated_at"] + + updated_at_resp_dt = datetime.fromisoformat(updated_at_resp.replace("Z", "+00:00")) + fixed_updated_at_dt = datetime.fromisoformat(fixed_updated_at) + assert ( + updated_at_resp_dt == fixed_updated_at_dt + ), f"Esperado {fixed_updated_at}, mas recebido {updated_at_resp}" + + async def test_controller_delete_should_return_no_content( client, products_url, product_inserted ): @@ -100,3 +210,121 @@ async def test_controller_delete_should_return_not_found(client, products_url): assert response.json() == { "detail": "Product not found with filter: 4fd7cd35-a3a0-4c1f-a78d-d24aa81e7dca" } + + +@pytest.mark.usefixtures("products_inserted") +async def test_controller_query_with_price_filter_should_return_success( + client: AsyncClient, products_url: str +): + response = await client.get( + products_url, params={"min_price": "5.000", "max_price": "8.000"} + ) + + assert response.status_code == 200 + products = response.json() + + for product in products: + assert "." in product["price"] + price_value = float(product["price"].replace(".", "")) + assert 5000 <= price_value <= 8000 + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_valid_range(client: AsyncClient, products_url: str): + """Testa filtro com range válido""" + response = await client.get( + products_url, params={"min_price": "5.000", "max_price": "8.000"} + ) + + assert response.status_code == status.HTTP_200_OK + products = response.json() + assert len(products) == 5 # Iphone 8 Pro, 9 Pro Max, 10 Pro Max + + for product in products: + price = Decimal(product["price"].replace(".", "")) + assert 5000 <= price <= 8000 + assert product["name"] in [ + "Iphone 8 Pro", + "Iphone 9 Pro Max", + "Iphone 10 Pro Max", + "Iphone 12 Pro Max", + "Iphone 13 Pro Max", + ] + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_only_min(client: AsyncClient, products_url: str): + response = await client.get(products_url, params={"min_price": "7.000"}) + + assert response.status_code == status.HTTP_200_OK + products = response.json() + assert len(products) == 5 + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_only_max(client: AsyncClient, products_url: str): + response = await client.get(products_url, params={"max_price": "6.570"}) + + assert response.status_code == status.HTTP_200_OK + products = response.json() + assert len(products) == 3 + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_exact_value(client: AsyncClient, products_url: str): + response = await client.get( + products_url, params={"min_price": "7.545", "max_price": "7.545"} + ) + + assert response.status_code == status.HTTP_200_OK + products = response.json() + assert len(products) == 1 + assert products[0]["name"] == "Iphone 10 Pro Max" + assert products[0]["price"] == "7.545" + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_invalid_format(client: AsyncClient, products_url: str): + response = await client.get( + products_url, params={"min_price": "5000", "max_price": "8000"} + ) + products = response.json() + assert len(products) == 0 + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_non_existent_range( + client: AsyncClient, products_url: str +): + response = await client.get( + products_url, params={"min_price": "20.000", "max_price": "30.000"} + ) + + assert response.status_code == status.HTTP_200_OK + products = response.json() + assert len(products) == 0 + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_swapped_values(client: AsyncClient, products_url: str): + response = await client.get( + products_url, params={"min_price": "8.000", "max_price": "5.000"} + ) + + assert response.status_code == status.HTTP_200_OK + products = response.json() + assert len(products) == 0 + + +@pytest.mark.usefixtures("products_inserted") +async def test_price_filter_with_status_true(client: AsyncClient, products_url: str): + response = await client.get( + products_url, + params={"min_price": "6.201", "max_price": "8.040"}, + ) + + assert response.status_code == status.HTTP_200_OK + products = response.json() + assert len(products) == 4 + assert all(product["status"] for product in products) diff --git a/tests/factories.py b/tests/factories.py index bbe30fa..a54b293 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -9,13 +9,27 @@ def product_data(): def products_data(): return [ - {"name": "Iphone 11 Pro Max", "quantity": 20, "price": "4.500", "status": True}, - {"name": "Iphone 12 Pro Max", "quantity": 15, "price": "5.500", "status": True}, - {"name": "Iphone 13 Pro Max", "quantity": 5, "price": "6.500", "status": True}, + {"name": "Iphone 7", "quantity": 50, "price": "4.500", "status": True}, + {"name": "Iphone 8 Pro", "quantity": 20, "price": "5.001", "status": True}, + {"name": "Iphone 9 Pro Max", "quantity": 15, "price": "6.569", "status": True}, + {"name": "Iphone 10 Pro Max", "quantity": 3, "price": "7.545", "status": True}, + { + "name": "Iphone 13 Pro Max", + "quantity": 1, + "price": "7.500", + "status": True, + }, + { + "name": "Iphone 12 Pro Max", + "quantity": 1, + "price": "7.999", + "status": True, + }, + {"name": "Iphone 11 Pro Max", "quantity": 2, "price": "9.532", "status": False}, { "name": "Iphone 15 Pro Max", "quantity": 3, - "price": "10.500", + "price": "17.550", "status": False, }, ] diff --git a/tests/usecases/test_product.py b/tests/usecases/test_product.py index 7bdc820..01532b1 100644 --- a/tests/usecases/test_product.py +++ b/tests/usecases/test_product.py @@ -60,3 +60,16 @@ async def test_usecases_delete_should_not_found(): err.value.message == "Product not found with filter: 1e4f214e-85f7-461a-89d0-a751a32e3bb9" ) + + +# @pytest.mark.asyncio +# async def test_controller_create_duplicate_id_should_fail( +# client, products_url, product_inserted +# ): +# duplicate_product = product_inserted.model_dump() +# response = await client.post(products_url, json=duplicate_product) + +# assert response.status_code == 400 +# assert ( +# response.json()["detail"] == f"Produto com ID {product_inserted.id} já existe" +# )