From 4f0dadc9364f6dfb68c66bdb1c2fd2307b469d9d Mon Sep 17 00:00:00 2001 From: grillazz Date: Mon, 16 Feb 2026 10:25:55 +0100 Subject: [PATCH 1/4] refactor: update logger import paths to use app.services.logging --- app/api/health.py | 2 +- app/api/ml.py | 2 +- app/api/stuff.py | 2 +- app/api/user.py | 2 +- app/database.py | 2 +- app/exception_handlers/base.py | 2 +- app/main.py | 9 ++-- app/models/base.py | 2 +- app/server.py | 19 +++++++ app/services/auth.py | 2 +- app/services/logging.py | 96 ++++++++++++++++++++++++++++++++++ app/services/scheduler.py | 2 +- app/services/smtp.py | 2 +- 13 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 app/server.py create mode 100644 app/services/logging.py diff --git a/app/api/health.py b/app/api/health.py index 4990281..518bd31 100644 --- a/app/api/health.py +++ b/app/api/health.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Query, Request, status from pydantic import EmailStr -from rotoger import get_logger +from app.services.logging import get_logger from starlette.concurrency import run_in_threadpool from app.services.smtp import SMTPEmailService diff --git a/app/api/ml.py b/app/api/ml.py index 21abde2..f1af2b1 100644 --- a/app/api/ml.py +++ b/app/api/ml.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Form from fastapi.responses import StreamingResponse -from rotoger import get_logger +from app.services.logging import get_logger from app.services.llm import get_llm_service diff --git a/app/api/stuff.py b/app/api/stuff.py index 2f4743d..6da3c5f 100644 --- a/app/api/stuff.py +++ b/app/api/stuff.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status -from rotoger import get_logger +from app.services.logging import get_logger from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession diff --git a/app/api/user.py b/app/api/user.py index 5b5ee33..c589a7b 100644 --- a/app/api/user.py +++ b/app/api/user.py @@ -1,7 +1,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, Form, HTTPException, Request, status -from rotoger import get_logger +from app.services.logging import get_logger from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db diff --git a/app/database.py b/app/database.py index e1003d7..50f26f7 100644 --- a/app/database.py +++ b/app/database.py @@ -1,7 +1,7 @@ from collections.abc import AsyncGenerator from fastapi.exceptions import ResponseValidationError -from rotoger import get_logger +from app.services.logging import get_logger from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine diff --git a/app/exception_handlers/base.py b/app/exception_handlers/base.py index b8357f6..7535f58 100644 --- a/app/exception_handlers/base.py +++ b/app/exception_handlers/base.py @@ -1,7 +1,7 @@ import orjson from attrs import define, field from fastapi import Request -from rotoger import get_logger +from app.services.logging import get_logger logger = get_logger() diff --git a/app/main.py b/app/main.py index 801393d..7bc0ed8 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi import Depends, FastAPI, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates -from rotoger import get_logger +from app.services.logging import get_logger from starlette.middleware import Middleware from starlette.middleware.gzip import GZipMiddleware @@ -21,12 +21,13 @@ from app.redis import get_redis from app.services.auth import AuthBearer -logger = get_logger() +# logger = get_logger() templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") @asynccontextmanager async def lifespan(app: FastAPI): + app.logger = get_logger() app.redis = await get_redis() postgres_dsn = global_settings.postgres_url.unicode_string() try: @@ -35,12 +36,12 @@ async def lifespan(app: FastAPI): min_size=5, max_size=20, ) - await logger.ainfo( + await app.logger.ainfo( "Postgres pool created", idle_size=app.postgres_pool.get_idle_size() ) yield except Exception as e: - await logger.aerror("Error during app startup", error=repr(e)) + await app.logger.aerror("Error during app startup", error=repr(e)) raise finally: await app.redis.close() diff --git a/app/models/base.py b/app/models/base.py index 54b96d2..ff617e5 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -2,7 +2,7 @@ from asyncpg import UniqueViolationError from fastapi import HTTPException, status -from rotoger import get_logger +from app.services.logging import get_logger from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import DeclarativeBase, declared_attr diff --git a/app/server.py b/app/server.py new file mode 100644 index 0000000..40d297f --- /dev/null +++ b/app/server.py @@ -0,0 +1,19 @@ +from granian import Granian + +def startup(): + print("Server starting up...") + +def shutdown(): + print("Server shutting down...") + +server = Granian( + "main:app", + host="0.0.0.0", # Bind to all interfaces + port=8000, + workers=4, + interface="asgi", + blocking_threads=8 # Optional: threads per worker for blocking ops +) +server.on_startup(startup) +server.on_shutdown(shutdown) +server.serve_forever() \ No newline at end of file diff --git a/app/services/auth.py b/app/services/auth.py index 144384e..71b6129 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -3,7 +3,7 @@ import jwt from fastapi import HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from rotoger import get_logger +from app.services.logging import get_logger from app.config import settings as global_settings from app.models.user import User diff --git a/app/services/logging.py b/app/services/logging.py new file mode 100644 index 0000000..593629b --- /dev/null +++ b/app/services/logging.py @@ -0,0 +1,96 @@ +import logging +import os +from logging.handlers import RotatingFileHandler +from pathlib import Path + +import orjson +import structlog +from whenever._whenever import Instant + + +def _configure_logger() -> structlog.BoundLogger: + """ + Configures and returns a structlog logger with a rotating file handler. + + The logger is configured using environment variables for path, file size, + and backup count. It formats logs as JSON. + """ + log_dir = Path(os.environ.get("ROTOGER_LOG_PATH", ".")) + log_dir.mkdir(parents=True, exist_ok=True) + log_date = Instant.now().py_datetime().strftime("%Y%m%d") + log_path = log_dir / f"{log_date}_{os.getpid()}.log" + + # Use int() to ensure env var values are correctly typed + max_bytes = int(os.environ.get("ROTOGER_LOG_MAX_BYTES", 10 * 1024 * 1024)) + backup_count = int(os.environ.get("ROTOGER_LOG_BACKUP_COUNT", 5)) + + handler = RotatingFileHandler( + filename=log_path, + maxBytes=max_bytes, + backupCount=backup_count, + encoding="utf-8", + + ) + + # Use structlog's standard library integration + structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="iso", utc=True), + structlog.processors.format_exc_info, + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + # structlog.stdlib.add_logger_name, + ], + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + # Configure the underlying standard logger + formatter = structlog.stdlib.ProcessorFormatter( + # These run after the processors defined in structlog.configure + foreign_pre_chain=[ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="iso", utc=True), + structlog.processors.format_exc_info, + structlog.stdlib.add_logger_name, + ], + processor=structlog.processors.JSONRenderer( + serializer=lambda *args, **kwargs: orjson.dumps(*args, **kwargs).decode() + ), + ) + handler.setFormatter(formatter) + root_logger = logging.getLogger("root") # Get the root logger + root_logger.addHandler(handler) + root_logger.propagate = False # Prevent logs from being propagated to the root logger + root_logger.setLevel(logging.INFO) + + uvicorn_logger = logging.getLogger("uvicorn") # Get the root logger + uvicorn_logger.addHandler(handler) + uvicorn_logger.propagate = False # Prevent logs from being propagated to the root logger + uvicorn_logger.setLevel(logging.INFO) + + sa_logger = logging.getLogger("sqlalchemy") # Get the root logger + sa_logger.addHandler(handler) + sa_logger.propagate = False # Prevent logs from being propagated to the root logger + sa_logger.setLevel(logging.WARNING) + + # Set SQLAlchemy engine logger level specifically if needed + # logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) + return structlog.get_logger() + + + +# Module-level singleton instance +_logger_instance = _configure_logger() + + +def get_logger() -> structlog.BoundLogger: + """ + Returns the configured singleton logger instance. + """ + return _logger_instance diff --git a/app/services/scheduler.py b/app/services/scheduler.py index 6352fb6..43a7bf9 100644 --- a/app/services/scheduler.py +++ b/app/services/scheduler.py @@ -3,7 +3,7 @@ from apscheduler import AsyncScheduler from apscheduler.triggers.interval import IntervalTrigger from attrs import define -from rotoger import get_logger +from app.services.logging import get_logger from sqlalchemy import text from starlette.types import ASGIApp, Receive, Scope, Send diff --git a/app/services/smtp.py b/app/services/smtp.py index bb5ea24..086cd3c 100644 --- a/app/services/smtp.py +++ b/app/services/smtp.py @@ -5,7 +5,7 @@ from attrs import define, field from fastapi.templating import Jinja2Templates from pydantic import EmailStr -from rotoger import get_logger +from app.services.logging import get_logger from app.config import settings as global_settings from app.utils.singleton import SingletonMetaNoArgs From 07c98d79ae4eb30d28bd927a6d12fad3ba4785cd Mon Sep 17 00:00:00 2001 From: grillazz Date: Sat, 28 Feb 2026 13:25:00 +0100 Subject: [PATCH 2/4] feat: enhance logging configuration with default values and shared processors --- app/services/logging.py | 116 ++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 63 deletions(-) diff --git a/app/services/logging.py b/app/services/logging.py index 593629b..a29a709 100644 --- a/app/services/logging.py +++ b/app/services/logging.py @@ -7,90 +7,80 @@ import structlog from whenever._whenever import Instant - -def _configure_logger() -> structlog.BoundLogger: - """ - Configures and returns a structlog logger with a rotating file handler. - - The logger is configured using environment variables for path, file size, - and backup count. It formats logs as JSON. - """ - log_dir = Path(os.environ.get("ROTOGER_LOG_PATH", ".")) +# --------------------------------------------------------------------------- +# Constants / defaults +# --------------------------------------------------------------------------- +_DEFAULT_LOG_PATH = "." +_DEFAULT_MAX_BYTES = 10 * 1024 * 1024 # 10 MiB +_DEFAULT_BACKUP_COUNT = 5 + +# Generic registry: add any stdlib logger name + its desired level here. +_STDLIB_LOGGERS: dict[str, int] = { + "root": logging.INFO, + "uvicorn": logging.INFO, + "sqlalchemy": logging.WARNING, +} + +# Shared processor chain used by both structlog and the stdlib formatter. +_SHARED_PROCESSORS: list[structlog.types.Processor] = [ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_log_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="iso", utc=True), + structlog.processors.format_exc_info, +] + + +def _build_handler() -> RotatingFileHandler: + log_dir = Path(os.getenv("ROTOGER_LOG_PATH", _DEFAULT_LOG_PATH)) log_dir.mkdir(parents=True, exist_ok=True) - log_date = Instant.now().py_datetime().strftime("%Y%m%d") - log_path = log_dir / f"{log_date}_{os.getpid()}.log" - - # Use int() to ensure env var values are correctly typed - max_bytes = int(os.environ.get("ROTOGER_LOG_MAX_BYTES", 10 * 1024 * 1024)) - backup_count = int(os.environ.get("ROTOGER_LOG_BACKUP_COUNT", 5)) + log_path = log_dir / f"{Instant.now().py_datetime().strftime('%Y%m%d')}_{os.getpid()}.log" handler = RotatingFileHandler( filename=log_path, - maxBytes=max_bytes, - backupCount=backup_count, + maxBytes=int(os.getenv("ROTOGER_LOG_MAX_BYTES", _DEFAULT_MAX_BYTES)), + backupCount=int(os.getenv("ROTOGER_LOG_BACKUP_COUNT", _DEFAULT_BACKUP_COUNT)), encoding="utf-8", - ) + handler.setFormatter( + structlog.stdlib.ProcessorFormatter( + foreign_pre_chain=_SHARED_PROCESSORS, + processor=structlog.processors.JSONRenderer( + serializer=lambda *a, **kw: orjson.dumps(*a, **kw).decode() + ), + ) + ) + return handler + - # Use structlog's standard library integration +def _configure_logger() -> structlog.BoundLogger: + """Configure structlog + stdlib loggers and return a bound logger.""" structlog.configure( processors=[ - structlog.contextvars.merge_contextvars, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="iso", utc=True), - structlog.processors.format_exc_info, + *_SHARED_PROCESSORS, structlog.stdlib.ProcessorFormatter.wrap_for_formatter, - # structlog.stdlib.add_logger_name, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) - # Configure the underlying standard logger - formatter = structlog.stdlib.ProcessorFormatter( - # These run after the processors defined in structlog.configure - foreign_pre_chain=[ - structlog.contextvars.merge_contextvars, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="iso", utc=True), - structlog.processors.format_exc_info, - structlog.stdlib.add_logger_name, - ], - processor=structlog.processors.JSONRenderer( - serializer=lambda *args, **kwargs: orjson.dumps(*args, **kwargs).decode() - ), - ) - handler.setFormatter(formatter) - root_logger = logging.getLogger("root") # Get the root logger - root_logger.addHandler(handler) - root_logger.propagate = False # Prevent logs from being propagated to the root logger - root_logger.setLevel(logging.INFO) - - uvicorn_logger = logging.getLogger("uvicorn") # Get the root logger - uvicorn_logger.addHandler(handler) - uvicorn_logger.propagate = False # Prevent logs from being propagated to the root logger - uvicorn_logger.setLevel(logging.INFO) - - sa_logger = logging.getLogger("sqlalchemy") # Get the root logger - sa_logger.addHandler(handler) - sa_logger.propagate = False # Prevent logs from being propagated to the root logger - sa_logger.setLevel(logging.WARNING) - - # Set SQLAlchemy engine logger level specifically if needed - # logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) - return structlog.get_logger() + handler = _build_handler() + + for name, level in _STDLIB_LOGGERS.items(): + logger = logging.getLogger(name) + logger.addHandler(handler) + logger.propagate = False + logger.setLevel(level) + return structlog.get_logger() -# Module-level singleton instance +# Module-level singleton _logger_instance = _configure_logger() def get_logger() -> structlog.BoundLogger: - """ - Returns the configured singleton logger instance. - """ + """Return the configured singleton logger instance.""" return _logger_instance From 21c59533851d8b3d1b8f470661b951632c589220 Mon Sep 17 00:00:00 2001 From: grillazz Date: Mon, 9 Mar 2026 18:05:05 +0100 Subject: [PATCH 3/4] refactor: update logger import paths to use rotoger --- app/api/health.py | 2 +- app/api/ml.py | 2 +- app/api/user.py | 2 +- app/database.py | 2 +- app/exception_handlers/base.py | 2 +- app/models/base.py | 2 +- app/schemas/stuff.py | 5 +- app/services/auth.py | 2 +- app/services/logging.py | 86 ---------------------------------- app/services/scheduler.py | 2 +- app/services/smtp.py | 2 +- pyproject.toml | 4 +- uv.lock | 16 +++---- 13 files changed, 21 insertions(+), 108 deletions(-) delete mode 100644 app/services/logging.py diff --git a/app/api/health.py b/app/api/health.py index 518bd31..4990281 100644 --- a/app/api/health.py +++ b/app/api/health.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Query, Request, status from pydantic import EmailStr -from app.services.logging import get_logger +from rotoger import get_logger from starlette.concurrency import run_in_threadpool from app.services.smtp import SMTPEmailService diff --git a/app/api/ml.py b/app/api/ml.py index f1af2b1..21abde2 100644 --- a/app/api/ml.py +++ b/app/api/ml.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Form from fastapi.responses import StreamingResponse -from app.services.logging import get_logger +from rotoger import get_logger from app.services.llm import get_llm_service diff --git a/app/api/user.py b/app/api/user.py index c589a7b..5b5ee33 100644 --- a/app/api/user.py +++ b/app/api/user.py @@ -1,7 +1,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, Form, HTTPException, Request, status -from app.services.logging import get_logger +from rotoger import get_logger from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db diff --git a/app/database.py b/app/database.py index 50f26f7..e1003d7 100644 --- a/app/database.py +++ b/app/database.py @@ -1,7 +1,7 @@ from collections.abc import AsyncGenerator from fastapi.exceptions import ResponseValidationError -from app.services.logging import get_logger +from rotoger import get_logger from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine diff --git a/app/exception_handlers/base.py b/app/exception_handlers/base.py index 7535f58..b8357f6 100644 --- a/app/exception_handlers/base.py +++ b/app/exception_handlers/base.py @@ -1,7 +1,7 @@ import orjson from attrs import define, field from fastapi import Request -from app.services.logging import get_logger +from rotoger import get_logger logger = get_logger() diff --git a/app/models/base.py b/app/models/base.py index ff617e5..54b96d2 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -2,7 +2,7 @@ from asyncpg import UniqueViolationError from fastapi import HTTPException, status -from app.services.logging import get_logger +from rotoger import get_logger from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import DeclarativeBase, declared_attr diff --git a/app/schemas/stuff.py b/app/schemas/stuff.py index 2a9d3c9..098d776 100644 --- a/app/schemas/stuff.py +++ b/app/schemas/stuff.py @@ -1,11 +1,10 @@ -from typing import Any +from typing import Any, Callable from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, WrapValidator, ValidationError config = ConfigDict(from_attributes=True) - class RandomStuff(BaseModel): chaos: dict[str, Any] = Field( ..., description="Pretty chaotic JSON data can be added here..." diff --git a/app/services/auth.py b/app/services/auth.py index 71b6129..144384e 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -3,7 +3,7 @@ import jwt from fastapi import HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from app.services.logging import get_logger +from rotoger import get_logger from app.config import settings as global_settings from app.models.user import User diff --git a/app/services/logging.py b/app/services/logging.py deleted file mode 100644 index a29a709..0000000 --- a/app/services/logging.py +++ /dev/null @@ -1,86 +0,0 @@ -import logging -import os -from logging.handlers import RotatingFileHandler -from pathlib import Path - -import orjson -import structlog -from whenever._whenever import Instant - -# --------------------------------------------------------------------------- -# Constants / defaults -# --------------------------------------------------------------------------- -_DEFAULT_LOG_PATH = "." -_DEFAULT_MAX_BYTES = 10 * 1024 * 1024 # 10 MiB -_DEFAULT_BACKUP_COUNT = 5 - -# Generic registry: add any stdlib logger name + its desired level here. -_STDLIB_LOGGERS: dict[str, int] = { - "root": logging.INFO, - "uvicorn": logging.INFO, - "sqlalchemy": logging.WARNING, -} - -# Shared processor chain used by both structlog and the stdlib formatter. -_SHARED_PROCESSORS: list[structlog.types.Processor] = [ - structlog.contextvars.merge_contextvars, - structlog.stdlib.add_log_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="iso", utc=True), - structlog.processors.format_exc_info, -] - - -def _build_handler() -> RotatingFileHandler: - log_dir = Path(os.getenv("ROTOGER_LOG_PATH", _DEFAULT_LOG_PATH)) - log_dir.mkdir(parents=True, exist_ok=True) - log_path = log_dir / f"{Instant.now().py_datetime().strftime('%Y%m%d')}_{os.getpid()}.log" - - handler = RotatingFileHandler( - filename=log_path, - maxBytes=int(os.getenv("ROTOGER_LOG_MAX_BYTES", _DEFAULT_MAX_BYTES)), - backupCount=int(os.getenv("ROTOGER_LOG_BACKUP_COUNT", _DEFAULT_BACKUP_COUNT)), - encoding="utf-8", - ) - handler.setFormatter( - structlog.stdlib.ProcessorFormatter( - foreign_pre_chain=_SHARED_PROCESSORS, - processor=structlog.processors.JSONRenderer( - serializer=lambda *a, **kw: orjson.dumps(*a, **kw).decode() - ), - ) - ) - return handler - - -def _configure_logger() -> structlog.BoundLogger: - """Configure structlog + stdlib loggers and return a bound logger.""" - structlog.configure( - processors=[ - *_SHARED_PROCESSORS, - structlog.stdlib.ProcessorFormatter.wrap_for_formatter, - ], - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, - ) - - handler = _build_handler() - - for name, level in _STDLIB_LOGGERS.items(): - logger = logging.getLogger(name) - logger.addHandler(handler) - logger.propagate = False - logger.setLevel(level) - - return structlog.get_logger() - - -# Module-level singleton -_logger_instance = _configure_logger() - - -def get_logger() -> structlog.BoundLogger: - """Return the configured singleton logger instance.""" - return _logger_instance diff --git a/app/services/scheduler.py b/app/services/scheduler.py index 43a7bf9..6352fb6 100644 --- a/app/services/scheduler.py +++ b/app/services/scheduler.py @@ -3,7 +3,7 @@ from apscheduler import AsyncScheduler from apscheduler.triggers.interval import IntervalTrigger from attrs import define -from app.services.logging import get_logger +from rotoger import get_logger from sqlalchemy import text from starlette.types import ASGIApp, Receive, Scope, Send diff --git a/app/services/smtp.py b/app/services/smtp.py index 086cd3c..bb5ea24 100644 --- a/app/services/smtp.py +++ b/app/services/smtp.py @@ -5,7 +5,7 @@ from attrs import define, field from fastapi.templating import Jinja2Templates from pydantic import EmailStr -from app.services.logging import get_logger +from rotoger import get_logger from app.config import settings as global_settings from app.utils.singleton import SingletonMetaNoArgs diff --git a/pyproject.toml b/pyproject.toml index e4b761c..7b1c5d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,14 +22,14 @@ dependencies = [ "redis==7.1.0", "bcrypt==5.0.0", "polars[pyarrow]==1.36.1", - "python-multipart==0.0.20", + "python-multipart==0.0.22", "fastexcel==0.18.0", "inline-snapshot==0.31.1", "dirty-equals==0.11", "polyfactory==3.1.0", "granian==2.6.0", "apscheduler[redis,sqlalchemy]>=4.0.0a6", - "rotoger==0.2.1", + "rotoger==0.3.0", "pyinstrument>=5.1.2", ] diff --git a/uv.lock b/uv.lock index af58202..cace6af 100644 --- a/uv.lock +++ b/uv.lock @@ -438,10 +438,10 @@ requires-dist = [ { name = "pyjwt", specifier = "==2.10.1" }, { name = "pytest", specifier = "==9.0.2" }, { name = "pytest-cov", specifier = "==7.0.0" }, - { name = "python-multipart", specifier = "==0.0.20" }, + { name = "python-multipart", specifier = "==0.0.22" }, { name = "redis", specifier = "==7.1.0" }, { name = "rich", specifier = "==14.2.0" }, - { name = "rotoger", specifier = "==0.2.1" }, + { name = "rotoger", specifier = "==0.3.0" }, { name = "sqlalchemy", specifier = "==2.0.45" }, { name = "uvicorn", extras = ["standard"], specifier = "==0.38.0" }, { name = "uvloop", specifier = "==0.22.1" }, @@ -1075,11 +1075,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -1196,7 +1196,7 @@ wheels = [ [[package]] name = "rotoger" -version = "0.2.1" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1204,9 +1204,9 @@ dependencies = [ { name = "structlog" }, { name = "whenever" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/ad/75a22ddd259505547fd47c36ea984688e3b56d9cbc49c0f98bb95c84c01b/rotoger-0.2.1.tar.gz", hash = "sha256:823bb39c781d6038d2aae1c2c3f6d74c0abb1e9f07b257c079028d6ae3f2589d", size = 1647, upload-time = "2025-11-13T16:12:27.833Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/40/475df92ef562d22489a1b07bc41cf1094dce1d4d03475f59112b97070560/rotoger-0.3.0.tar.gz", hash = "sha256:e407e3f4cf4948886bd26b35bd54e635f58703db406f5a706e4c4579dc273241", size = 2714, upload-time = "2026-03-01T17:08:46.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/da/9422061c62499eaafcf90c4adf3e13c51031d4b593af899451825fa85a7b/rotoger-0.2.1-py3-none-any.whl", hash = "sha256:849ed131068ab724991c38c32fb63e4904efb79e29bf084f37ec11a31ec0c703", size = 2603, upload-time = "2025-11-13T16:12:26.895Z" }, + { url = "https://files.pythonhosted.org/packages/72/85/2164d61cff7594366d5797cc6f33784a05e83a39841701860bbbc41631dc/rotoger-0.3.0-py3-none-any.whl", hash = "sha256:08d3c239f05c0551a9cdb682332f4c1e981844b1b0afa7b1e04c50730bbe2098", size = 3458, upload-time = "2026-03-01T17:08:47.684Z" }, ] [[package]] From a24f440a19f5fe6407d35ab02a84407c7af170ec Mon Sep 17 00:00:00 2001 From: grillazz Date: Mon, 9 Mar 2026 18:08:22 +0100 Subject: [PATCH 4/4] lint --- app/api/stuff.py | 25 ++++++++++++++++++++++--- app/main.py | 3 +-- app/schemas/stuff.py | 4 ++-- app/server.py | 3 ++- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/api/stuff.py b/app/api/stuff.py index 6da3c5f..0703458 100644 --- a/app/api/stuff.py +++ b/app/api/stuff.py @@ -1,5 +1,9 @@ +from collections.abc import Callable +from typing import Annotated, Any + from fastapi import APIRouter, Depends, HTTPException, Request, status -from app.services.logging import get_logger +from pydantic import ValidationError, WrapValidator +from rotoger import get_logger from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession @@ -22,12 +26,26 @@ async def create_random_stuff( return {"id": str(random_stuff.id)} +failed_items: list[dict] = [] # Global or pass via context + +def catch_invalid(v: Any, handler: Callable[[Any], Any] ) -> Any: + try: + return handler(v) + except ValidationError: + failed_items.append(v) # Intercept here! + return None # Or raise if needed + @router.post("/add_many", status_code=status.HTTP_201_CREATED) async def create_multi_stuff( - payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db) + payload: list[Annotated[StuffSchema, WrapValidator(catch_invalid)]], db_session: AsyncSession = Depends(get_db) ): + await logger.ainfo(f">>>{failed_items}") try: - stuff_instances = [Stuff(**stuff.model_dump()) for stuff in payload] + await logger.ainfo(f">>>{failed_items}") + await logger.ainfo(f">>>{payload}") + stuff_instances = [ + Stuff(**stuff.model_dump()) for stuff in payload if stuff is not None + ] db_session.add_all(stuff_instances) await db_session.commit() except SQLAlchemyError as ex: @@ -39,6 +57,7 @@ async def create_multi_stuff( await logger.ainfo( f"{len(stuff_instances)} Stuff instances inserted into the database." ) + return {"inserted": len(stuff_instances)} return True diff --git a/app/main.py b/app/main.py index 7bc0ed8..75c6acd 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi import Depends, FastAPI, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates -from app.services.logging import get_logger +from rotoger import get_logger from starlette.middleware import Middleware from starlette.middleware.gzip import GZipMiddleware @@ -21,7 +21,6 @@ from app.redis import get_redis from app.services.auth import AuthBearer -# logger = get_logger() templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") diff --git a/app/schemas/stuff.py b/app/schemas/stuff.py index 098d776..6343cdc 100644 --- a/app/schemas/stuff.py +++ b/app/schemas/stuff.py @@ -1,7 +1,7 @@ -from typing import Any, Callable +from typing import Any from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field, WrapValidator, ValidationError +from pydantic import BaseModel, ConfigDict, Field config = ConfigDict(from_attributes=True) diff --git a/app/server.py b/app/server.py index 40d297f..744463d 100644 --- a/app/server.py +++ b/app/server.py @@ -1,5 +1,6 @@ from granian import Granian + def startup(): print("Server starting up...") @@ -16,4 +17,4 @@ def shutdown(): ) server.on_startup(startup) server.on_shutdown(shutdown) -server.serve_forever() \ No newline at end of file +server.serve_forever()