Files
template-fastapi/{{project_slug}}/config.py.jinja
Aleksei Sokol b8acb017fd
Some checks failed
Run linters on applied template / Python 3.13 lint and build (push) Failing after 2m36s
Version 0.3.0
Changes:
- fix double exception message in main request_processing span
- add OpenSearch to Jaeger and OpenTelemetry Logs
- add optional OpenTelemetry Logs Exporter to structlog
- update deploy README
2025-12-06 19:41:33 +03:00

193 lines
6.8 KiB
Django/Jinja

"""Application configuration class is defined here."""
from collections import OrderedDict
from dataclasses import asdict, dataclass, field, fields
from pathlib import Path
from types import NoneType, UnionType
from typing import Any, Literal, TextIO, Type
import yaml
from {{project_slug}}.db.config import DBConfig, MultipleDBsConfig
from {{project_slug}}.utils.observability import LoggingConfig, FileLogger, ExporterConfig
from {{project_slug}}.utils.secrets import SecretStr, representSecretStrYAML
@dataclass
class CORSConfig:
allow_origins: list[str]
allow_methods: list[str]
allow_headers: list[str]
allow_credentials: bool
@dataclass
class AppConfig:
host: str
port: int
debug: bool
cors: CORSConfig
@dataclass
class PrometheusConfig:
host: str
port: int
urls_mapping: dict[str, str] = field(default_factory=dict)
@dataclass
class JaegerConfig:
endpoint: str
@dataclass
class ObservabilityConfig:
logging: LoggingConfig
prometheus: PrometheusConfig | None = None
jaeger: JaegerConfig | None = None
@dataclass
class {{ProjectName}}Config:
app: AppConfig
db: MultipleDBsConfig
observability: ObservabilityConfig
def to_order_dict(self) -> OrderedDict:
"""OrderDict transformer."""
def to_ordered_dict_recursive(obj) -> OrderedDict:
"""Recursive OrderDict transformer."""
if isinstance(obj, (dict, OrderedDict)):
return OrderedDict((k, to_ordered_dict_recursive(v)) for k, v in obj.items())
if isinstance(obj, list):
return [to_ordered_dict_recursive(item) for item in obj]
if hasattr(obj, "__dataclass_fields__"):
return OrderedDict(
(field, to_ordered_dict_recursive(getattr(obj, field))) for field in obj.__dataclass_fields__
)
return obj
return OrderedDict([(section, to_ordered_dict_recursive(getattr(self, section))) for section in asdict(self)])
def dump(self, file: str | Path | TextIO) -> None:
"""Export current configuration to a file"""
class OrderedDumper(yaml.SafeDumper):
def represent_dict_preserve_order(self, data):
return self.represent_dict(data.items())
def increase_indent(self, flow=False, indentless=False):
return super().increase_indent(flow, False)
OrderedDumper.add_representer(OrderedDict, OrderedDumper.represent_dict_preserve_order)
OrderedDumper.add_representer(SecretStr, representSecretStrYAML)
if isinstance(file, (str, Path)):
with open(str(file), "w", encoding="utf-8") as file_w:
yaml.dump(self.to_order_dict(), file_w, Dumper=OrderedDumper)
else:
yaml.dump(self.to_order_dict(), file, Dumper=OrderedDumper)
@classmethod
def get_example(cls) -> "{{ProjectName}}Config":
"""Generate an example of configuration."""
res = cls(
app=AppConfig(host="0.0.0.0", port=8080, debug=False, cors=CORSConfig(["*"], ["*"], ["*"], True)),
db=MultipleDBsConfig(
master=DBConfig(
host="localhost",
port=5432,
database="{{project_slug}}_db",
user="postgres",
password="postgres",
pool_size=15,
),
replicas=[
DBConfig(
host="localhost",
port=5433,
user="readonly",
password="readonly",
database="{{project_slug}}_db",
pool_size=8,
)
],
),
observability=ObservabilityConfig(
logging=LoggingConfig(
level="INFO",
root_logger_level="INFO",
exporter=ExporterConfig(endpoint="http://127.0.0.1:4317", level="INFO", tls_insecure=True),
files=[FileLogger(filename="logs/info.log", level="INFO")],
),
prometheus=PrometheusConfig(host="0.0.0.0", port=9090, urls_mapping={"/api/debug/.*": "/api/debug/*"}),
jaeger=JaegerConfig(endpoint="http://127.0.0.1:4318/v1/traces"),
),
)
return res
@classmethod
def load(cls, file: str | Path | TextIO) -> "{{ProjectName}}Config":
"""Import config from the given filename or raise `ValueError` on error."""
try:
if isinstance(file, (str, Path)):
with open(file, "r", encoding="utf-8") as file_r:
data = yaml.safe_load(file_r)
else:
data: dict = yaml.safe_load(file)
return {{ProjectName}}Config._initialize_from_dict({{ProjectName}}Config, data)
except TypeError as exc:
raise ValueError(f"Seems like config file is invalid: {file}") from exc
except Exception as exc:
raise ValueError(f"Could not read app config file: {file}") from exc
@staticmethod
def _initialize_from_dict(t: Type, data: Any) -> Any:
"""Try to initialize given type field-by-field recursively with data from dictionary substituting {} and None
if no value provided.
"""
if isinstance(t, UnionType):
for inner_type in t.__args__:
if inner_type is NoneType and data is None:
return None
try:
return {{ProjectName}}Config._initialize_from_dict(inner_type, data)
except Exception: # pylint: disable=broad-except
pass
raise ValueError(f"Cannot instanciate type '{t}' from {data}")
if hasattr(t, "__origin__") and t.__origin__ is dict:
return data
if not isinstance(data, dict):
if hasattr(t, "__origin__") and t.__origin__ is Literal and data in t.__args__:
return data
return t(data)
init_dict = {}
for fld in fields(t):
inner_data = data.get(fld.name)
if inner_data is None:
if isinstance(fld.type, UnionType) and NoneType in fld.type.__args__:
init_dict[fld.name] = None
continue
inner_data = {}
else:
init_dict[fld.name] = {{ProjectName}}Config._initialize_from_dict(fld.type, inner_data)
return t(**init_dict)
@classmethod
def from_file(cls, config_path: str) -> "{{ProjectName}}Config":
"""Load configuration from the given path."""
if not config_path or not Path(config_path).is_file():
raise ValueError(f"Requested config is not a valid file: {config_path}")
return cls.load(config_path)