From d707c8b3299c4dae540a97f83fa9ea3e552fe4a0 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Sun, 6 Sep 2026 15:40:08 +0300 Subject: [PATCH 1/8] rabbitmq docker instance --- .gitignore | 2 ++ configs/.env.example | 6 +++++ docker/compose-dev.yaml | 33 ++++++++++++++++++++++++++-- makefile | 4 ++-- src/messaging/__init__.py | 1 + src/messaging/consumers/consumers.py | 0 src/messaging/producers/producers.py | 0 src/messaging/queue/queue.py | 0 src/messaging/rabbitmq/rabbitmq.py | 0 9 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/messaging/__init__.py create mode 100644 src/messaging/consumers/consumers.py create mode 100644 src/messaging/producers/producers.py create mode 100644 src/messaging/queue/queue.py create mode 100644 src/messaging/rabbitmq/rabbitmq.py diff --git a/.gitignore b/.gitignore index 3d509c5..41df06c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ Thumbs.db #db *.db DB/ +#rabbitmq +RTMQ/ #logs logs/ diff --git a/configs/.env.example b/configs/.env.example index e20e27c..27b7172 100644 --- a/configs/.env.example +++ b/configs/.env.example @@ -13,4 +13,10 @@ REDIS_PASSWORD="change_me" REDIS_PORT=change_me #int REDIS_HOST="change_me" + +RABBITMQ_PASSWORD="change_me" +RABBITMQ_LOGIN="change_me" +RABBITMQ_PORT=change_me #int +RABBITMQ_PORT_UI=chane_me #int + PROD_MODE=bool \ No newline at end of file diff --git a/docker/compose-dev.yaml b/docker/compose-dev.yaml index 3626ae1..67bdc00 100644 --- a/docker/compose-dev.yaml +++ b/docker/compose-dev.yaml @@ -39,6 +39,8 @@ services: condition: service_healthy redis: condition: service_healthy + rabbitmq: + condition: service_healthy ports: - "80:8000" entrypoint: ["./entrypoint.sh", "--dev"] @@ -76,11 +78,13 @@ services: condition: service_healthy redis: condition: service_healthy + rabbitmq: + condition: service_healthy ports: - "80:8000" psql: - profiles: ["prod", "dev", "db"] + profiles: ["prod", "dev", "local"] image: postgres:16-alpine container_name: psql init: true @@ -106,7 +110,7 @@ services: redis: image: redis:latest - profiles: ["prod", "dev", "redis"] + profiles: ["prod", "dev", "local"] container_name: redis init: true ipc: private @@ -121,6 +125,31 @@ services: restart: unless-stopped networks: - backend + + rabbitmq: + image: rabbitmq:3-management-alpine + profiles: ["prod", "dev", "local"] + container_name: rabbitmq_broker + restart: always + init: true + ipc: private + environment: + - RABBITMQ_DEFAULT_USER=${RABBITMQ_LOGIN} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD} + ports: + - "${RABBITMQ_PORT}:5672" + - "${RABBITMQ_PORT_UI}:15672" + volumes: + - type: bind + source: ../RTMQ + target: /var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - backend networks: backend: diff --git a/makefile b/makefile index a7cd129..239de41 100644 --- a/makefile +++ b/makefile @@ -17,11 +17,11 @@ help: ## .PHONY: run run: ## Run dev local application - docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile db --profile redis up -d && ${VENV} uvicorn main:app --host 0.0.0.0 --port 8000 --reload + docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile local up -d && ${VENV} uvicorn main:app --host 0.0.0.0 --port 8000 --reload .PHONY: down down: ## Down dev local db - docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile db --profile redis down + docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile local down .PHONY: run-dev run-dev: ## Run dev application diff --git a/src/messaging/__init__.py b/src/messaging/__init__.py new file mode 100644 index 0000000..73d04e6 --- /dev/null +++ b/src/messaging/__init__.py @@ -0,0 +1 @@ +# rabbitmq code \ No newline at end of file diff --git a/src/messaging/consumers/consumers.py b/src/messaging/consumers/consumers.py new file mode 100644 index 0000000..e69de29 diff --git a/src/messaging/producers/producers.py b/src/messaging/producers/producers.py new file mode 100644 index 0000000..e69de29 diff --git a/src/messaging/queue/queue.py b/src/messaging/queue/queue.py new file mode 100644 index 0000000..e69de29 diff --git a/src/messaging/rabbitmq/rabbitmq.py b/src/messaging/rabbitmq/rabbitmq.py new file mode 100644 index 0000000..e69de29 From 10ce1755bde815ab83201e59f14c33bfd79aacf0 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Sun, 6 Sep 2026 18:12:59 +0300 Subject: [PATCH 2/8] producers, consumers, rabbit_client --- daemon_run.py | 11 + main.py | 4 +- poetry.lock | 319 ++++++++++++++++-- pyproject.toml | 3 +- .../queue/queue.py => daemons/__init__.py} | 0 .../rabbitmq/rabbitmq.py => daemons/base.py} | 0 src/daemons/email_consumer.py | 0 src/daemons/registry.py | 0 src/fake/__init__.py | 1 - src/messaging/consumers/consumers.py | 35 ++ src/messaging/producers/producers.py | 33 ++ src/messaging/rabbitmq_client.py | 35 ++ src/models/configs_read/env.py | 6 + src/web/protected_routes/auth_routes.py | 4 + 14 files changed, 428 insertions(+), 23 deletions(-) create mode 100644 daemon_run.py rename src/{messaging/queue/queue.py => daemons/__init__.py} (100%) rename src/{messaging/rabbitmq/rabbitmq.py => daemons/base.py} (100%) create mode 100644 src/daemons/email_consumer.py create mode 100644 src/daemons/registry.py delete mode 100644 src/fake/__init__.py create mode 100644 src/messaging/rabbitmq_client.py diff --git a/daemon_run.py b/daemon_run.py new file mode 100644 index 0000000..354f527 --- /dev/null +++ b/daemon_run.py @@ -0,0 +1,11 @@ +import asyncio + +from src.messaging.consumers.consumers import email_consumer + + +async def main() -> None: + await email_consumer.start_consuming() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/main.py b/main.py index 3ab7fd7..1bdc874 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,12 @@ from contextlib import asynccontextmanager from pathlib import Path -# import uvicorn from fastapi import FastAPI from src.cache.redis_client import redis_client from src.database.users.crud import Seed from src.logging.logger import LoggingMiddleware, ProcessingTimeMiddleware +from src.messaging.rabbitmq_client import rabbitmq_client from src.web.protected_routes.auth_routes import router as protected_router from src.web.protected_routes.protected_user_action_routes import ( router as protected_user_action_routes, @@ -19,7 +19,7 @@ async def lifespan(app: FastAPI): await create_first_user() yield await redis_client.close() - + await rabbitmq_client.close() app=FastAPI(root_path="/", lifespan=lifespan) app.add_middleware(LoggingMiddleware) diff --git a/poetry.lock b/poetry.lock index 1363908..b487e66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,21 @@ # This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +[[package]] +name = "aio-pika" +version = "10.0.1" +description = "Wrapper around the aiormq for asyncio and humans" +optional = false +python-versions = "<4,>=3.11" +groups = ["main"] +files = [ + {file = "aio_pika-10.0.1-py3-none-any.whl", hash = "sha256:12120a3cf8022d2a8bc5dc89e716512a38bf742c24c5562f54764af27eec7edd"}, + {file = "aio_pika-10.0.1.tar.gz", hash = "sha256:96ec3ef748ca7a25a9d2fa6e511c16c3ffcfa6b1f40ade79b8a5baabba682efd"}, +] + +[package.dependencies] +aiormq = ">=7,<8" +yarl = "*" + [[package]] name = "aiofiles" version = "25.1.0" @@ -12,6 +28,22 @@ files = [ {file = "aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2"}, ] +[[package]] +name = "aiormq" +version = "7.0.0" +description = "Pure python AMQP asynchronous client library" +optional = false +python-versions = "<4,>=3.11" +groups = ["main"] +files = [ + {file = "aiormq-7.0.0-py3-none-any.whl", hash = "sha256:df49bb2282e5374a28507c4c43948e8c8e5321590f2998781c2d90a34e100789"}, + {file = "aiormq-7.0.0.tar.gz", hash = "sha256:f524121f1afbb875f50235b2748f81331e3be47542ee600e83c321c4e97ea168"}, +] + +[package.dependencies] +pamqp = ">=4,<5" +yarl = "*" + [[package]] name = "alembic" version = "1.18.5" @@ -1124,7 +1156,7 @@ version = "6.7.1" description = "multidict implementation" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, @@ -1340,6 +1372,21 @@ files = [ {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, ] +[[package]] +name = "pamqp" +version = "4.0.1" +description = "RabbitMQ Focused AMQP low-level library" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "pamqp-4.0.1-py3-none-any.whl", hash = "sha256:a547f45128b06e42ce8d7a739b0cfcc40f2c724770622eaaff4a3f587b1cf7d0"}, + {file = "pamqp-4.0.1.tar.gz", hash = "sha256:9dd13b828e346622793981f14a5df817fce5de998c746209d6c0154eb8403970"}, +] + +[package.extras] +codegen = ["lxml", "requests", "yapf"] + [[package]] name = "pandas" version = "3.0.3" @@ -1507,6 +1554,137 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "propcache" +version = "0.5.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, +] + [[package]] name = "psutil" version = "7.2.2" @@ -2107,21 +2285,6 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] -[[package]] -name = "rsa" -version = "4.2" -description = "Pure-Python RSA implementation" -optional = false -python-versions = "*" -groups = ["main"] -markers = "python_version >= \"3.14\"" -files = [ - {file = "rsa-4.2.tar.gz", hash = "sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - [[package]] name = "rsa" version = "4.9.1" @@ -2129,7 +2292,6 @@ description = "Pure-Python RSA implementation" optional = false python-versions = "<4,>=3.6" groups = ["main"] -markers = "python_version == \"3.13\"" files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -2417,7 +2579,126 @@ files = [ {file = "wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda"}, ] +[[package]] +name = "yarl" +version = "1.24.5" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + [metadata] lock-version = "2.1" -python-versions = ">=3.13" -content-hash = "30e907fcd3da88ca5c08edcb5cf49e0011f4cc702052583dd4496ca89b40e26b" +python-versions = ">=3.13,<4.0" +content-hash = "afc1aa09280c992138f9bc4c3edb89aa146e04bbf5c045f18d5c886a5b735cc3" diff --git a/pyproject.toml b/pyproject.toml index 841d794..c4b22ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MH.Dmitrii's project" readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.13,<4.0" dependencies = [ "alembic (>=1.18.5,<2.0.0)", @@ -26,6 +26,7 @@ dependencies = [ "asyncpg (>=0.31.0,<0.32.0)", "psycopg2-binary (>=2.9.12,<3.0.0)", "redis (>=8.1.0,<9.0.0)", + "aio-pika (>=10.0.1,<11.0.0)", ] [tool.poetry.group.dev.dependencies] diff --git a/src/messaging/queue/queue.py b/src/daemons/__init__.py similarity index 100% rename from src/messaging/queue/queue.py rename to src/daemons/__init__.py diff --git a/src/messaging/rabbitmq/rabbitmq.py b/src/daemons/base.py similarity index 100% rename from src/messaging/rabbitmq/rabbitmq.py rename to src/daemons/base.py diff --git a/src/daemons/email_consumer.py b/src/daemons/email_consumer.py new file mode 100644 index 0000000..e69de29 diff --git a/src/daemons/registry.py b/src/daemons/registry.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fake/__init__.py b/src/fake/__init__.py deleted file mode 100644 index f2f481f..0000000 --- a/src/fake/__init__.py +++ /dev/null @@ -1 +0,0 @@ -'''fake data for tests''' \ No newline at end of file diff --git a/src/messaging/consumers/consumers.py b/src/messaging/consumers/consumers.py index e69de29..6ac0451 100644 --- a/src/messaging/consumers/consumers.py +++ b/src/messaging/consumers/consumers.py @@ -0,0 +1,35 @@ +import json + +from src.messaging.rabbitmq_client import rabbitmq_client + + +class EmailConsumer: + + def __init__(self) -> None: + self.channel = None + self.queue = None + + async def setup(self)->None: + + self.channel = await rabbitmq_client.get_channel() + await self.channel.set_qos(prefetch_count=10) + self.queue = await self.channel.declare_queue("email_queue", durable=True) + + async def process_message(self, message) -> None: + async with message.process(): + data = json.loads(message.body) + print(f"Отправляю email на {data['email']}") + + async def start_consuming(self)->None: + + if self.queue is None: + await self.setup() + + if self.channel is None or self.queue is None: + raise RuntimeError("Failed to set up RabbitMQ channel/queue") + + async with self.queue.iterator() as queue_iter: + async for message in queue_iter: + await self.process_message(message) + +email_consumer=EmailConsumer() \ No newline at end of file diff --git a/src/messaging/producers/producers.py b/src/messaging/producers/producers.py index e69de29..6ed6e1d 100644 --- a/src/messaging/producers/producers.py +++ b/src/messaging/producers/producers.py @@ -0,0 +1,33 @@ +import json + +import aio_pika + +from src.messaging.rabbitmq_client import rabbitmq_client + + +class EmailProducer: + + def __init__(self) -> None: + self.channel = None + self.queue = None + + async def setup(self)->None: + + self.channel = await rabbitmq_client.get_channel() + self.queue = await self.channel.declare_queue("email_queue", durable=True) + + async def send_welcome_email(self, email:str)->None: + + if self.queue is None: + await self.setup() + + if self.channel is None or self.queue is None: + raise RuntimeError("Failed to set up RabbitMQ channel/queue") + + message=aio_pika.Message( + body=json.dumps({"email": email}).encode(), + delivery_mode=aio_pika.DeliveryMode.PERSISTENT, + ) + await self.channel.default_exchange.publish(message=message, routing_key=self.queue.name) + +email_producer=EmailProducer() \ No newline at end of file diff --git a/src/messaging/rabbitmq_client.py b/src/messaging/rabbitmq_client.py new file mode 100644 index 0000000..6c4fdb5 --- /dev/null +++ b/src/messaging/rabbitmq_client.py @@ -0,0 +1,35 @@ +import aio_pika +from aio_pika.abc import AbstractChannel, AbstractRobustConnection + +from src.models.configs_read.env import env_settings + + +class RabbitMQClient: + def __init__(self) -> None: + self.connection: AbstractRobustConnection | None = None + self.channel: AbstractChannel | None = None + + async def connect(self) -> None: + if self.connection is None or self.connection.is_closed: + self.connection = await aio_pika.connect_robust( + host=env_settings.RABBITMQ_HOST, + port=env_settings.RABBITMQ_PORT, + login=env_settings.RABBITMQ_LOGIN, + password=env_settings.RABBITMQ_PASSWORD, + ) + if self.channel is None or self.channel.is_closed: + self.channel = await self.connection.channel() + + async def get_channel(self) -> AbstractChannel: + await self.connect() + if self.channel is None: + raise RuntimeError("Failed to establish RabbitMQ channel") + return self.channel + + async def close(self) -> None: + if self.channel and not self.channel.is_closed: + await self.channel.close() + if self.connection and not self.connection.is_closed: + await self.connection.close() + +rabbitmq_client = RabbitMQClient() \ No newline at end of file diff --git a/src/models/configs_read/env.py b/src/models/configs_read/env.py index 332dabb..29002de 100644 --- a/src/models/configs_read/env.py +++ b/src/models/configs_read/env.py @@ -20,6 +20,12 @@ class Env(Base): REDIS_PORT:int REDIS_HOST:str + RABBITMQ_PASSWORD:str + RABBITMQ_LOGIN:str + RABBITMQ_HOST:str + RABBITMQ_PORT:int + RABBITMQ_PORT_UI:int + PROD_MODE:bool model_config=SettingsConfigDict(env_file="configs/.env", extra=None) diff --git a/src/web/protected_routes/auth_routes.py b/src/web/protected_routes/auth_routes.py index 4f95c5c..6d33990 100644 --- a/src/web/protected_routes/auth_routes.py +++ b/src/web/protected_routes/auth_routes.py @@ -74,6 +74,10 @@ async def logout(response:Response, response.delete_cookie("refresh_token") return await auth.logout(refresh_token, access_token) +from src.messaging.producers.producers import email_producer + + @router.get("") async def protected(current_user:UserOut=Depends(require_permissions()))->dict: + await email_producer.send_welcome_email("test@test.com") return {"protected router": "Hello, this is a protected router"} From 1aa308546842eacd64a22b918c45139fb8143bca Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Wed, 9 Sep 2026 14:07:08 +0300 Subject: [PATCH 3/8] topic exchange rabbitmq --- configs/daemons.json | 7 +++ daemon_run.py | 51 ++++++++++++++++++-- src/daemons/base.py | 9 ++++ src/daemons/email_consumer.py | 0 src/daemons/email_daemons.py | 19 ++++++++ src/daemons/registry.py | 6 +++ src/messaging/consumers/consumers.py | 62 +++++++++++++++++++++---- src/messaging/producers/producers.py | 24 ++++++---- src/models/configs_read/daemons_json.py | 12 +++++ 9 files changed, 170 insertions(+), 20 deletions(-) create mode 100644 configs/daemons.json delete mode 100644 src/daemons/email_consumer.py create mode 100644 src/daemons/email_daemons.py create mode 100644 src/models/configs_read/daemons_json.py diff --git a/configs/daemons.json b/configs/daemons.json new file mode 100644 index 0000000..f438df0 --- /dev/null +++ b/configs/daemons.json @@ -0,0 +1,7 @@ +{ + "daemons": + [ + "welcome_email", + "reset_email" + ] +} \ No newline at end of file diff --git a/daemon_run.py b/daemon_run.py index 354f527..4d46501 100644 --- a/daemon_run.py +++ b/daemon_run.py @@ -1,11 +1,56 @@ import asyncio +import os +import sys -from src.messaging.consumers.consumers import email_consumer +from src.daemons.registry import DAEMONS +from src.models.configs_read.daemons_json import daemons_config + + +async def run_one(daemon_name: str) -> None: + daemon_cls = DAEMONS.get(daemon_name) + if daemon_cls is None: + print(f"Unknown daemon: {daemon_name}. Available: {list(DAEMONS.keys())}") + sys.exit(1) + daemon = daemon_cls() + await daemon.run() + + +async def run_enabled_from_config() -> None: + + daemons = [] + + for name in daemons_config.daemons: + name = name.strip() + if name not in DAEMONS: + print(f"Warning: unknown daemon '{name}' in config, skipping") + continue + daemons.append(DAEMONS[name]()) + + if not daemons: + print("No enabled daemons found in config") + sys.exit(1) + + await asyncio.gather(*(d.run() for d in daemons)) async def main() -> None: - await email_consumer.start_consuming() + if len(sys.argv) < 2: + print("Usage: python run_daemon.py | --all") + sys.exit(1) + + arg = sys.argv[1] + if arg == "--all": + await run_enabled_from_config() + else: + await run_one(arg) if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('Interrupted') + try: + sys.exit(0) + except SystemExit: + os._exit(0) \ No newline at end of file diff --git a/src/daemons/base.py b/src/daemons/base.py index e69de29..249ed79 100644 --- a/src/daemons/base.py +++ b/src/daemons/base.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + + +class BaseDaemon(ABC): + name: str + + @abstractmethod + async def run(self) -> None: + ... \ No newline at end of file diff --git a/src/daemons/email_consumer.py b/src/daemons/email_consumer.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/daemons/email_daemons.py b/src/daemons/email_daemons.py new file mode 100644 index 0000000..2dbeda5 --- /dev/null +++ b/src/daemons/email_daemons.py @@ -0,0 +1,19 @@ +from src.daemons.base import BaseDaemon +from src.messaging.consumers.consumers import ( + reset_email_consumer, + welcome_email_consumer, +) + + +class WelcomeEmailDaemon(BaseDaemon): + name = "welcome_email" + + async def run(self) -> None: + await welcome_email_consumer.start_consuming() + + +class ResetEmailDaemon(BaseDaemon): + name = "reset_email" + + async def run(self) -> None: + await reset_email_consumer.start_consuming() \ No newline at end of file diff --git a/src/daemons/registry.py b/src/daemons/registry.py index e69de29..580c33c 100644 --- a/src/daemons/registry.py +++ b/src/daemons/registry.py @@ -0,0 +1,6 @@ +from src.daemons.email_daemons import ResetEmailDaemon, WelcomeEmailDaemon + +DAEMONS = { + "welcome_email": WelcomeEmailDaemon, + "reset_email": ResetEmailDaemon, +} \ No newline at end of file diff --git a/src/messaging/consumers/consumers.py b/src/messaging/consumers/consumers.py index 6ac0451..068502d 100644 --- a/src/messaging/consumers/consumers.py +++ b/src/messaging/consumers/consumers.py @@ -1,9 +1,11 @@ import json +import aio_pika + from src.messaging.rabbitmq_client import rabbitmq_client -class EmailConsumer: +class WelcomeEmailConsumer: def __init__(self) -> None: self.channel = None @@ -13,23 +15,65 @@ class EmailConsumer: self.channel = await rabbitmq_client.get_channel() await self.channel.set_qos(prefetch_count=10) - self.queue = await self.channel.declare_queue("email_queue", durable=True) + + exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC) + self.queue = await self.channel.declare_queue("queue_welcome_email", durable=True, arguments={"x-queue-type": "quorum"}) + + await self.queue.bind(exchange, routing_key="email.welcome") async def process_message(self, message) -> None: + async with message.process(): data = json.loads(message.body) - print(f"Отправляю email на {data['email']}") + print(f"Обрабатываю: {data}, метка: {message.routing_key}") async def start_consuming(self)->None: if self.queue is None: await self.setup() - - if self.channel is None or self.queue is None: - raise RuntimeError("Failed to set up RabbitMQ channel/queue") - - async with self.queue.iterator() as queue_iter: + + queue = self.queue + if queue is None: + raise RuntimeError("Failed to set up RabbitMQ queue") + + async with queue.iterator() as queue_iter: async for message in queue_iter: await self.process_message(message) -email_consumer=EmailConsumer() \ No newline at end of file +class ResetEmailConsumer: + + def __init__(self) -> None: + self.channel = None + self.queue = None + + async def setup(self)->None: + + self.channel = await rabbitmq_client.get_channel() + await self.channel.set_qos(prefetch_count=10) + + exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC) + self.queue = await self.channel.declare_queue("queue_reset_email", durable=True, arguments={"x-queue-type": "quorum"}) + + await self.queue.bind(exchange, routing_key="email.reset") + + async def process_message(self, message) -> None: + + async with message.process(): + data = json.loads(message.body) + print(f"Обрабатываю: {data}, метка: {message.routing_key}") + + async def start_consuming(self)->None: + + if self.queue is None: + await self.setup() + + queue = self.queue + if queue is None: + raise RuntimeError("Failed to set up RabbitMQ queue") + + async with queue.iterator() as queue_iter: + async for message in queue_iter: + await self.process_message(message) + +welcome_email_consumer=WelcomeEmailConsumer() +reset_email_consumer=ResetEmailConsumer() \ No newline at end of file diff --git a/src/messaging/producers/producers.py b/src/messaging/producers/producers.py index 6ed6e1d..1c49fc0 100644 --- a/src/messaging/producers/producers.py +++ b/src/messaging/producers/producers.py @@ -14,20 +14,28 @@ class EmailProducer: async def setup(self)->None: self.channel = await rabbitmq_client.get_channel() - self.queue = await self.channel.declare_queue("email_queue", durable=True) + self.exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC) async def send_welcome_email(self, email:str)->None: - if self.queue is None: - await self.setup() + await self._publish({"email":email}, routing_key="email.welcome") - if self.channel is None or self.queue is None: - raise RuntimeError("Failed to set up RabbitMQ channel/queue") + async def send_reset_email(self, email:str)->None: + + await self._publish({"email":email}, routing_key="email.reset") + + async def _publish(self,data:dict, routing_key:str)->None: + + if self.exchange is None: + await self.setup() - message=aio_pika.Message( - body=json.dumps({"email": email}).encode(), + if self.exchange is None: + raise RuntimeError("Failed to set up RabbitMQ exchange") + + message = aio_pika.Message( + body=json.dumps(data).encode(), delivery_mode=aio_pika.DeliveryMode.PERSISTENT, ) - await self.channel.default_exchange.publish(message=message, routing_key=self.queue.name) + await self.exchange.publish(message=message, routing_key=routing_key) email_producer=EmailProducer() \ No newline at end of file diff --git a/src/models/configs_read/daemons_json.py b/src/models/configs_read/daemons_json.py new file mode 100644 index 0000000..684c087 --- /dev/null +++ b/src/models/configs_read/daemons_json.py @@ -0,0 +1,12 @@ +from pydantic_settings import SettingsConfigDict + +from src.models.configs_read.env import Base + + +class DaemonsConfig(Base): + + daemons: list[str] + + model_config = SettingsConfigDict(json_file="configs/daemons.json") + +daemons_config = DaemonsConfig() # type: ignore[call-arg] \ No newline at end of file From b4e88a6ff481dfd89bac43a4772936249a6085cf Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Wed, 9 Sep 2026 18:01:48 +0300 Subject: [PATCH 4/8] docker container for daemons --- daemon_run.py | 2 +- docker/compose-dev.yaml | 34 ++++ docker/dockerfile | 50 +++++- poetry.lock | 212 +++--------------------- pyproject.toml | 13 +- src/messaging/producers/producers.py | 8 +- src/models/configs_read/daemons_json.py | 18 +- 7 files changed, 134 insertions(+), 203 deletions(-) diff --git a/daemon_run.py b/daemon_run.py index 4d46501..0fcf912 100644 --- a/daemon_run.py +++ b/daemon_run.py @@ -29,7 +29,7 @@ async def run_enabled_from_config() -> None: if not daemons: print("No enabled daemons found in config") sys.exit(1) - + await asyncio.gather(*(d.run() for d in daemons)) diff --git a/docker/compose-dev.yaml b/docker/compose-dev.yaml index 67bdc00..53175c7 100644 --- a/docker/compose-dev.yaml +++ b/docker/compose-dev.yaml @@ -1,6 +1,7 @@ name: disexcel services: + backend-dev: profiles: ["dev"] image: "${DOCKER_REGISTRY:-local}/excel-dev:${IMAGE_TAG:-local}" @@ -13,6 +14,8 @@ services: ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications environment: - DB_HOST=psql #rewrite DB_HOST var to communicate inside the docker network + - RABBITMQ_HOST=rabbitmq + - REDIS_HOST=redis volumes: - type: bind source: ../src @@ -32,6 +35,9 @@ services: - type: bind source: ../logs target: /home/excel-project/logs + - type: bind + source: ../daemon_run.py + target: /home/excel-project/daemon_run.py networks: - backend depends_on: @@ -57,6 +63,8 @@ services: ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications environment: - DB_HOST=psql + - RABBITMQ_HOST=rabbitmq + - REDIS_HOST=redis volumes: - type: bind source: ../configs @@ -83,6 +91,32 @@ services: ports: - "80:8000" + daemons: + profiles: ["prod", "dev"] + image: "${DOCKER_REGISTRY:-local}/excel-daemons:${IMAGE_TAG:-local}" + container_name: daemons + environment: + - RABBITMQ_HOST=rabbitmq + build: + dockerfile: ./docker/dockerfile + context: ../ + target: daemon + init: true + ipc: private + volumes: + - type: bind + source: ../configs + target: /home/excel-project/configs + - type: bind + source: ../logs + target: /home/excel-project/logs + depends_on: + rabbitmq: + condition: service_healthy + networks: + - backend + restart: unless-stopped + psql: profiles: ["prod", "dev", "local"] image: postgres:16-alpine diff --git a/docker/dockerfile b/docker/dockerfile index 9c82060..d8ec64c 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -34,7 +34,7 @@ COPY pyproject.toml poetry.lock ./ RUN pip install --no-cache-dir poetry \ && poetry config virtualenvs.create false \ - && poetry install --no-root --no-interaction --only main \ + && poetry install --no-root --no-interaction --only main,web \ && pip uninstall -y poetry poetry-core poetry-plugin-export # --- Stage 2: Python Backend prod --- @@ -62,4 +62,50 @@ RUN groupadd --gid 1000 appuser \ USER appuser -ENTRYPOINT ["./entrypoint.sh", "--prod"] \ No newline at end of file +ENTRYPOINT ["./entrypoint.sh", "--prod"] + +# --- Stage 1: Python daemon builder --- + +FROM python:3.14-slim AS worker-builder + +LABEL org.opencontainers.image.title="The-DisExcel-project-dev" +LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project" + +WORKDIR /home/excel-project + +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +RUN python -m venv "$VIRTUAL_ENV" + +COPY pyproject.toml poetry.lock ./ + +RUN pip install --no-cache-dir poetry \ + && poetry config virtualenvs.create false \ + && poetry install --no-root --no-interaction --only main,daemon \ + && pip uninstall -y poetry poetry-core poetry-plugin-export + +# --- Stage 2: Python daemons --- + +FROM python:3.14-slim AS daemon + +LABEL org.opencontainers.image.title="The-DisExcel-project-prod" +LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project" + +WORKDIR /home/excel-project + +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +COPY --from=worker-builder /opt/venv /opt/venv + +COPY pyproject.toml poetry.lock alembic.ini daemon_run.py ./ +COPY src/ ./src/ + +RUN groupadd --gid 1000 appuser \ + && useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser \ + && chown -R appuser:appuser /home/excel-project + +USER appuser + +ENTRYPOINT ["python","-u", "daemon_run.py", "--all"] diff --git a/poetry.lock b/poetry.lock index b487e66..2ef8ecb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -102,7 +102,7 @@ version = "0.0.4" description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["web"] files = [ {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, @@ -114,7 +114,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "web"] 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"}, @@ -126,7 +126,7 @@ version = "4.14.2" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["dev", "web"] files = [ {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, @@ -434,7 +434,7 @@ version = "8.4.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["web"] files = [ {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, @@ -449,12 +449,12 @@ 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"] +groups = ["dev", "web"] 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\"", dev = "sys_platform == \"win32\""} +markers = {dev = "sys_platform == \"win32\"", web = "platform_system == \"Windows\""} [[package]] name = "coverage" @@ -661,7 +661,7 @@ version = "0.139.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["web"] files = [ {file = "fastapi-0.139.1-py3-none-any.whl", hash = "sha256:17faa81907751a8a85cd44c46f37fb576bde0078cb37de40bf1cd55de7104d87"}, {file = "fastapi-0.139.1.tar.gz", hash = "sha256:99461bde7ac3fc34c78443da1f4dad3ca8f3182580029a2827692db216a8d7ae"}, @@ -778,7 +778,7 @@ version = "26.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["web"] files = [ {file = "gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc"}, {file = "gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf"}, @@ -801,7 +801,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev", "web"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -889,7 +889,7 @@ version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main", "dev", "web"] files = [ {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, @@ -1306,67 +1306,13 @@ files = [ {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, ] -[[package]] -name = "numpy" -version = "2.5.1" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.12" -groups = ["main"] -files = [ - {file = "numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277"}, - {file = "numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1"}, - {file = "numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0"}, - {file = "numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e"}, - {file = "numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75"}, - {file = "numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca"}, - {file = "numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3"}, - {file = "numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9"}, - {file = "numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2"}, - {file = "numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2"}, - {file = "numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b"}, - {file = "numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1"}, - {file = "numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6"}, - {file = "numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d"}, - {file = "numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1"}, - {file = "numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd"}, - {file = "numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a"}, - {file = "numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7"}, - {file = "numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6"}, - {file = "numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9"}, - {file = "numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74"}, - {file = "numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107"}, - {file = "numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8"}, - {file = "numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75"}, - {file = "numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2"}, - {file = "numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b"}, - {file = "numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95"}, - {file = "numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21"}, - {file = "numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373"}, - {file = "numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438"}, - {file = "numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace"}, - {file = "numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a"}, - {file = "numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0"}, - {file = "numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22"}, - {file = "numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7"}, - {file = "numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d"}, - {file = "numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09"}, - {file = "numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4"}, - {file = "numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1"}, - {file = "numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077"}, - {file = "numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf"}, - {file = "numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af"}, - {file = "numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb"}, - {file = "numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3"}, -] - [[package]] name = "packaging" version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev", "web"] files = [ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, @@ -1387,98 +1333,6 @@ files = [ [package.extras] codegen = ["lxml", "requests", "yapf"] -[[package]] -name = "pandas" -version = "3.0.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98"}, - {file = "pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639"}, - {file = "pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2"}, - {file = "pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27"}, - {file = "pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824"}, - {file = "pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938"}, - {file = "pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea"}, - {file = "pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a"}, - {file = "pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09"}, - {file = "pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4"}, - {file = "pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c"}, - {file = "pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9"}, - {file = "pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf"}, - {file = "pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c"}, - {file = "pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc"}, - {file = "pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49"}, - {file = "pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa"}, - {file = "pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7"}, - {file = "pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8"}, - {file = "pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a"}, - {file = "pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb"}, - {file = "pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2"}, - {file = "pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44"}, - {file = "pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e"}, - {file = "pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d"}, - {file = "pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066"}, - {file = "pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd"}, - {file = "pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085"}, - {file = "pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870"}, - {file = "pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f"}, - {file = "pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13"}, - {file = "pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac"}, - {file = "pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f"}, - {file = "pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb"}, - {file = "pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a"}, - {file = "pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360"}, - {file = "pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76"}, - {file = "pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5"}, - {file = "pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977"}, - {file = "pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04"}, - {file = "pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6"}, - {file = "pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c"}, - {file = "pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028"}, - {file = "pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d"}, - {file = "pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a"}, - {file = "pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1"}, - {file = "pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1"}, - {file = "pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.26.0", markers = "python_version < \"3.14\""}, - {version = ">=2.3.3", markers = "python_version >= \"3.14\""}, -] -python-dateutil = ">=2.8.2" -tzdata = {version = "*", markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\""} - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)", "beautifulsoup4 (>=4.12.3)", "bottleneck (>=1.4.2)", "fastparquet (>=2024.11.0)", "fsspec (>=2024.10.0)", "gcsfs (>=2024.10.0)", "html5lib (>=1.1)", "hypothesis (>=6.116.0)", "jinja2 (>=3.1.5)", "lxml (>=5.3.0)", "matplotlib (>=3.9.3)", "numba (>=0.60.0)", "numexpr (>=2.10.2)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "psycopg2 (>=2.9.10)", "pyarrow (>=13.0.0)", "pyiceberg (>=0.8.1)", "pymysql (>=1.1.1)", "pyreadstat (>=1.2.8)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)", "python-calamine (>=0.3.0)", "pytz (>=2020.1)", "pyxlsb (>=1.0.10)", "qtpy (>=2.4.2)", "s3fs (>=2024.10.0)", "scipy (>=1.14.1)", "tables (>=3.10.1)", "tabulate (>=0.9.0)", "xarray (>=2024.10.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)", "zstandard (>=0.23.0)"] -aws = ["s3fs (>=2024.10.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.4.2)"] -compression = ["zstandard (>=0.23.0)"] -computation = ["scipy (>=1.14.1)", "xarray (>=2024.10.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "python-calamine (>=0.3.0)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)"] -feather = ["pyarrow (>=13.0.0)"] -fss = ["fsspec (>=2024.10.0)"] -gcp = ["gcsfs (>=2024.10.0)"] -hdf5 = ["tables (>=3.10.1)"] -html = ["beautifulsoup4 (>=4.12.3)", "html5lib (>=1.1)", "lxml (>=5.3.0)"] -iceberg = ["pyiceberg (>=0.8.1)"] -mysql = ["SQLAlchemy (>=2.0.36)", "pymysql (>=1.1.1)"] -output-formatting = ["jinja2 (>=3.1.5)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=13.0.0)"] -performance = ["bottleneck (>=1.4.2)", "numba (>=0.60.0)", "numexpr (>=2.10.2)"] -plot = ["matplotlib (>=3.9.3)"] -postgresql = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "psycopg2 (>=2.9.10)"] -pyarrow = ["pyarrow (>=13.0.0)"] -spss = ["pyreadstat (>=1.2.8)"] -sql-other = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)"] -test = ["hypothesis (>=6.116.0)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)"] -timezone = ["pytz (>=2020.1)"] -xml = ["lxml (>=5.3.0)"] - [[package]] name = "parso" version = "0.8.7" @@ -1844,7 +1698,7 @@ version = "2.13.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "web"] files = [ {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, @@ -1867,7 +1721,7 @@ version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "web"] files = [ {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, @@ -2125,21 +1979,6 @@ pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - [[package]] name = "python-dotenv" version = "1.2.2" @@ -2184,7 +2023,7 @@ version = "0.0.32" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["web"] files = [ {file = "python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23"}, {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, @@ -2462,7 +2301,7 @@ version = "1.3.1" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["web"] files = [ {file = "starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6"}, {file = "starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0"}, @@ -2496,7 +2335,7 @@ version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "web"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, @@ -2508,7 +2347,7 @@ version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "web"] files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, @@ -2517,19 +2356,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" -[[package]] -name = "tzdata" -version = "2026.3" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\"" -files = [ - {file = "tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931"}, - {file = "tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415"}, -] - [[package]] name = "urllib3" version = "2.7.0" @@ -2554,7 +2380,7 @@ version = "0.51.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["web"] files = [ {file = "uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b"}, {file = "uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0"}, @@ -2701,4 +2527,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "afc1aa09280c992138f9bc4c3edb89aa146e04bbf5c045f18d5c886a5b735cc3" +content-hash = "025191389a53d34b4a8430d8ab7afde489ccd7692f5e0e8e2d29b8c26aa94364" diff --git a/pyproject.toml b/pyproject.toml index c4b22ba..481f030 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,16 +11,11 @@ requires-python = ">=3.13,<4.0" dependencies = [ "alembic (>=1.18.5,<2.0.0)", - "uvicorn (>=0.51.0,<0.52.0)", - "gunicorn (>=26.0.0,<27.0.0)", - "fastapi (>=0.139.1,<0.140.0)", "pydantic[email] (>=2.13.4,<3.0.0)", "pydantic-settings (>=2.14.2,<3.0.0)", "sqlalchemy[asyncio] (>=2.0.52,<3.0.0)", - "pandas (>=3.0.3,<4.0.0)", "bcrypt (>=5.0.0,<6.0.0)", "python-jose (>=3.5.0,<4.0.0)", - "python-multipart (>=0.0.32,<0.0.33)", "greenlet (>=3.5.4,<4.0.0)", "aiofiles (>=25.1.0,<26.0.0)", "asyncpg (>=0.31.0,<0.32.0)", @@ -29,6 +24,12 @@ dependencies = [ "aio-pika (>=10.0.1,<11.0.0)", ] +[tool.poetry.group.web.dependencies] +uvicorn = ">=0.51.0,<0.52.0" +gunicorn = ">=26.0.0,<27.0.0" +fastapi = ">=0.139.1,<0.140.0" +python-multipart = ">=0.0.32,<0.0.33" + [tool.poetry.group.dev.dependencies] pytest = ">=9.1.1,<10.0.0" pytest-cov = ">=7.1.0,<8.0.0" @@ -39,6 +40,8 @@ httpie = ">=3.2.4,<4.0.0" pytest-asyncio = ">=1.4.0,<2.0.0" requests-async = ">=0.2.4,<0.3.0" +[tool.poetry.group.daemon.dependencies] + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/src/messaging/producers/producers.py b/src/messaging/producers/producers.py index 1c49fc0..8179f4c 100644 --- a/src/messaging/producers/producers.py +++ b/src/messaging/producers/producers.py @@ -9,13 +9,19 @@ class EmailProducer: def __init__(self) -> None: self.channel = None - self.queue = None + self.exchange = None async def setup(self)->None: self.channel = await rabbitmq_client.get_channel() self.exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC) + welcome_queue = await self.channel.declare_queue("queue_welcome_email", durable=True, arguments={"x-queue-type": "quorum"}) + await welcome_queue.bind(self.exchange, routing_key="email.welcome") + + reset_queue = await self.channel.declare_queue("queue_reset_email", durable=True, arguments={"x-queue-type": "quorum"}) + await reset_queue.bind(self.exchange, routing_key="email.reset") + async def send_welcome_email(self, email:str)->None: await self._publish({"email":email}, routing_key="email.welcome") diff --git a/src/models/configs_read/daemons_json.py b/src/models/configs_read/daemons_json.py index 684c087..a26fb0b 100644 --- a/src/models/configs_read/daemons_json.py +++ b/src/models/configs_read/daemons_json.py @@ -1,4 +1,9 @@ -from pydantic_settings import SettingsConfigDict +from pydantic_settings import ( + BaseSettings, + JsonConfigSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, +) from src.models.configs_read.env import Base @@ -9,4 +14,15 @@ class DaemonsConfig(Base): model_config = SettingsConfigDict(json_file="configs/daemons.json") + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + return (JsonConfigSettingsSource(settings_cls),) + daemons_config = DaemonsConfig() # type: ignore[call-arg] \ No newline at end of file From a93c6d5fca134e492518196a0974e1ab8be3ec54 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Sun, 13 Sep 2026 18:40:14 +0300 Subject: [PATCH 5/8] refactor logging module, add welcome and reset daemons, changed structure of the rabbitmq queues --- .claude/agents/code-reviewer.md | 7 +++ .claude/settings.local.json | 9 +++ .gitignore | 11 ++-- configs/.env.example | 7 +++ daemon_run.py | 5 ++ main.py | 10 +++- poetry.lock | 52 +++++++++++++++++- pyproject.toml | 3 + src/logging/logger.py | 70 ++++++++++++++++-------- src/messaging/consumers/consumers.py | 42 ++++++++------ src/messaging/producers/producers.py | 10 +--- src/messaging/topology_setup.py | 22 ++++++++ src/models/configs_read/env.py | 5 ++ src/models/rabbitmq_models/email.py | 24 ++++++++ src/service/email/email_reset.py | 0 src/service/email/email_welcome.py | 40 ++++++++++++++ src/service/email/jinja_env.py | 6 ++ src/service/email/templates/reset.html | 55 +++++++++++++++++++ src/service/email/templates/welcome.html | 52 ++++++++++++++++++ src/web/protected_routes/auth_routes.py | 2 +- 20 files changed, 374 insertions(+), 58 deletions(-) create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/settings.local.json create mode 100644 src/messaging/topology_setup.py create mode 100644 src/models/rabbitmq_models/email.py create mode 100644 src/service/email/email_reset.py create mode 100644 src/service/email/email_welcome.py create mode 100644 src/service/email/jinja_env.py create mode 100644 src/service/email/templates/reset.html create mode 100644 src/service/email/templates/welcome.html diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000..9c5308b --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,7 @@ +--- +name: code-reviewer +description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code. +tools: Read, Grep, Glob, Bash +model: sonnet +--- +You are a senior code reviewer. Read the recent git diff or modified files, then report only what matters: correctness bugs, security vulnerabilities, and maintainability traps. Lead with the highest-severity finding. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..16d24f5 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "sandbox": { + "filesystem": { + "allowRead": ["."], + "denyRead": ["**/.env", "./DB", "./RTMQ", "./.vscode", "./.pytest_cache", "**/__pycache__"] + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 41df06c..4f00b33 100644 --- a/.gitignore +++ b/.gitignore @@ -17,14 +17,17 @@ __pycache__/ .DS_Store Thumbs.db -#env +# env files *.env -#db + +# DB *.db DB/ -#rabbitmq + +# rabbitmq RTMQ/ -#logs + +# logs logs/ #Примеры документов diff --git a/configs/.env.example b/configs/.env.example index 27b7172..b4875b3 100644 --- a/configs/.env.example +++ b/configs/.env.example @@ -16,7 +16,14 @@ REDIS_HOST="change_me" RABBITMQ_PASSWORD="change_me" RABBITMQ_LOGIN="change_me" +RABBITMQ_HOST="change_me" RABBITMQ_PORT=change_me #int RABBITMQ_PORT_UI=chane_me #int +EMAIL_PORT=change_me #int +SMTP_SERVER="change_me" +EMAIL_LOGIN="change_me" +EMAIL_PASSWORD="change_me" + + PROD_MODE=bool \ No newline at end of file diff --git a/daemon_run.py b/daemon_run.py index 0fcf912..e04b2c2 100644 --- a/daemon_run.py +++ b/daemon_run.py @@ -3,7 +3,10 @@ import os import sys from src.daemons.registry import DAEMONS +from src.messaging.rabbitmq_client import rabbitmq_client +from src.messaging.topology_setup import apply_topology from src.models.configs_read.daemons_json import daemons_config +from src.models.rabbitmq_models.email import email_topology async def run_one(daemon_name: str) -> None: @@ -34,6 +37,8 @@ async def run_enabled_from_config() -> None: async def main() -> None: + channel = await rabbitmq_client.get_channel() + await apply_topology(channel, email_topology) if len(sys.argv) < 2: print("Usage: python run_daemon.py | --all") sys.exit(1) diff --git a/main.py b/main.py index 1bdc874..c3ffcac 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +import asyncio from contextlib import asynccontextmanager from pathlib import Path @@ -5,19 +6,26 @@ from fastapi import FastAPI from src.cache.redis_client import redis_client from src.database.users.crud import Seed -from src.logging.logger import LoggingMiddleware, ProcessingTimeMiddleware +from src.logging.logger import LoggingMiddleware, LogWriter, ProcessingTimeMiddleware from src.messaging.rabbitmq_client import rabbitmq_client +from src.messaging.topology_setup import apply_topology +from src.models.rabbitmq_models.email import email_topology from src.web.protected_routes.auth_routes import router as protected_router from src.web.protected_routes.protected_user_action_routes import ( router as protected_user_action_routes, ) +writer=LogWriter() @asynccontextmanager async def lifespan(app: FastAPI): create_dirs() + channel = await rabbitmq_client.get_channel() + await apply_topology(channel, email_topology) await create_first_user() + writer_task=asyncio.create_task(writer.log_writer()) yield + writer_task.cancel() await redis_client.close() await rabbitmq_client.close() diff --git a/poetry.lock b/poetry.lock index 2ef8ecb..c469b1d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,6 +44,22 @@ files = [ pamqp = ">=4,<5" yarl = "*" +[[package]] +name = "aiosmtpd" +version = "1.4.6" +description = "aiosmtpd - asyncio based SMTP server" +optional = false +python-versions = ">=3.8" +groups = ["daemon"] +files = [ + {file = "aiosmtpd-1.4.6-py3-none-any.whl", hash = "sha256:72c99179ba5aa9ae0abbda6994668239b64a5ce054471955fe75f581d2592475"}, + {file = "aiosmtpd-1.4.6.tar.gz", hash = "sha256:5a811826e1a5a06c25ebc3e6c4a704613eb9a1bcf6b78428fbe865f4f6c9a4b8"}, +] + +[package.dependencies] +atpublic = "*" +attrs = "*" + [[package]] name = "alembic" version = "1.18.5" @@ -224,13 +240,25 @@ files = [ [package.extras] gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] +[[package]] +name = "atpublic" +version = "7.0.0" +description = "Keep all y'all's __all__'s in sync" +optional = false +python-versions = ">=3.10" +groups = ["daemon"] +files = [ + {file = "atpublic-7.0.0-py3-none-any.whl", hash = "sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b"}, + {file = "atpublic-7.0.0.tar.gz", hash = "sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e"}, +] + [[package]] name = "attrs" version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["daemon", "dev"] files = [ {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, @@ -977,6 +1005,24 @@ parso = ">=0.8.6,<0.9.0" dev = ["Django", "attrs", "colorama", "docopt", "flake8 (==7.1.2)", "pytest (<9.0.0)", "types-setuptools (==80.9.0.20250529)", "typing-extensions", "zuban (==0.7.0)"] docs = ["Jinja2 (==3.1.6)", "MarkupSafe (==3.0.3)", "Pygments (==2.20.0)", "Sphinx (==9.1.0)", "alabaster (==1.0.0)", "babel (==2.18.0)", "certifi (==2026.4.22)", "charset-normalizer (==3.4.7)", "docutils (==0.22.4)", "idna (==3.13)", "imagesize (==2.0.0)", "iniconfig (==2.3.0)", "packaging (==26.2)", "pluggy (==1.6.0)", "pytest (==9.0.3)", "requests (==2.33.1)", "roman-numerals (==4.1.0)", "snowballstemmer (==3.0.1)", "sphinx-rtd-theme (==3.1.0)", "sphinxcontrib-applehelp (==2.0.0)", "sphinxcontrib-devhelp (==2.0.0)", "sphinxcontrib-htmlhelp (==2.1.0)", "sphinxcontrib-jquery (==4.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==2.0.0)", "sphinxcontrib-serializinghtml (==2.0.0)", "urllib3 (==2.6.3)"] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["daemon"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "mako" version = "1.3.12" @@ -1027,7 +1073,7 @@ version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "daemon"] files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -2527,4 +2573,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "025191389a53d34b4a8430d8ab7afde489ccd7692f5e0e8e2d29b8c26aa94364" +content-hash = "2ac8d07b41e04495f16ac9da7093f96ec08d094402ebafe48a421a40a98c7d05" diff --git a/pyproject.toml b/pyproject.toml index 481f030..0e60d68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "psycopg2-binary (>=2.9.12,<3.0.0)", "redis (>=8.1.0,<9.0.0)", "aio-pika (>=10.0.1,<11.0.0)", + ] [tool.poetry.group.web.dependencies] @@ -41,6 +42,8 @@ pytest-asyncio = ">=1.4.0,<2.0.0" requests-async = ">=0.2.4,<0.3.0" [tool.poetry.group.daemon.dependencies] +aiosmtpd = ">=1.4.6,<2.0.0" +jinja2 = ">=3.1.6,<4.0.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] diff --git a/src/logging/logger.py b/src/logging/logger.py index c270942..7e0ffbd 100644 --- a/src/logging/logger.py +++ b/src/logging/logger.py @@ -1,8 +1,10 @@ import asyncio import json import logging +from contextvars import ContextVar from time import gmtime, perf_counter, strftime from typing import cast +from uuid import uuid4 import aiofiles from fastapi import Request @@ -11,8 +13,28 @@ from starlette.concurrency import iterate_in_threadpool from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import Response, StreamingResponse +request_id_ctx: ContextVar[str] = ContextVar("request_id", default="-") -class ProcessingTimeMiddleware(BaseHTTPMiddleware): +log_queue=asyncio.Queue() + +class LogWriter: + def __init__(self) -> None: + pass + + async def log_writer(self)->None: + while True: + log_type, msg=await log_queue.get() + await self._write(msg, log_type) + + async def _write(self, msg:str, type:str)->None: + file_time = strftime("%b_%Y", gmtime()) + current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + + async with aiofiles.open(f"./logs/{type}_log_{file_time}.txt", "a") as f: + await f.write(f"[{current_time}] {msg}\n") + + +class ProcessingTimeMiddleware(BaseHTTPMiddleware, ): async def dispatch(self, request: Request, call_next)->Response: start_time = perf_counter() response = await call_next(request) @@ -22,19 +44,26 @@ class ProcessingTimeMiddleware(BaseHTTPMiddleware): class LoggingMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request: Request, call_next) -> Response: - current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) - file_time = strftime("%b_%Y", gmtime()) - client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown') + + async def build_log_line(self, request_id, method, path, status_code, detail, client_ip) -> str: + return f"[{request_id}] [{method}] [{path}] [{status_code}] [{detail}] [{client_ip}]" + async def dispatch(self, request: Request, call_next) -> Response: + request_id=str(uuid4()) + request_id_ctx.set(request_id) + client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown') + method = request.method + path=request.url.path try: response = await call_next(request) except Exception as exc: # noqa: BLE001 - body = str(exc) - async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file: - await file.write(f"[{current_time}] [500] [{body}] [{client_ip}]\n") - return JSONResponse(status_code=500, content={"detail": "Internal Server Error"}) - + line=await self.build_log_line(request_id=request_id, method=method, path=path, status_code=500, detail=repr(exc), client_ip=client_ip) + log_queue.put_nowait(("endpoints", line)) + return JSONResponse( + status_code=500, + content={"detail": "Internal Server Error", "request_id": request_id} + ) + streaming_response = cast(StreamingResponse, response) chunks = [] @@ -49,23 +78,16 @@ class LoggingMiddleware(BaseHTTPMiddleware): body = parsed.get("detail", None) if not isinstance(parsed, bool) else None except (json.JSONDecodeError, TypeError): body = None - - async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file: - await file.write(f"[{current_time}] [{response.status_code}] [{body}] [{client_ip}]\n") - + + line=await self.build_log_line(request_id=request_id, method=method, path=path, status_code=response.status_code, detail=body, client_ip=client_ip) + log_queue.put_nowait(("endpoints", line)) + return response -class LoggerDB(logging.Handler): +class LoggerDB(logging.Handler, LogWriter): def emit(self, record: logging.LogRecord) -> None: msg = self.format(record) - asyncio.create_task(self._write(msg)) - - async def _write(self, msg: str) -> None: - - file_time = strftime("%b_%Y", gmtime()) - current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) - - async with aiofiles.open(f"./logs/sql_log_{file_time}.txt", "a") as f: - await f.write(f"[{current_time}] {msg}\n") + rid=request_id_ctx.get() + log_queue.put_nowait(("sql",f"[{rid}] {msg}")) \ No newline at end of file diff --git a/src/messaging/consumers/consumers.py b/src/messaging/consumers/consumers.py index 068502d..974f771 100644 --- a/src/messaging/consumers/consumers.py +++ b/src/messaging/consumers/consumers.py @@ -1,8 +1,8 @@ import json - -import aio_pika +import smtplib from src.messaging.rabbitmq_client import rabbitmq_client +from src.service.email.email_welcome import DaemonEmailSender class WelcomeEmailConsumer: @@ -10,22 +10,33 @@ class WelcomeEmailConsumer: def __init__(self) -> None: self.channel = None self.queue = None - - async def setup(self)->None: + self.daemon=DaemonEmailSender() + async def setup(self) -> None: self.channel = await rabbitmq_client.get_channel() await self.channel.set_qos(prefetch_count=10) - - exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC) - self.queue = await self.channel.declare_queue("queue_welcome_email", durable=True, arguments={"x-queue-type": "quorum"}) - - await self.queue.bind(exchange, routing_key="email.welcome") + self.queue = await self.channel.get_queue("queue_welcome_email") + async def process_message(self, message) -> None: - async with message.process(): + async with message.process(ignore_processed=True): data = json.loads(message.body) print(f"Обрабатываю: {data}, метка: {message.routing_key}") + try: + await self.daemon.send_email(data.get("email")) + except ( + smtplib.SMTPServerDisconnected, + smtplib.SMTPConnectError, + TimeoutError, + ConnectionRefusedError, + ) as exc: + print(f"transient error, retrying: {exc!r}") + await message.nack(requeue=True) + except Exception as exc: # noqa: BLE001 + print(f"permanent error, sending to DLQ: {exc!r}") + await message.nack(requeue=False) + async def start_consuming(self)->None: @@ -46,15 +57,12 @@ class ResetEmailConsumer: self.channel = None self.queue = None - async def setup(self)->None: - + + async def setup(self) -> None: self.channel = await rabbitmq_client.get_channel() await self.channel.set_qos(prefetch_count=10) - - exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC) - self.queue = await self.channel.declare_queue("queue_reset_email", durable=True, arguments={"x-queue-type": "quorum"}) - - await self.queue.bind(exchange, routing_key="email.reset") + self.queue = await self.channel.get_queue("queue_reset_email") + async def process_message(self, message) -> None: diff --git a/src/messaging/producers/producers.py b/src/messaging/producers/producers.py index 8179f4c..157c9e3 100644 --- a/src/messaging/producers/producers.py +++ b/src/messaging/producers/producers.py @@ -11,16 +11,10 @@ class EmailProducer: self.channel = None self.exchange = None - async def setup(self)->None: - + async def setup(self) -> None: self.channel = await rabbitmq_client.get_channel() - self.exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC) - - welcome_queue = await self.channel.declare_queue("queue_welcome_email", durable=True, arguments={"x-queue-type": "quorum"}) - await welcome_queue.bind(self.exchange, routing_key="email.welcome") + self.exchange = await self.channel.get_exchange("email") - reset_queue = await self.channel.declare_queue("queue_reset_email", durable=True, arguments={"x-queue-type": "quorum"}) - await reset_queue.bind(self.exchange, routing_key="email.reset") async def send_welcome_email(self, email:str)->None: diff --git a/src/messaging/topology_setup.py b/src/messaging/topology_setup.py new file mode 100644 index 0000000..e31bdb0 --- /dev/null +++ b/src/messaging/topology_setup.py @@ -0,0 +1,22 @@ +from src.models.rabbitmq_models.email import Topology + + +async def apply_topology(channel, topology: Topology) -> None: + dlx = await channel.declare_exchange("email.dlx", "direct") + + for exch_spec in topology.exchanges: + exchange = await channel.declare_exchange(exch_spec.name, exch_spec.type) + for q in exch_spec.queues: + queue = await channel.declare_queue( + q.name, + durable=True, + arguments={ + "x-queue-type": "quorum", + "x-dead-letter-exchange": "email.dlx", + }, + ) + await queue.bind(exchange, routing_key=q.routing_key) + + if q.dlx_queue: + dlq = await channel.declare_queue(q.dlx_queue, durable=True) + await dlq.bind(dlx, routing_key=q.routing_key) diff --git a/src/models/configs_read/env.py b/src/models/configs_read/env.py index 29002de..25ecfe3 100644 --- a/src/models/configs_read/env.py +++ b/src/models/configs_read/env.py @@ -26,6 +26,11 @@ class Env(Base): RABBITMQ_PORT:int RABBITMQ_PORT_UI:int + EMAIL_PORT:int + SMTP_SERVER:str + EMAIL_LOGIN:str + EMAIL_PASSWORD:str + PROD_MODE:bool model_config=SettingsConfigDict(env_file="configs/.env", extra=None) diff --git a/src/models/rabbitmq_models/email.py b/src/models/rabbitmq_models/email.py new file mode 100644 index 0000000..731b982 --- /dev/null +++ b/src/models/rabbitmq_models/email.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel + + +class QueueSpec(BaseModel): + name:str + routing_key:str + dlx_queue:str|None = None + +class ExchangeSpec(BaseModel): + name:str + type:str = "topic" + queues: list[QueueSpec] + +class Topology(BaseModel): + exchanges:list[ExchangeSpec] + +email_topology=Topology(exchanges=[ + ExchangeSpec(name="email", queues=[ + QueueSpec(name="queue_welcome_email", routing_key="email.welcome", dlx_queue="queue_welcome_email.dlq"), + QueueSpec(name="queue_reset_email", routing_key="email.reset", dlx_queue="queue_reset_email.dlq"), + ]), +]) + + \ No newline at end of file diff --git a/src/service/email/email_reset.py b/src/service/email/email_reset.py new file mode 100644 index 0000000..e69de29 diff --git a/src/service/email/email_welcome.py b/src/service/email/email_welcome.py new file mode 100644 index 0000000..6f076f5 --- /dev/null +++ b/src/service/email/email_welcome.py @@ -0,0 +1,40 @@ +import smtplib +import ssl +from email.message import EmailMessage + +from src.models.configs_read.env import env_settings +from src.service.email.jinja_env import jinja_env + + +class DaemonEmailSender: + def __init__(self) -> None: + pass + + async def send_email(self, target_email:str)->None: + context = ssl.create_default_context() + with smtplib.SMTP_SSL( + env_settings.SMTP_SERVER, env_settings.EMAIL_PORT, context=context + ) as server: + server.login(env_settings.EMAIL_LOGIN, env_settings.EMAIL_PASSWORD) + msg=await self.build_email_message(target_email) + server.send_message(msg) + + async def build_email_message(self, target_email: str) -> EmailMessage: + template = jinja_env.get_template("welcome.html") + html_body = template.render(name=target_email) + + text_body = ( + f"Добро пожаловать, {target_email}!\n\n" + "Спасибо за регистрацию на проекте «The DisExcel». " + "Мы рады, что вы с нами — учётная запись уже готова к работе.\n\n" + "Если вы не регистрировались на The DisExcel, просто проигнорируйте это письмо." + ) + + msg = EmailMessage() + msg["to"] = target_email + msg["from"] = env_settings.EMAIL_LOGIN + msg["subject"] = "Добро пожаловать" + msg.set_content(text_body) + msg.add_alternative(html_body, subtype="html") + return msg + diff --git a/src/service/email/jinja_env.py b/src/service/email/jinja_env.py new file mode 100644 index 0000000..b5a4d59 --- /dev/null +++ b/src/service/email/jinja_env.py @@ -0,0 +1,6 @@ +from jinja2 import Environment, FileSystemLoader + +jinja_env = Environment( + loader=FileSystemLoader("src/service/email/templates"), + autoescape=True, +) \ No newline at end of file diff --git a/src/service/email/templates/reset.html b/src/service/email/templates/reset.html new file mode 100644 index 0000000..55eb806 --- /dev/null +++ b/src/service/email/templates/reset.html @@ -0,0 +1,55 @@ + + + + + + Пароль сброшен + + + + + + +
+ + + + + + + + + + + + + + +
+

The DisExcel

+
+

Пароль был сброшен

+

+ Пароль от вашей учётной записи в «The DisExcel» был сброшен. Ниже — временный пароль для входа. Рекомендуем сменить его на свой сразу после входа в аккаунт. +

+ + + + + +
+ Новый пароль + Тест1234! +
+ +

+ Если вы не запрашивали сброс пароля, срочно свяжитесь с поддержкой — возможно, кто-то получил доступ к вашей учётной записи. +

+
+

+ Это автоматическое уведомление от The DisExcel. Не пересылайте это письмо никому. +

+
+
+ + \ No newline at end of file diff --git a/src/service/email/templates/welcome.html b/src/service/email/templates/welcome.html new file mode 100644 index 0000000..f66eff2 --- /dev/null +++ b/src/service/email/templates/welcome.html @@ -0,0 +1,52 @@ + + + + + + Добро пожаловать + + + + + + +
+ + + + + + + + + + + + + + +
+

The DisExcel

+
+

Добро пожаловать, {{ name }}

+

+ Спасибо за регистрацию на проекте «The DisExcel». Мы рады, что вы с нами — учётная запись уже готова к работе. +

+ + + + + +
+ + Перейти в аккаунт + +
+
+

+ Если вы не регистрировались на The DisExcel, просто проигнорируйте это письмо. +

