diff --git a/app/api/stuff.py b/app/api/stuff.py index 2f4743d..0703458 100644 --- a/app/api/stuff.py +++ b/app/api/stuff.py @@ -1,4 +1,8 @@ +from collections.abc import Callable +from typing import Annotated, Any + from fastapi import APIRouter, Depends, HTTPException, Request, status +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 801393d..75c6acd 100644 --- a/app/main.py +++ b/app/main.py @@ -21,12 +21,12 @@ from app.redis import get_redis from app.services.auth import AuthBearer -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 +35,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/schemas/stuff.py b/app/schemas/stuff.py index 2a9d3c9..6343cdc 100644 --- a/app/schemas/stuff.py +++ b/app/schemas/stuff.py @@ -5,7 +5,6 @@ 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/server.py b/app/server.py new file mode 100644 index 0000000..744463d --- /dev/null +++ b/app/server.py @@ -0,0 +1,20 @@ +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() 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]]