refactor logging module, add welcome and reset daemons, changed structure of the rabbitmq queues
This commit is contained in:
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||||
|
"sandbox": {
|
||||||
|
"filesystem": {
|
||||||
|
"allowRead": ["."],
|
||||||
|
"denyRead": ["**/.env", "./DB", "./RTMQ", "./.vscode", "./.pytest_cache", "**/__pycache__"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-4
@@ -17,14 +17,17 @@ __pycache__/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
#env
|
# env files
|
||||||
*.env
|
*.env
|
||||||
#db
|
|
||||||
|
# DB
|
||||||
*.db
|
*.db
|
||||||
DB/
|
DB/
|
||||||
#rabbitmq
|
|
||||||
|
# rabbitmq
|
||||||
RTMQ/
|
RTMQ/
|
||||||
#logs
|
|
||||||
|
# logs
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
#Примеры документов
|
#Примеры документов
|
||||||
|
|||||||
@@ -16,7 +16,14 @@ REDIS_HOST="change_me"
|
|||||||
|
|
||||||
RABBITMQ_PASSWORD="change_me"
|
RABBITMQ_PASSWORD="change_me"
|
||||||
RABBITMQ_LOGIN="change_me"
|
RABBITMQ_LOGIN="change_me"
|
||||||
|
RABBITMQ_HOST="change_me"
|
||||||
RABBITMQ_PORT=change_me #int
|
RABBITMQ_PORT=change_me #int
|
||||||
RABBITMQ_PORT_UI=chane_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
|
PROD_MODE=bool
|
||||||
@@ -3,7 +3,10 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from src.daemons.registry import DAEMONS
|
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.configs_read.daemons_json import daemons_config
|
||||||
|
from src.models.rabbitmq_models.email import email_topology
|
||||||
|
|
||||||
|
|
||||||
async def run_one(daemon_name: str) -> None:
|
async def run_one(daemon_name: str) -> None:
|
||||||
@@ -34,6 +37,8 @@ async def run_enabled_from_config() -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
|
channel = await rabbitmq_client.get_channel()
|
||||||
|
await apply_topology(channel, email_topology)
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: python run_daemon.py <daemon_name> | --all")
|
print("Usage: python run_daemon.py <daemon_name> | --all")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -5,19 +6,26 @@ from fastapi import FastAPI
|
|||||||
|
|
||||||
from src.cache.redis_client import redis_client
|
from src.cache.redis_client import redis_client
|
||||||
from src.database.users.crud import Seed
|
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.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.auth_routes import router as protected_router
|
||||||
from src.web.protected_routes.protected_user_action_routes import (
|
from src.web.protected_routes.protected_user_action_routes import (
|
||||||
router as protected_user_action_routes,
|
router as protected_user_action_routes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
writer=LogWriter()
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
create_dirs()
|
create_dirs()
|
||||||
|
channel = await rabbitmq_client.get_channel()
|
||||||
|
await apply_topology(channel, email_topology)
|
||||||
await create_first_user()
|
await create_first_user()
|
||||||
|
writer_task=asyncio.create_task(writer.log_writer())
|
||||||
yield
|
yield
|
||||||
|
writer_task.cancel()
|
||||||
await redis_client.close()
|
await redis_client.close()
|
||||||
await rabbitmq_client.close()
|
await rabbitmq_client.close()
|
||||||
|
|
||||||
|
|||||||
Generated
+49
-3
@@ -44,6 +44,22 @@ files = [
|
|||||||
pamqp = ">=4,<5"
|
pamqp = ">=4,<5"
|
||||||
yarl = "*"
|
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]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.18.5"
|
version = "1.18.5"
|
||||||
@@ -224,13 +240,25 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""]
|
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]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "26.1.0"
|
version = "26.1.0"
|
||||||
description = "Classes Without Boilerplate"
|
description = "Classes Without Boilerplate"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["dev"]
|
groups = ["daemon", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"},
|
{file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"},
|
||||||
{file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"},
|
{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)"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "mako"
|
name = "mako"
|
||||||
version = "1.3.12"
|
version = "1.3.12"
|
||||||
@@ -1027,7 +1073,7 @@ version = "3.0.3"
|
|||||||
description = "Safely add untrusted strings to HTML/XML markup."
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main", "daemon"]
|
||||||
files = [
|
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_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
|
||||||
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
|
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
|
||||||
@@ -2527,4 +2573,4 @@ propcache = ">=0.2.1"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.13,<4.0"
|
python-versions = ">=3.13,<4.0"
|
||||||
content-hash = "025191389a53d34b4a8430d8ab7afde489ccd7692f5e0e8e2d29b8c26aa94364"
|
content-hash = "2ac8d07b41e04495f16ac9da7093f96ec08d094402ebafe48a421a40a98c7d05"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies = [
|
|||||||
"psycopg2-binary (>=2.9.12,<3.0.0)",
|
"psycopg2-binary (>=2.9.12,<3.0.0)",
|
||||||
"redis (>=8.1.0,<9.0.0)",
|
"redis (>=8.1.0,<9.0.0)",
|
||||||
"aio-pika (>=10.0.1,<11.0.0)",
|
"aio-pika (>=10.0.1,<11.0.0)",
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.group.web.dependencies]
|
[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"
|
requests-async = ">=0.2.4,<0.3.0"
|
||||||
|
|
||||||
[tool.poetry.group.daemon.dependencies]
|
[tool.poetry.group.daemon.dependencies]
|
||||||
|
aiosmtpd = ">=1.4.6,<2.0.0"
|
||||||
|
jinja2 = ">=3.1.6,<4.0.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
|||||||
+46
-24
@@ -1,8 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from contextvars import ContextVar
|
||||||
from time import gmtime, perf_counter, strftime
|
from time import gmtime, perf_counter, strftime
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
@@ -11,8 +13,28 @@ from starlette.concurrency import iterate_in_threadpool
|
|||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.responses import Response, StreamingResponse
|
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:
|
async def dispatch(self, request: Request, call_next)->Response:
|
||||||
start_time = perf_counter()
|
start_time = perf_counter()
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
@@ -22,19 +44,26 @@ class ProcessingTimeMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
|
|
||||||
class LoggingMiddleware(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())
|
async def build_log_line(self, request_id, method, path, status_code, detail, client_ip) -> str:
|
||||||
file_time = strftime("%b_%Y", gmtime())
|
return f"[{request_id}] [{method}] [{path}] [{status_code}] [{detail}] [{client_ip}]"
|
||||||
client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown')
|
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
body = str(exc)
|
line=await self.build_log_line(request_id=request_id, method=method, path=path, status_code=500, detail=repr(exc), client_ip=client_ip)
|
||||||
async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file:
|
log_queue.put_nowait(("endpoints", line))
|
||||||
await file.write(f"[{current_time}] [500] [{body}] [{client_ip}]\n")
|
return JSONResponse(
|
||||||
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
|
status_code=500,
|
||||||
|
content={"detail": "Internal Server Error", "request_id": request_id}
|
||||||
|
)
|
||||||
|
|
||||||
streaming_response = cast(StreamingResponse, response)
|
streaming_response = cast(StreamingResponse, response)
|
||||||
|
|
||||||
chunks = []
|
chunks = []
|
||||||
@@ -49,23 +78,16 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|||||||
body = parsed.get("detail", None) if not isinstance(parsed, bool) else None
|
body = parsed.get("detail", None) if not isinstance(parsed, bool) else None
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
body = None
|
body = None
|
||||||
|
|
||||||
async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file:
|
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)
|
||||||
await file.write(f"[{current_time}] [{response.status_code}] [{body}] [{client_ip}]\n")
|
log_queue.put_nowait(("endpoints", line))
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class LoggerDB(logging.Handler):
|
class LoggerDB(logging.Handler, LogWriter):
|
||||||
|
|
||||||
def emit(self, record: logging.LogRecord) -> None:
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
msg = self.format(record)
|
msg = self.format(record)
|
||||||
asyncio.create_task(self._write(msg))
|
rid=request_id_ctx.get()
|
||||||
|
log_queue.put_nowait(("sql",f"[{rid}] {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")
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
import smtplib
|
||||||
import aio_pika
|
|
||||||
|
|
||||||
from src.messaging.rabbitmq_client import rabbitmq_client
|
from src.messaging.rabbitmq_client import rabbitmq_client
|
||||||
|
from src.service.email.email_welcome import DaemonEmailSender
|
||||||
|
|
||||||
|
|
||||||
class WelcomeEmailConsumer:
|
class WelcomeEmailConsumer:
|
||||||
@@ -10,22 +10,33 @@ class WelcomeEmailConsumer:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.channel = None
|
self.channel = None
|
||||||
self.queue = None
|
self.queue = None
|
||||||
|
self.daemon=DaemonEmailSender()
|
||||||
async def setup(self)->None:
|
|
||||||
|
|
||||||
|
async def setup(self) -> None:
|
||||||
self.channel = await rabbitmq_client.get_channel()
|
self.channel = await rabbitmq_client.get_channel()
|
||||||
await self.channel.set_qos(prefetch_count=10)
|
await self.channel.set_qos(prefetch_count=10)
|
||||||
|
self.queue = await self.channel.get_queue("queue_welcome_email")
|
||||||
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 def process_message(self, message) -> None:
|
||||||
|
|
||||||
async with message.process():
|
async with message.process(ignore_processed=True):
|
||||||
data = json.loads(message.body)
|
data = json.loads(message.body)
|
||||||
print(f"Обрабатываю: {data}, метка: {message.routing_key}")
|
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:
|
async def start_consuming(self)->None:
|
||||||
|
|
||||||
@@ -46,15 +57,12 @@ class ResetEmailConsumer:
|
|||||||
self.channel = None
|
self.channel = None
|
||||||
self.queue = None
|
self.queue = None
|
||||||
|
|
||||||
async def setup(self)->None:
|
|
||||||
|
async def setup(self) -> None:
|
||||||
self.channel = await rabbitmq_client.get_channel()
|
self.channel = await rabbitmq_client.get_channel()
|
||||||
await self.channel.set_qos(prefetch_count=10)
|
await self.channel.set_qos(prefetch_count=10)
|
||||||
|
self.queue = await self.channel.get_queue("queue_reset_email")
|
||||||
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 def process_message(self, message) -> None:
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,10 @@ class EmailProducer:
|
|||||||
self.channel = None
|
self.channel = None
|
||||||
self.exchange = None
|
self.exchange = None
|
||||||
|
|
||||||
async def setup(self)->None:
|
async def setup(self) -> None:
|
||||||
|
|
||||||
self.channel = await rabbitmq_client.get_channel()
|
self.channel = await rabbitmq_client.get_channel()
|
||||||
self.exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC)
|
self.exchange = await self.channel.get_exchange("email")
|
||||||
|
|
||||||
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:
|
async def send_welcome_email(self, email:str)->None:
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -26,6 +26,11 @@ class Env(Base):
|
|||||||
RABBITMQ_PORT:int
|
RABBITMQ_PORT:int
|
||||||
RABBITMQ_PORT_UI:int
|
RABBITMQ_PORT_UI:int
|
||||||
|
|
||||||
|
EMAIL_PORT:int
|
||||||
|
SMTP_SERVER:str
|
||||||
|
EMAIL_LOGIN:str
|
||||||
|
EMAIL_PASSWORD:str
|
||||||
|
|
||||||
PROD_MODE:bool
|
PROD_MODE:bool
|
||||||
|
|
||||||
model_config=SettingsConfigDict(env_file="configs/.env", extra=None)
|
model_config=SettingsConfigDict(env_file="configs/.env", extra=None)
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
jinja_env = Environment(
|
||||||
|
loader=FileSystemLoader("src/service/email/templates"),
|
||||||
|
autoescape=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Пароль сброшен</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0; padding:0; background-color:#f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7; padding:40px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background-color:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.06);">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#dc2626; padding:32px 40px; text-align:center;">
|
||||||
|
<h1 style="margin:0; color:#ffffff; font-size:22px; font-weight:600;">The DisExcel</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:40px;">
|
||||||
|
<h2 style="margin:0 0 16px 0; color:#111827; font-size:20px;">Пароль был сброшен</h2>
|
||||||
|
<p style="margin:0 0 24px 0; color:#4b5563; font-size:15px; line-height:1.6;">
|
||||||
|
Пароль от вашей учётной записи в «The DisExcel» был сброшен. Ниже — временный пароль для входа. Рекомендуем сменить его на свой сразу после входа в аккаунт.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f9fafb; border:1px solid #e5e7eb; border-radius:8px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px; text-align:center;">
|
||||||
|
<span style="display:block; margin:0 0 4px 0; color:#9ca3af; font-size:12px; text-transform:uppercase; letter-spacing:0.05em;">Новый пароль</span>
|
||||||
|
<span style="display:inline-block; font-family: 'Courier New', monospace; font-size:20px; font-weight:700; color:#111827; letter-spacing:0.05em;">Тест1234!</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin:24px 0 0 0; color:#9ca3af; font-size:13px; line-height:1.5;">
|
||||||
|
Если вы не запрашивали сброс пароля, срочно свяжитесь с поддержкой — возможно, кто-то получил доступ к вашей учётной записи.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 40px; background-color:#f9fafb; border-top:1px solid #e5e7eb;">
|
||||||
|
<p style="margin:0; color:#9ca3af; font-size:12px; line-height:1.5;">
|
||||||
|
Это автоматическое уведомление от The DisExcel. Не пересылайте это письмо никому.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Добро пожаловать</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0; padding:0; background-color:#f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7; padding:40px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background-color:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.06);">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#4f46e5; padding:32px 40px; text-align:center;">
|
||||||
|
<h1 style="margin:0; color:#ffffff; font-size:22px; font-weight:600;">The DisExcel</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:40px;">
|
||||||
|
<h2 style="margin:0 0 16px 0; color:#111827; font-size:20px;">Добро пожаловать, {{ name }}</h2>
|
||||||
|
<p style="margin:0 0 24px 0; color:#4b5563; font-size:15px; line-height:1.6;">
|
||||||
|
Спасибо за регистрацию на проекте «The DisExcel». Мы рады, что вы с нами — учётная запись уже готова к работе.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" align="center" style="margin:0 auto;">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius:8px; background-color:#4f46e5;">
|
||||||
|
<a href="#" style="display:inline-block; padding:12px 28px; color:#ffffff; font-size:14px; font-weight:600; text-decoration:none;">
|
||||||
|
Перейти в аккаунт
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 40px; background-color:#f9fafb; border-top:1px solid #e5e7eb;">
|
||||||
|
<p style="margin:0; color:#9ca3af; font-size:12px; line-height:1.5;">
|
||||||
|
Если вы не регистрировались на The DisExcel, просто проигнорируйте это письмо.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -79,5 +79,5 @@ from src.messaging.producers.producers import email_producer
|
|||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def protected(current_user:UserOut=Depends(require_permissions()))->dict:
|
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"}
|
return {"protected router": "Hello, this is a protected router"}
|
||||||
|
|||||||
Reference in New Issue
Block a user