first commit
Some checks failed
Build And Push / publish (push) Failing after 3m15s

This commit is contained in:
2025-09-24 04:11:55 +03:00
commit 967bb8d936
45 changed files with 2651 additions and 0 deletions

0
src/core/__init__.py Normal file
View File

62
src/core/config.py Normal file
View File

@ -0,0 +1,62 @@
from os import environ
from os.path import exists
from pydantic import Field, model_validator
from pydantic_extra_types.semantic_version import SemanticVersion
from pydantic_settings import BaseSettings, SettingsConfigDict
def get_version():
if exists('.version'):
with open('.version', encoding='utf-8') as f:
return SemanticVersion.parse(f.read().strip())
return SemanticVersion.parse('0.0.0')
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file='.env',
validate_default=False,
extra='ignore',
)
# App info
APP_NAME: str = 'Hospital Assistant API'
APP_DESCRIPTION: str = 'API for the Hospital Assistant'
APP_PORT: int = Field(default=6767)
VERSION: SemanticVersion = Field(default_factory=get_version)
DEBUG: bool = Field(default=False)
# Security
SECRET_KEY: str = Field(default='secret')
ALGORITHM: str = 'HS256'
# Database
DATABASE_URL: str = Field(default='sqlite:///sql.db')
# Redis
REDIS_URL: str = Field(default='redis://localhost:6379/0')
# Loki Logging
LOKI_URL: str | None = Field(default=None)
# Environment
TMK_BASE_URL: str = Field(default='https://tmk-api.tatar.ru/api')
# ESIA
ESIA_BASE_URL: str = Field(default='https://esia.gosuslugi.ru')
ESIA_CLIENT_ID: str = Field(default='')
ESIA_REDIRECT_URI: str = Field(default='')
ESIA_CONTAINER_PASSWORD: str = Field(default='')
ESIA_CONTAINER_THUMBPRINT: str = Field(default='')
@model_validator(mode='after')
def celery_env(self):
environ['CELERY_BROKER_URL'] = self.REDIS_URL
environ['CELERY_RESULT_BACKEND'] = self.REDIS_URL
return self
settings = Settings()

54
src/core/exceptions.py Normal file
View File

@ -0,0 +1,54 @@
from logging import getLogger
from fastapi import FastAPI, Request, Response, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import (
RequestValidationError,
WebSocketRequestValidationError,
)
from fastapi.responses import ORJSONResponse
from fastapi.utils import is_body_allowed_for_status_code
from fastapi.websockets import WebSocket
from starlette.exceptions import HTTPException
logger = getLogger(__name__)
logger_format = '%s: %s'
def register_exceptions(app: FastAPI):
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException): # type: ignore
headers = getattr(exc, 'headers', None)
if not is_body_allowed_for_status_code(exc.status_code):
return Response(status_code=exc.status_code, headers=headers)
return ORJSONResponse(
status_code=exc.status_code,
content={'detail': exc.detail},
headers=headers,
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler( # type: ignore
request: Request,
exc: RequestValidationError,
):
logger.warning(logger_format, 'Validation Error', exc.body)
return ORJSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({'detail': exc.errors()}),
)
@app.exception_handler(WebSocketRequestValidationError)
async def websocket_validation_exception_handler( # type: ignore
websocket: WebSocket,
exc: WebSocketRequestValidationError,
):
logger.warning(
logger_format, 'WebSocket Validation Error', exc.errors()
)
return await websocket.close(
code=status.WS_1008_POLICY_VIOLATION,
reason=jsonable_encoder(exc.errors()),
)
return app

67
src/core/log.py Normal file
View File

@ -0,0 +1,67 @@
import logging
from typing import Any
from logging_loki import LokiHandler as Loki # type: ignore
from core.config import settings
class LokiHandler(Loki):
def __init__(self):
if not settings.LOKI_URL:
msg = 'LOKI_URL is not set'
raise ValueError(msg)
super().__init__( # type: ignore
settings.LOKI_URL,
tags={
'application': settings.APP_NAME,
'version': str(settings.VERSION),
},
version='1',
)
class Config:
def __init__(self):
self.version = 1
self.disable_existing_loggers = False
self.handlers = self._get_handlers()
self.loggers = self._get_loggers()
@staticmethod
def _get_handlers():
handlers: dict[str, Any] = {
'console': {
'class': 'logging.StreamHandler',
'level': logging.INFO,
'stream': 'ext://sys.stderr',
}
}
if settings.LOKI_URL:
handlers['loki'] = {'class': LokiHandler}
return handlers
def _get_loggers(self):
loggers = {
'': {
'level': logging.INFO,
'handlers': list(self.handlers.keys()),
'propagate': False,
},
}
return loggers
def render(self):
return {
'version': self.version,
'disable_existing_loggers': self.disable_existing_loggers,
'handlers': self.handlers,
'loggers': self.loggers,
}
config = Config().render()

29
src/core/main.py Normal file
View File

@ -0,0 +1,29 @@
from logging import getLogger
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
# from database import lifespan
from middlewares import register_middlewares
from .config import settings
from .exceptions import register_exceptions
from .routers.v1 import router as v1_router
logger = getLogger(__name__)
app = FastAPI(
debug=settings.DEBUG,
title=settings.APP_NAME,
description=settings.APP_DESCRIPTION,
version=str(settings.VERSION),
openapi_url=None,
default_response_class=ORJSONResponse,
# lifespan=lifespan,
docs_url=None,
redoc_url=None,
)
app = register_middlewares(app)
app = register_exceptions(app)
app.include_router(v1_router)

View File

@ -0,0 +1,40 @@
from fastapi import APIRouter
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from fastapi.responses import ORJSONResponse
from core.config import settings
def get_openapi_schema(router: APIRouter):
# if not settings.DEBUG:
# return None
return ORJSONResponse(
get_openapi(
title=settings.APP_NAME,
version=str(settings.VERSION),
description=settings.APP_DESCRIPTION,
routes=router.routes,
servers=[
{
'url': '/',
'description': 'Development environment',
},
{
'url': 'https://med-assistant-api.tatar.ru/',
'description': 'Production environment',
},
],
)
)
def get_swagger_html(router: APIRouter):
# if not settings.DEBUG:
# return None
return get_swagger_ui_html(
openapi_url=f'{router.prefix}/openapi.json',
title='Docs',
)

30
src/core/routers/v1.py Normal file
View File

@ -0,0 +1,30 @@
from fastapi import APIRouter, HTTPException
from apps.esia.v1.router import router as esia_router
from apps.users.v1.router import router as users_router
from . import get_openapi_schema, get_swagger_html
router = APIRouter(prefix='/v1')
router.include_router(esia_router)
router.include_router(users_router)
openapi_schema = get_openapi_schema(router)
swagger_ui_html = get_swagger_html(router)
@router.get('/openapi.json', include_in_schema=False)
async def openapi():
if openapi_schema is None:
raise HTTPException(status_code=404)
return openapi_schema
@router.get('/docs', include_in_schema=False)
async def docs():
if swagger_ui_html is None:
raise HTTPException(status_code=404)
return swagger_ui_html