From b31eb43f9ca1db967ad212e3ddb28a37d3fb7aaa Mon Sep 17 00:00:00 2001 From: Mariia Zaiets Date: Fri, 29 Aug 2025 13:52:15 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=BE=D0=B7=D1=80=D0=BE=D0=B1=D0=BA?= =?UTF-8?q?=D0=B0=20API=20=D0=B4=D0=BB=D1=8F=20=D0=A3=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D1=96=D0=BD=D0=BD=D1=8F=20=D0=90=D0=B4=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D0=B0=D0=BC=D0=B8=20=D0=9A=D0=BE=D1=80=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=83=D0=B2=D0=B0=D1=87=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- docker-compose.yml | 1 - poetry.lock | 75 +++++++++++++++++++++++++++++---- src/catalogue/routes.py | 3 ++ src/routes.py | 12 +++--- src/users/models/pydantic.py | 17 +++++++- src/users/repository.py | 33 +++++++++++++-- src/users/routes.py | 3 +- src/users/services.py | 23 ++++++++-- src/users/views/user_address.py | 60 +++++++++++++++++++++++++- 10 files changed, 202 insertions(+), 27 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1410224..9fda0358 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY pyproject.toml pyproject.toml RUN pip install poetry RUN poetry config virtualenvs.create false -RUN poetry install --no-dev +RUN poetry install --no-root COPY . /app diff --git a/docker-compose.yml b/docker-compose.yml index 9c4431fc..ee5c892b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.8' services: web: diff --git a/poetry.lock b/poetry.lock index f35fe6b0..8efd012f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 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 = "aiomysql" @@ -6,6 +6,7 @@ version = "0.2.0" description = "MySQL driver for asyncio." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a"}, {file = "aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67"}, @@ -24,6 +25,7 @@ version = "1.13.1" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, @@ -35,7 +37,7 @@ SQLAlchemy = ">=1.3.0" typing-extensions = ">=4" [package.extras] -tz = ["backports.zoneinfo"] +tz = ["backports.zoneinfo ; python_version < \"3.9\""] [[package]] name = "annotated-types" @@ -43,6 +45,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -54,6 +57,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"}, @@ -65,7 +69,7 @@ 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]] @@ -74,6 +78,7 @@ version = "1.0.0" description = "asyncio sqlalchemy support" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "async_sqlalchemy-1.0.0-py3-none-any.whl", hash = "sha256:211c923ff3a0f885357496c3902ef1bd03af4e74c43758688321cb1a03f44a8e"}, ] @@ -89,6 +94,8 @@ version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.11\"" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, @@ -100,6 +107,7 @@ version = "0.29.0" description = "An asyncio PostgreSQL driver" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, @@ -149,7 +157,7 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} [package.extras] docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.12.0\""] [[package]] name = "bcrypt" @@ -157,6 +165,7 @@ version = "4.1.3" description = "Modern password hashing for your software and your servers" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:48429c83292b57bf4af6ab75809f8f4daf52aa5d480632e53707805cc1ce9b74"}, {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8bea4c152b91fd8319fef4c6a790da5c07840421c2b785084989bf8bbb7455"}, @@ -197,6 +206,7 @@ version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, @@ -208,6 +218,8 @@ version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, @@ -272,6 +284,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"}, @@ -286,6 +299,8 @@ 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"] +markers = "platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -297,6 +312,7 @@ version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, @@ -351,6 +367,7 @@ version = "2.6.1" description = "DNS toolkit" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, @@ -371,6 +388,7 @@ version = "0.19.0" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" +groups = ["main"] files = [ {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, @@ -389,6 +407,7 @@ version = "2.1.2" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "email_validator-2.1.2-py3-none-any.whl", hash = "sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115"}, {file = "email_validator-2.1.2.tar.gz", hash = "sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c"}, @@ -404,6 +423,7 @@ 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"}, @@ -424,6 +444,7 @@ version = "12.1.3" description = "Ready-to-use and customizable users management for FastAPI" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi_users-12.1.3-py3-none-any.whl", hash = "sha256:9faff9fc072df12a9e60595a620f44e0028cc7ecc7130d6048d448befec39a1e"}, {file = "fastapi_users-12.1.3.tar.gz", hash = "sha256:81236f802228e25d1fe4c3006bc5e9d1d651d83147747d6ab19edd81401d09d4"}, @@ -451,6 +472,7 @@ version = "6.0.1" description = "FastAPI Users database adapter for SQLAlchemy" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi_users_db_sqlalchemy-6.0.1-py3-none-any.whl", hash = "sha256:d1050ec31eb75e8c4fa9abafa4addaf0baf5c97afeea2f0f910ea55e2451fcad"}, {file = "fastapi_users_db_sqlalchemy-6.0.1.tar.gz", hash = "sha256:f0ef9fe3250453712d25c13170700c80fa205867ce7add7ef391c384ec27cbe1"}, @@ -466,6 +488,7 @@ version = "3.0.3" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, @@ -537,6 +560,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"}, @@ -548,6 +572,7 @@ version = "1.0.5" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, @@ -569,6 +594,7 @@ version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, @@ -582,7 +608,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.*)"] @@ -593,6 +619,7 @@ version = "0.14.1" description = "Async OAuth client using HTTPX" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx_oauth-0.14.1-py3-none-any.whl", hash = "sha256:3ccf35760d157a9abbf4a78024b744b5532ec5d4d03fc1db3e71123ce9d33e32"}, {file = "httpx_oauth-0.14.1.tar.gz", hash = "sha256:e58c60bce99568b4e98ce88abf0e2111a9b6c7f79c29bea88cc89d9edb78dfbc"}, @@ -607,6 +634,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -618,6 +646,7 @@ version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, @@ -635,6 +664,7 @@ version = "1.15.2" description = "Small library to dynamically create python functions." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "makefun-1.15.2-py2.py3-none-any.whl", hash = "sha256:1c83abfaefb6c3c7c83ed4a993b4a310af80adf6db15625b184b1f0f7545a041"}, {file = "makefun-1.15.2.tar.gz", hash = "sha256:16f2a2b34d9ee0c2b578c960a1808c974e2822cf79f6e9b9c455aace10882d45"}, @@ -646,6 +676,7 @@ version = "1.3.5" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, @@ -665,6 +696,7 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -734,6 +766,7 @@ version = "1.7.4" description = "comprehensive password hashing framework supporting over 30 schemes" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, @@ -754,6 +787,7 @@ version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, @@ -835,6 +869,7 @@ version = "0.6.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, @@ -846,6 +881,8 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -857,6 +894,7 @@ version = "2.7.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, @@ -876,6 +914,7 @@ version = "2.18.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"}, {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"}, @@ -967,6 +1006,7 @@ version = "2.3.3" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_settings-2.3.3-py3-none-any.whl", hash = "sha256:e4ed62ad851670975ec11285141db888fd24947f9440bd4380d7d8788d4965de"}, {file = "pydantic_settings-2.3.3.tar.gz", hash = "sha256:87fda838b64b5039b970cd47c3e8a1ee460ce136278ff672980af21516f6e6ce"}, @@ -986,6 +1026,7 @@ version = "2.8.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, @@ -1006,6 +1047,7 @@ version = "1.1.1" description = "Pure Python MySQL Driver" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, @@ -1021,6 +1063,7 @@ version = "1.0.1" 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.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -1035,6 +1078,7 @@ version = "3.3.0" description = "JOSE implementation in Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, @@ -1057,6 +1101,7 @@ version = "0.0.7" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "python_multipart-0.0.7-py3-none-any.whl", hash = "sha256:b1fef9a53b74c795e2347daac8c54b252d9e0df9c619712691c1cc8021bd3c49"}, {file = "python_multipart-0.0.7.tar.gz", hash = "sha256:288a6c39b06596c1b988bb6794c6fbc80e6c369e35e5062637df256bee0c9af9"}, @@ -1071,6 +1116,7 @@ version = "4.9" description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" +groups = ["main"] files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -1085,6 +1131,7 @@ version = "0.1.15" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, @@ -1111,6 +1158,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1122,6 +1170,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1133,6 +1182,7 @@ version = "0.16.1" description = "SQLAlchemy admin for FastAPI and Starlette" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "sqladmin-0.16.1-py3-none-any.whl", hash = "sha256:eb90d453740a5749e5f9701e1d4d35d4db9c54f5d907f0705873e2aa0ac2913f"}, {file = "sqladmin-0.16.1.tar.gz", hash = "sha256:5d43ddf5bdca05fe5e806d9d328268f96adae37558dbd0aad2fc57e0f660ba83"}, @@ -1154,6 +1204,7 @@ version = "2.0.30" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b48154678e76445c7ded1896715ce05319f74b1e73cf82d4f8b59b46e9c0ddc"}, {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2753743c2afd061bb95a61a51bbb6a1a11ac1c44292fad898f10c9839a7f75b2"}, @@ -1241,6 +1292,7 @@ version = "1.1.0" description = "Internationalization extension for SQLAlchemy models." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "SQLAlchemy-i18n-1.1.0.tar.gz", hash = "sha256:de33376483a581ca14218d8f57a114466c5f72b674a95839b6c4564a6e67796f"}, {file = "SQLAlchemy_i18n-1.1.0-py3-none-any.whl", hash = "sha256:7b9635c6aed9d68c4360a207b72a8fc1b8f8cd6bdba9602c81d1101ccd6894b0"}, @@ -1260,6 +1312,7 @@ version = "0.41.2" description = "Various utility functions for SQLAlchemy." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"}, @@ -1277,8 +1330,8 @@ intervals = ["intervals (>=0.7.1)"] password = ["passlib (>=1.6,<2.0)"] pendulum = ["pendulum (>=2.0.5)"] phone = ["phonenumbers (>=5.9.2)"] -test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo ; python_version < \"3.9\"", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo ; python_version < \"3.9\"", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] timezone = ["python-dateutil"] url = ["furl (>=0.4.1)"] @@ -1288,6 +1341,7 @@ 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"}, @@ -1305,6 +1359,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1316,6 +1371,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"}, @@ -1326,7 +1382,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 = "wtforms" @@ -1334,6 +1390,7 @@ version = "3.1.2" description = "Form validation and rendering for Python web development." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "wtforms-3.1.2-py3-none-any.whl", hash = "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07"}, {file = "wtforms-3.1.2.tar.gz", hash = "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9"}, @@ -1346,6 +1403,6 @@ markupsafe = "*" email = ["email-validator"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" content-hash = "4e057f281bf3093e6070908633a3b3c4e914a481b10e85e0868db1968fd3efa4" diff --git a/src/catalogue/routes.py b/src/catalogue/routes.py index c40fe3ac..120f4c96 100644 --- a/src/catalogue/routes.py +++ b/src/catalogue/routes.py @@ -7,3 +7,6 @@ class CatalogueRoutesPrefixes: class ProductRoutesPrefixes(BaseCrudPrefixes): ... + +class AddressRoutesPrefixes(BaseCrudPrefixes): + ... \ No newline at end of file diff --git a/src/routes.py b/src/routes.py index e893cd4b..3eaf26a2 100644 --- a/src/routes.py +++ b/src/routes.py @@ -1,8 +1,8 @@ class BaseRoutesPrefixes: - swagger: str = '/docs' - redoc: str = '/redoc' - openapi: str = '/openapi.json' + swagger = '/docs' + redoc = '/redoc' + openapi = '/openapi.json' - catalogue: str = '/catalogue' - authentication: str = '/auth' - account: str = '/account' + catalogue = '/catalogue' + authentication = '/auth' + account = '/account' diff --git a/src/users/models/pydantic.py b/src/users/models/pydantic.py index cda96f2a..0777f662 100644 --- a/src/users/models/pydantic.py +++ b/src/users/models/pydantic.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Optional from pydantic import ( BaseModel, @@ -19,3 +19,18 @@ class UserModel(BaseModel): class UserWithPassword(UserModel): hashed_password: str + +class UserAddressListResponse(BaseModel): + id: int + title: str + +class UserAddressDetailResponse(BaseModel): + id: int + title: Optional[str] + city: str + street: str + house: str + apartment: Optional[str] + post_code: Optional[str] + floor: Optional[str] + additional_info: Optional[str] \ No newline at end of file diff --git a/src/users/repository.py b/src/users/repository.py index e53dab38..d0113452 100644 --- a/src/users/repository.py +++ b/src/users/repository.py @@ -8,10 +8,10 @@ from src.common.repository.sqlalchemy import BaseSQLAlchemyRepository from src.users.models.pydantic import ( UserModel, - UserWithPassword, + UserWithPassword, UserAddressDetailResponse, UserAddressListResponse ) -from src.users.models.sqlalchemy import User - +from src.users.models.sqlalchemy import User, UserAddress +from src.common.exceptions.base import ObjectDoesNotExistException class UserRepository(BaseSQLAlchemyRepository[User, UserModel]): def __init__(self, session: AsyncSession): @@ -35,3 +35,30 @@ async def get_by_email(self, email: str) -> Optional[UserWithPassword]: def get_user_repository(session: AsyncSession = Depends(get_session)) -> UserRepository: return UserRepository(session=session) + +class UserAddressRepository(BaseSQLAlchemyRepository[UserAddress, UserAddressDetailResponse]): + def __init__(self, session: AsyncSession): + super().__init__(model=UserAddress, pydantic_model=UserAddressDetailResponse, session=session) + + async def create(self, *args, **kwargs): + raise NotImplementedError + + async def delete(self, *args, **kwargs): + raise NotImplementedError + + async def get_by_user(self, user_id : int) -> list[UserAddressListResponse]: + stmt = select(self.model).where(self.model.user_id == user_id) + result = await self.session.execute(stmt) + addresses = result.scalars().all() + return [UserAddressListResponse.model_validate(a) for a in addresses] + + async def get_detail(self, address_id: int) -> UserAddressDetailResponse: + stmt = select(self.model).where(self.model.id == address_id) + result = await self.session.execute(stmt) + address = result.scalar_one_or_none() + if not address: + raise ObjectDoesNotExistException(f"Address with id {address_id} not found") + return UserAddressDetailResponse.model_validate(address) + +def get_address_repository(session: AsyncSession = Depends(get_session)) -> UserAddressRepository: + return UserAddressRepository(session=session) \ No newline at end of file diff --git a/src/users/routes.py b/src/users/routes.py index 1350e5c2..6fb0575a 100644 --- a/src/users/routes.py +++ b/src/users/routes.py @@ -6,4 +6,5 @@ class UserManagementRoutesPrefixes: class UserRoutesPrefixes(BaseCrudPrefixes): - ... + root = "/addresses" + detail = "/addresses/{address_id}" diff --git a/src/users/services.py b/src/users/services.py index ba7a2d37..d66a2702 100644 --- a/src/users/services.py +++ b/src/users/services.py @@ -5,11 +5,12 @@ from src.authentication.security import verify_password from src.common.service import BaseService from src.users.models.pydantic import ( - UserModel, + UserModel, UserAddressDetailResponse, UserAddressListResponse ) +from src.users.models.sqlalchemy import UserAddress from src.users.repository import ( - UserRepository, - get_user_repository, + UserRepository, UserAddressRepository, + get_user_repository, get_address_repository ) @@ -31,3 +32,19 @@ async def authenticate(self, email: str, password: str) -> Optional[UserModel]: def get_user_service(repo: UserRepository = Depends(get_user_repository)) -> UserService: return UserService(repository=repo) + +class UserAddressService(BaseService[UserAddress]): + def __init__(self, repository: UserAddressRepository): + super().__init__(repository) + + async def get_by_user_list(self, user_id: int): + return await self.repository.get_by_user(user_id=user_id) + + async def get_detail_for_user(self, address_id: int, user_id: int): + address = await self.repository.get_detail(address_id=address_id) + if address.user_id != user_id: + raise PermissionError("Forbidden") + return UserAddressDetailResponse.model_validate(address) + +def get_address_service(repo: UserAddressRepository = Depends(get_address_repository)) -> UserAddressService: + return UserAddressService(repository=repo) \ No newline at end of file diff --git a/src/users/views/user_address.py b/src/users/views/user_address.py index 5b05453d..d4d44e95 100644 --- a/src/users/views/user_address.py +++ b/src/users/views/user_address.py @@ -11,14 +11,17 @@ ) from src.catalogue.models.pydantic import ProductModel +from src.users.models.sqlalchemy import UserAddress +from src.users.models.pydantic import UserAddressListResponse, UserAddressDetailResponse from src.catalogue.routes import ( CatalogueRoutesPrefixes, - ProductRoutesPrefixes, + ProductRoutesPrefixes, AddressRoutesPrefixes, ) from src.catalogue.services import get_product_service +from src.users.services import get_address_service, UserAddressService from src.common.exceptions.base import ObjectDoesNotExistException from src.common.schemas.common import ErrorResponse - +from src.authentication.utils import get_current_user router = APIRouter(prefix=CatalogueRoutesPrefixes.product) @@ -65,3 +68,56 @@ async def product_detail( return ErrorResponse(message=exc.message) return response + + + + + + +@router.get( + AddressRoutesPrefixes.root, + status_code=status.HTTP_200_OK, + response_model=list[UserAddressListResponse], +) +async def address_list( + address_service: Annotated[UserAddressService, Depends(get_address_service)], + current_user = Depends(get_current_user), +) -> list[UserAddressListResponse]: + """ + Get list of addresses for the current user. + """ + addresses = await address_service.get_by_user_list(user_id=current_user.id) + return addresses + + +@router.get( + AddressRoutesPrefixes.detail, + responses={ + status.HTTP_200_OK: {'model': UserAddress}, + status.HTTP_404_NOT_FOUND: {'model': ErrorResponse}, + }, + status_code=status.HTTP_200_OK, + response_model=Union[UserAddressDetailResponse, ErrorResponse], +) +async def address_detail( + address_id: int, + response: Response, + address_service: Annotated[UserAddressService, Depends(get_address_service)], + current_user = Depends(get_current_user), +) -> Union[UserAddressDetailResponse, ErrorResponse]: + """ + Retrieve address details for the current user. + """ + try: + address = await address_service.get_detail_for_user( + address_id=address_id, + user_id=current_user.id + ) + except ObjectDoesNotExistException as exc: + response.status_code = status.HTTP_404_NOT_FOUND + return ErrorResponse(message=exc.message) + except PermissionError: + response.status_code = status.HTTP_403_FORBIDDEN + return ErrorResponse(message="Forbidden") + + return address \ No newline at end of file