From 2a3827ff91adcfa3ded90474ed8ea4ce7484cd68 Mon Sep 17 00:00:00 2001 From: Raykov-MS Date: Thu, 14 May 2026 17:36:35 +0300 Subject: [PATCH 1/2] Add services endpoints --- api/src/api/v1/admin.py | 25 ++++++++++++++++++++ api/src/api/v1/router.py | 3 ++- api/src/main.py | 49 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 api/src/api/v1/admin.py diff --git a/api/src/api/v1/admin.py b/api/src/api/v1/admin.py new file mode 100644 index 0000000..ec9cd75 --- /dev/null +++ b/api/src/api/v1/admin.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError + +from src.api.v1.deps import require_admin +from src.db.base import engine +from src.domain.models import Users + +router = APIRouter(prefix="/admin", tags=["admin"]) + + +@router.post("/refresh-tree") +async def refresh_tree(current_user: Users = Depends(require_admin)): + """Обновляет MV дерева статей расходов.""" + try: + async with engine.connect() as conn: + # CONCURRENTLY требует autocommit-режим. + conn = await conn.execution_options(isolation_level="AUTOCOMMIT") + await conn.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY v3.mv_expense_item_tree")) + except SQLAlchemyError as exc: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Не удалось обновить mv_expense_item_tree: {exc}", + ) from exc + return {"status": "ok", "message": "mv_expense_item_tree refreshed"} diff --git a/api/src/api/v1/router.py b/api/src/api/v1/router.py index 388ff1d..449c01e 100644 --- a/api/src/api/v1/router.py +++ b/api/src/api/v1/router.py @@ -1,7 +1,8 @@ from fastapi import APIRouter -from src.api.v1 import auth, users +from src.api.v1 import auth, users, admin api_router = APIRouter() api_router.include_router(auth.router) api_router.include_router(users.router) +api_router.include_router(admin.router) \ No newline at end of file diff --git a/api/src/main.py b/api/src/main.py index f98ba0b..c5b02ef 100644 --- a/api/src/main.py +++ b/api/src/main.py @@ -2,8 +2,10 @@ import os from contextlib import asynccontextmanager from datetime import datetime, timezone -from fastapi import FastAPI +from fastapi import FastAPI, Response from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError from fastapi.staticfiles import StaticFiles from starlette.middleware.gzip import GZipMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware @@ -69,6 +71,41 @@ async def healthcheck2(): "port": settings.PORT, } +@app.get("/healthz", status_code=200) +async def healthz(): + return {"status": "ok", "uptime": UP_TIME, "version": VERSION} + + +@app.get("/readyz") +async def readyz(response: Response): + result = {"status": "ok", "db_status": "ok", "mv_expense_item_tree": "ok"} + try: + async with engine.connect() as conn: + await conn.execute(text("SELECT 1")) + if "postgresql" in settings.DATABASE_URL: + my_exists = ( + await conn.execute( + text( + """ + SELECT 1 + FROM pg_matviews + WHERE schemaname = 'v3' + AND matviewname = 'mv_expense_item_tree' + """ + ) + ) + ).scalar_one_or_none() + if my_exists is None: + result["status"] = "fail" + result["mv_expense_item_tree"] = "missing" + response.status_code = 503 + except SQLAlchemyError as exc: + result["status"] = "fail" + result["db"] = f"error: {exc.__class__.__name__}" + response.status_code = 503 + return result + + if not settings.DEBUG: app.add_middleware( @@ -83,6 +120,16 @@ if not settings.DEBUG: path=settings.ROOT_PATH + "/healthcheck", type_search=SearchType.ABSOLUTE, ), + OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE), + OpenEndpoint( + path=settings.ROOT_PATH + "/healthz", + type_search=SearchType.ABSOLUTE, + ), + OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE), + OpenEndpoint( + path=settings.ROOT_PATH + "/readyz", + type_search=SearchType.ABSOLUTE, + ), OpenEndpoint(path="/openapi.json", type_search=SearchType.START), OpenEndpoint( path=settings.ROOT_PATH + "/openapi.json", From f1f8f5c7b6316fb45e30ff8c51bb536c2c112500 Mon Sep 17 00:00:00 2001 From: Raykov-MS Date: Thu, 14 May 2026 17:40:51 +0300 Subject: [PATCH 2/2] resolve src.db.models --- api/src/api/v1/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/api/v1/admin.py b/api/src/api/v1/admin.py index ec9cd75..8ed6ab2 100644 --- a/api/src/api/v1/admin.py +++ b/api/src/api/v1/admin.py @@ -4,13 +4,13 @@ from sqlalchemy.exc import SQLAlchemyError from src.api.v1.deps import require_admin from src.db.base import engine -from src.domain.models import Users +from src.db.models.app_user import AppUser router = APIRouter(prefix="/admin", tags=["admin"]) @router.post("/refresh-tree") -async def refresh_tree(current_user: Users = Depends(require_admin)): +async def refresh_tree(current_user: AppUser = Depends(require_admin)): """Обновляет MV дерева статей расходов.""" try: async with engine.connect() as conn: