This commit is contained in:
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
62
src/core/config.py
Normal file
62
src/core/config.py
Normal 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
54
src/core/exceptions.py
Normal 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
67
src/core/log.py
Normal 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
29
src/core/main.py
Normal 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)
|
||||
40
src/core/routers/__init__.py
Normal file
40
src/core/routers/__init__.py
Normal 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
30
src/core/routers/v1.py
Normal 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
|
||||
Reference in New Issue
Block a user