+
+
+ + \ No newline at end of file diff --git a/src/web/protected_routes/auth_routes.py b/src/web/protected_routes/auth_routes.py index 6d33990..92b6419 100644 --- a/src/web/protected_routes/auth_routes.py +++ b/src/web/protected_routes/auth_routes.py @@ -79,5 +79,5 @@ from src.messaging.producers.producers import email_producer @router.get("") async def protected(current_user:UserOut=Depends(require_permissions()))->dict: - await email_producer.send_welcome_email("test@test.com") + await email_producer.send_welcome_email(current_user.email) return {"protected router": "Hello, this is a protected router"} From e124d897ebc03476db148339d33412dd2e4be5a0 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Mon, 14 Sep 2026 18:34:15 +0300 Subject: [PATCH 6/8] add logging and retry to connect to the rabbitmq server --- CLAUDE.md | 112 ++++++++++++++++++ daemon_run.py | 22 ++-- main.py | 3 +- src/logging/__init__.py | 10 +- src/logging/http_logger.py | 63 ++++++++++ src/logging/logger.py | 74 ++---------- src/messaging/consumers/consumers.py | 15 ++- src/messaging/rabbitmq_client.py | 25 ++-- src/web/protected_routes/auth_routes.py | 3 - .../protected_user_action_routes.py | 4 +- 10 files changed, 241 insertions(+), 90 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/logging/http_logger.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..15a2dbe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# The DisExcel Project — Context for Claude Code + +Backend: FastAPI + SQLAlchemy (async) + Pydantic v2 + PostgreSQL. JWT auth +(access + refresh tokens), RBAC permissions, Redis cache, RabbitMQ background +workers. Python >=3.13,<4.0, Poetry for dependency management. + +## Auth & Permissions + +- Users have `direct_permissions` (list of `Permissions`) and `group` + (list of `PermissionsGroups`, each with its own `permissions`) — + many-to-many both ways. +- Effective permissions = `direct_permissions ∪ (union of all groups' permissions)`. +- `require_permissions(*permissions)` in `src/web/protected_routes` is a + FastAPI dependency factory — wraps `CurrentUserService.get_current_user`. + Call with no args (`require_permissions()`) for "just authenticated, no + specific permission needed". +- Access tokens carry a `jti` claim. Logout writes `revoked_access_token:{jti}` + to Redis with TTL = remaining token lifetime — `get_current_user` checks + this key before anything else. +- `secure` flag on refresh_token cookie is driven by `env_settings.PROD_MODE` + (bool) — `False` locally/tests so cookies work over plain HTTP, `True` in + prod. + +## Redis (`src/cache/`) + +- `RedisClient(redis.Redis)` — module-level shared singleton, subclasses + `redis.Redis` directly (inherits all commands, no manual wrapping needed). +- Three uses: permissions is-cache was considered and rejected (no real DB + savings — `get_user_by_id` already eager-loads everything via `selectin` + in one call); rate limiting on login (`RateLimit.rate_limit(ip)` — + `INCR` + `EXPIRE` on first attempt, blocks >5/60s); access-token revoke + blacklist (see above). +- Rate limit is only triggered inside `except HTTPException` on `/protected/token` + — i.e. only on failed logins, not successful ones (otherwise legitimate + repeated logins would trip it). + +## RabbitMQ (`src/messaging/`) + +- `RabbitMQClient` — shared class, lazy `connect()` (can't be async `__init__`), + holds one `connection` + one `channel`, `get_channel()` ensures setup. +- Topic exchange named `"email"`. Each message type gets its own routing key + (`email.welcome`, `email.reset`) and its own durable queue + (`queue_welcome_email`, `queue_reset_email`), each queue explicitly bound + to the exchange with its own key. +- **Important**: both producer and consumer must declare/bind the queues — + if only the consumer does it and the consumer has never run, `publish` + on a not-yet-existing queue silently loses the message. `EmailProducer.setup()` + also declares+binds both queues defensively. +- `message.process()` async context manager = manual ack (auto-ack on + success, requeue/nack on exception) — this is the right choice for email, + not `no_ack=True`. +- Email templates: Jinja2, inline CSS (email clients don't support `