diff --git a/src/fastapi_app/.env.example b/src/fastapi_app/.env.example index d557292..2651cfd 100644 --- a/src/fastapi_app/.env.example +++ b/src/fastapi_app/.env.example @@ -1,10 +1,10 @@ -DB_HOST=db -DB_PORT=5432 -DB_USER=user -DB_PASSWORD=Qwe123 -DB_NAME=api_db +POSTGRES_HOST=db +POSTGRES_PORT=5432 +POSTGRES_USER=user +POSTGRES_PASSWORD=Qwe123 +POSTGRES_NAME=api_db -SERVER_HOST=0.0.0.0 -SERVER_PORT=8000 +API_HOST=0.0.0.0 +API_PORT=8000 JWT_SECRET_KEY=v9LctjUWwol4XbvczPiLFMDtZ8aal7mm diff --git a/src/fastapi_app/bin/__main__.py b/src/fastapi_app/bin/__main__.py index 5343917..f7e51e9 100644 --- a/src/fastapi_app/bin/__main__.py +++ b/src/fastapi_app/bin/__main__.py @@ -3,14 +3,14 @@ import logging import uvicorn import lib.app.app as app_module -from lib.app import settings as app_settings +import lib.app.settings as app_settings logger = logging.getLogger(__name__) app_instance = app_module.Application() app = app_instance.create_app() -settings = app_settings.get_settings() +settings = app_settings.settings if __name__ == "__main__": diff --git a/src/fastapi_app/docker-compose.dev.yml b/src/fastapi_app/docker-compose.dev.yml index 043620c..e38d4e4 100644 --- a/src/fastapi_app/docker-compose.dev.yml +++ b/src/fastapi_app/docker-compose.dev.yml @@ -4,13 +4,13 @@ services: db: image: postgres:15.2 environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_NAME} env_file: - .env ports: - - "${DB_PORT}:${DB_PORT}" + - "${POSTGRES_PORT}:${POSTGRES_PORT}" volumes: - postgres_data:/var/lib/postgresql/data/ restart: always @@ -25,7 +25,7 @@ services: restart: always entrypoint: ["/opt/app/entrypoint.sh"] ports: - - "${SERVER_PORT}:${SERVER_PORT}" + - "${API_PORT}:${API_PORT}" depends_on: - db env_file: diff --git a/src/fastapi_app/docker-compose.yml b/src/fastapi_app/docker-compose.yml index d9879bf..08ef87e 100644 --- a/src/fastapi_app/docker-compose.yml +++ b/src/fastapi_app/docker-compose.yml @@ -4,13 +4,13 @@ services: db: image: postgres:15.2 environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_NAME} env_file: - .env ports: - - "127.0.0.1:${DB_PORT}:${DB_PORT}" + - "127.0.0.1:${API_PORT}:${API_PORT}" volumes: - postgres_data:/var/lib/postgresql/data/ restart: always diff --git a/src/fastapi_app/lib/api/v1/services/token.py b/src/fastapi_app/lib/api/v1/services/token.py index d4064b9..c18b5f2 100644 --- a/src/fastapi_app/lib/api/v1/services/token.py +++ b/src/fastapi_app/lib/api/v1/services/token.py @@ -2,11 +2,11 @@ import fastapi from jose import JWTError, jwt from pydantic import ValidationError +import lib.app.settings as app_settings from lib.api.v1 import schemas as app_schemas -from lib.app import settings as app_settings app = fastapi.FastAPI() -settings = app_settings.get_settings() +settings = app_settings.settings security = fastapi.security.HTTPBearer() @@ -16,7 +16,7 @@ def get_token_data( ) -> app_schemas.entity.Token: token = authorization.credentials try: - secret_key = settings.jwt_secret_key + secret_key = settings.project.jwt_secret_key payload = jwt.decode(token, secret_key, algorithms=["HS256"]) return app_schemas.entity.Token(**payload) except (JWTError, ValidationError): diff --git a/src/fastapi_app/lib/app/app.py b/src/fastapi_app/lib/app/app.py index af2c2b6..6e24e89 100644 --- a/src/fastapi_app/lib/app/app.py +++ b/src/fastapi_app/lib/app/app.py @@ -1,18 +1,15 @@ import logging -import logging.config as logging_config import fastapi -from .logger import LOGGING -from .settings import get_settings +import lib.app.settings as app_settings -logging_config.dictConfig(LOGGING) logger = logging.getLogger(__name__) class Application: def __init__(self) -> None: - self.settings = get_settings() + self.settings = app_settings self.logger = logging.getLogger(__name__) self.producer = None diff --git a/src/fastapi_app/lib/app/settings.py b/src/fastapi_app/lib/app/settings.py index 46d1cbd..e3853ea 100644 --- a/src/fastapi_app/lib/app/settings.py +++ b/src/fastapi_app/lib/app/settings.py @@ -1,35 +1,26 @@ -import functools +import logging.config as logging_config import pydantic import pydantic_settings - -class DbSettings(pydantic_settings.BaseSettings): - host: str = pydantic.Field("127.0.0.1", validation_alias="db_host") - port: int = pydantic.Field(5432, validation_alias="db_port") - user: str = pydantic.Field(..., validation_alias="db_user") - password: str = pydantic.Field(..., validation_alias="db_password") - name: str = pydantic.Field("db_name", validation_alias="db_name") - - -class ApiSettings(pydantic_settings.BaseSettings): - host: str = pydantic.Field("0.0.0.0", validation_alias="server_host") - port: int = pydantic.Field(8000, validation_alias="server_port") +import lib.app.split_settings as app_split_settings class Settings(pydantic_settings.BaseSettings): - debug: str = pydantic.Field("false", validation_alias="debug") - db: DbSettings = pydantic.Field(default_factory=lambda: DbSettings()) - api: ApiSettings = pydantic.Field(default_factory=lambda: ApiSettings()) - - jwt_secret_key: str = pydantic.Field(..., validation_alias="jwt_secret_key") - - @pydantic.field_validator("debug") - @classmethod - def validate_debug(cls, v: str) -> bool: - return v.lower() == "true" + api: app_split_settings.ApiSettings = pydantic.Field(default_factory=lambda: app_split_settings.ApiSettings()) + postgres: app_split_settings.PostgresSettings = pydantic.Field( + default_factory=lambda: app_split_settings.PostgresSettings() + ) + logger: app_split_settings.LoggingSettings = pydantic.Field( + default_factory=lambda: app_split_settings.LoggingSettings() + ) + project: app_split_settings.ProjectSettings = pydantic.Field( + default_factory=lambda: app_split_settings.ProjectSettings() + ) -@functools.lru_cache -def get_settings() -> Settings: - return Settings() +settings = Settings() # todo Вынести в инициализацию + +logging_config.dictConfig( # todo Вынести в инициализацию + app_split_settings.get_logging_config(**settings.logger.model_dump()) +) diff --git a/src/fastapi_app/lib/app/split_settings/__init__.py b/src/fastapi_app/lib/app/split_settings/__init__.py new file mode 100644 index 0000000..627e186 --- /dev/null +++ b/src/fastapi_app/lib/app/split_settings/__init__.py @@ -0,0 +1,12 @@ +from .api import * +from .logger import * +from .postgres import * +from .project import * + +__all__ = [ + "ApiSettings", + "LoggingSettings", + "get_logging_config", + "PostgresSettings", + "ProjectSettings", +] diff --git a/src/fastapi_app/lib/app/split_settings/api.py b/src/fastapi_app/lib/app/split_settings/api.py new file mode 100644 index 0000000..3638d34 --- /dev/null +++ b/src/fastapi_app/lib/app/split_settings/api.py @@ -0,0 +1,15 @@ +import pydantic_settings + +import lib.app.split_settings.utils as app_split_settings_utils + + +class ApiSettings(pydantic_settings.BaseSettings): + model_config = pydantic_settings.SettingsConfigDict( + env_file=app_split_settings_utils.ENV_PATH, + env_prefix="API_", + env_file_encoding="utf-8", + extra="ignore", + ) + + host: str = "0.0.0.0" + port: int = 8000 diff --git a/src/fastapi_app/lib/app/split_settings/logger.py b/src/fastapi_app/lib/app/split_settings/logger.py new file mode 100644 index 0000000..360487a --- /dev/null +++ b/src/fastapi_app/lib/app/split_settings/logger.py @@ -0,0 +1,79 @@ +import pydantic_settings + +import lib.app.split_settings.utils as app_split_settings_utils + + +class LoggingSettings(pydantic_settings.BaseSettings): + model_config = pydantic_settings.SettingsConfigDict( + env_file=app_split_settings_utils.ENV_PATH, env_file_encoding="utf-8", extra="ignore" + ) + + log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + log_default_handlers: list[str] = [ + "console", + ] + + log_level_handlers: str = "INFO" + log_level_loggers: str = "INFO" + log_level_root: str = "INFO" + + +def get_logging_config( + log_format: str, + log_default_handlers: list[str], + log_level_handlers: str, + log_level_loggers: str, + log_level_root: str, +): + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": {"format": log_format}, + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "%(levelprefix)s %(message)s", + "use_colors": None, + }, + "access": { + "()": "uvicorn.logging.AccessFormatter", + "fmt": "%(levelprefix)s %(client_addr)s - '%(request_line)s' %(status_code)s", + }, + }, + "handlers": { + "console": { + "level": log_level_handlers, + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + "access": { + "formatter": "access", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "": { + "handlers": log_default_handlers, + "level": log_level_loggers, + }, + "uvicorn.error": { + "level": log_level_loggers, + }, + "uvicorn.access": { + "handlers": ["access"], + "level": log_level_loggers, + "propagate": False, + }, + }, + "root": { + "level": log_level_root, + "formatter": "verbose", + "handlers": log_default_handlers, + }, + } diff --git a/src/fastapi_app/lib/app/split_settings/postgres.py b/src/fastapi_app/lib/app/split_settings/postgres.py new file mode 100644 index 0000000..62452f6 --- /dev/null +++ b/src/fastapi_app/lib/app/split_settings/postgres.py @@ -0,0 +1,21 @@ +import pydantic +import pydantic_settings + +import lib.app.split_settings.utils as app_split_settings_utils + + +class PostgresSettings(pydantic_settings.BaseSettings): + model_config = pydantic_settings.SettingsConfigDict( + env_file=app_split_settings_utils.ENV_PATH, + env_prefix="POSTGRES_", + env_file_encoding="utf-8", + extra="ignore", + ) + + name: str = "database_name" + host: str = "localhost" + port: int = 5432 + user: str = "app" + password: pydantic.SecretStr = pydantic.Field( + default=..., validation_alias=pydantic.AliasChoices("password", "postgres_password") + ) diff --git a/src/fastapi_app/lib/app/split_settings/project.py b/src/fastapi_app/lib/app/split_settings/project.py new file mode 100644 index 0000000..6102809 --- /dev/null +++ b/src/fastapi_app/lib/app/split_settings/project.py @@ -0,0 +1,19 @@ +import pydantic +import pydantic_settings + +import lib.app.split_settings.utils as app_split_settings_utils + + +class ProjectSettings(pydantic_settings.BaseSettings): + model_config = pydantic_settings.SettingsConfigDict( + env_file=app_split_settings_utils.ENV_PATH, + env_file_encoding="utf-8", + extra="ignore", + ) + + debug: str = "false" + jwt_secret_key: pydantic.SecretStr = pydantic.Field(default=..., validation_alias="jwt_secret_key") + + @pydantic.field_validator("debug") + def validate_debug(cls, v: str) -> bool: + return v.lower() == "true" diff --git a/src/fastapi_app/lib/app/split_settings/utils.py b/src/fastapi_app/lib/app/split_settings/utils.py new file mode 100644 index 0000000..459631e --- /dev/null +++ b/src/fastapi_app/lib/app/split_settings/utils.py @@ -0,0 +1,4 @@ +import pathlib + +BASE_PATH = pathlib.Path(__file__).parent.parent.parent.parent.resolve() +ENV_PATH = BASE_PATH / ".env" diff --git a/src/fastapi_app/lib/db/postgres.py b/src/fastapi_app/lib/db/postgres.py index 7715c7c..ecefe41 100644 --- a/src/fastapi_app/lib/db/postgres.py +++ b/src/fastapi_app/lib/db/postgres.py @@ -3,9 +3,9 @@ import typing from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase -from lib.app import settings as app_settings +import lib.app.settings as app_settings -settings = app_settings.get_settings() +settings = app_settings.settings # Создаём базовый класс для будущих моделей @@ -22,7 +22,7 @@ class AsyncDB: f"postgresql+asyncpg://{settings.db.user}:{settings.db.password}" f"@{settings.db.host}:{settings.db.port}/{settings.db.name}" ) - self.engine = create_async_engine(self.database_dsn, echo=settings.debug, future=True) + self.engine = create_async_engine(self.database_dsn, echo=settings.project.debug, future=True) self.async_session = async_sessionmaker(self.engine, class_=AsyncSession, expire_on_commit=False)