"""Конфигурация приложения. Единственное место, читающее окружение.""" from __future__ import annotations from functools import lru_cache from typing import Literal from urllib.parse import quote from pydantic import Field, PostgresDsn from pydantic_settings import BaseSettings, SettingsConfigDict Environment = Literal["local", "staging", "production"] class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=(".env", "../.env"), env_file_encoding="utf-8", extra="ignore", case_sensitive=False, ) # --- Общее --- project_name: str = "lux_fiscal" environment: Environment = "local" debug: bool = False log_level: str = "INFO" base_url: str = "http://localhost:8000" # Хранится строкой, а не list[str]: для сложных типов pydantic-settings # пытается разобрать значение как JSON до валидаторов, и привычная запись # "a,b" из .env приводит к ошибке запуска. Разбор — в свойстве ниже. cors_origins_raw: str = Field(default="", alias="CORS_ORIGINS") # --- Postgres --- postgres_host: str = "postgres" postgres_port: int = 5432 postgres_db: str = "lux_fiscal" postgres_user: str = "lux_fiscal" postgres_password: str = "postgres" # --- Redis --- redis_host: str = "redis" redis_port: int = 6379 redis_db: int = 0 # --- Криптография --- secret_key: str encryption_key: str access_token_expire_minutes: int = 15 refresh_token_expire_days: int = 7 # --- Первый администратор (только для команды bootstrap) --- first_admin_email: str = "admin@example.com" first_admin_password: str = "" first_admin_name: str = "Адміністратор" # --- CRM --- crm_base_url: str = "https://optstore.exocrm.com/api/1.1/" crm_api_key: str = "" crm_secret_key: str = "" crm_shop_key: str = "" crm_sid: int = 1 # Стаб вместо реальной CRM: иначе локальный worker переводил бы боевые заказы # в PACKED после стаб-чеков Checkbox. В проде запрещено. crm_use_stub: bool = False # --- Nova Poshta --- # Ключи НП хранятся у касс (`cash_registers.np_api_key_enc`). Эта переменная # читается только миграцией 0008: переносит старый общий ключ в кассу по умолчанию. nova_poshta_api_key: str = "" # --- Checkbox --- checkbox_base_url: str = "https://api.checkbox.ua" checkbox_client_name: str = "lux_fiscal" checkbox_client_version: str = "0.1.0" # Пауза между запросами к Checkbox из одного процесса: при создании ЕТТН # Checkbox ходит в API Новой Почты, а та отвечает «To many requests» на частые вызовы. checkbox_min_request_interval_ms: int = 1000 # Стаб вместо реального Checkbox: ЕТТН работает только на боевой кассе, # поэтому локально весь цикл прогоняется через стаб. В проде запрещено. checkbox_use_stub: bool = False @property def cors_origins(self) -> list[str]: """Список разрешённых origin'ов из строки через запятую.""" return [item.strip() for item in self.cors_origins_raw.split(",") if item.strip()] @property def database_url(self) -> str: """DSN для асинхронного драйвера (приложение и воркер). Логин и пароль экранируются вручную: `PostgresDsn.build` не делает percent-encoding сам, и любой спецсимвол в пароле (`,`, `@`, `:`, `/`) ломает разбор URL — в т.ч. молча указывая неверный порт. """ return str( PostgresDsn.build( scheme="postgresql+asyncpg", username=quote(self.postgres_user, safe=""), password=quote(self.postgres_password, safe=""), host=self.postgres_host, port=self.postgres_port, path=self.postgres_db, ) ) @property def redis_url(self) -> str: return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}" @property def is_production(self) -> bool: return self.environment == "production" @lru_cache def get_settings() -> Settings: return Settings() # type: ignore[call-arg] settings = get_settings()