from os import environ
from pathlib import Path
import sys
from typing import Any, Literal, Optional, Self, Union
from pydantic import (
BaseModel,
Field,
HttpUrl,
model_validator,
TypeAdapter,
PostgresDsn,
NameEmail,
)
from pydantic_settings import BaseSettings
from pydantic.networks import IPvAnyAddress
import toml
class Application(BaseModel):
user: str = "materia"
group: str = "materia"
mode: Literal["production", "development"] = "production"
working_directory: Optional[Path] = Path.cwd()
class Log(BaseModel):
mode: Literal["console", "file", "all"] = "console"
level: Literal["info", "warning", "error", "critical", "debug", "trace"] = "info"
console_format: str = (
"{level: <8} {time:YYYY-MM-DD HH:mm:ss.SSS} - {message}"
)
file_format: str = (
"{level: <8}: {time:YYYY-MM-DD HH:mm:ss.SSS} - {message}"
)
file: Optional[Path] = None
file_rotation: str = "3 days"
file_retention: str = "1 week"
class Server(BaseModel):
scheme: Literal["http", "https"] = "http"
address: IPvAnyAddress = Field(default="127.0.0.1")
port: int = 54601
domain: str = "localhost"
class Database(BaseModel):
backend: Literal["postgresql"] = "postgresql"
scheme: Literal["postgresql+asyncpg"] = "postgresql+asyncpg"
address: IPvAnyAddress = Field(default="127.0.0.1")
port: int = 5432
name: Optional[str] = "materia"
user: str = "materia"
password: Optional[Union[str, Path]] = None
# ssl: bool = False
def url(self) -> str:
if self.backend in ["postgresql"]:
return (
"{}://{}:{}@{}:{}".format(
self.scheme, self.user, self.password, self.address, self.port
)
+ f"/{self.name}"
if self.name
else ""
)
else:
raise NotImplementedError()
class Cache(BaseModel):
backend: Literal["redis"] = "redis" # add: memory
# gc_interval: Optional[int] = 60 # for: memory
scheme: Literal["redis", "rediss"] = "redis"
address: Optional[IPvAnyAddress] = Field(default="127.0.0.1")
port: Optional[int] = 6379
user: Optional[str] = None
password: Optional[Union[str, Path]] = None
database: Optional[int] = 0 # for: redis
def url(self) -> str:
if self.backend in ["redis"]:
if self.user and self.password:
return "{}://{}:{}@{}:{}/{}".format(
self.scheme,
self.user,
self.password,
self.address,
self.port,
self.database,
)
else:
return "{}://{}:{}/{}".format(
self.scheme, self.address, self.port, self.database
)
else:
raise NotImplemented()
class Security(BaseModel):
secret_key: Optional[Union[str, Path]] = None
password_min_length: int = 8
password_hash_algo: Literal["bcrypt"] = "bcrypt"
cookie_http_only: bool = True
cookie_access_token_name: str = "materia_at"
cookie_refresh_token_name: str = "materia_rt"
class OAuth2(BaseModel):
enabled: bool = True
jwt_signing_algo: Literal["HS256"] = "HS256"
# check if signing algo need a key or generate it | HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, EdDSA
jwt_signing_key: Optional[Union[str, Path]] = None
jwt_secret: Optional[Union[str, Path]] = (
None # only for HS256, HS384, HS512 | generate
)
access_token_lifetime: int = 3600
refresh_token_lifetime: int = 730 * 60
refresh_token_validation: bool = False
# @model_validator(mode = "after")
# def check(self) -> Self:
# if self.jwt_signing_algo in ["HS256", "HS384", "HS512"]:
# assert self.jwt_secret is not None, "JWT secret must be set for HS256, HS384, HS512 algorithms"
# else:
# assert self.jwt_signing_key is not None, "JWT signing key must be set"
#
# return self
class Mailer(BaseModel):
enabled: bool = False
scheme: Optional[Literal["smtp", "smtps", "smtp+starttls"]] = None
address: Optional[IPvAnyAddress] = None
port: Optional[int] = None
helo: bool = True
cert_file: Optional[Path] = None
key_file: Optional[Path] = None
from_: Optional[NameEmail] = None
user: Optional[str] = None
password: Optional[str] = None
plain_text: bool = False
class Cron(BaseModel):
pass
class Repository(BaseModel):
capacity: int = 41943040
class Config(BaseSettings, env_prefix="materia_", env_nested_delimiter="_"):
application: Application = Application()
log: Log = Log()
server: Server = Server()
database: Database = Database()
cache: Cache = Cache()
security: Security = Security()
oauth2: OAuth2 = OAuth2()
mailer: Mailer = Mailer()
cron: Cron = Cron()
repository: Repository = Repository()
@staticmethod
def open(path: Path) -> Self | None:
try:
data: dict = toml.load(path)
except Exception as e:
raise e
# return None
else:
return Config(**data)
def write(self, path: Path):
dump = self.model_dump()
# TODO: make normal filter or check model_dump abilities
for key_first in dump.keys():
for key_second in dump[key_first].keys():
if isinstance(dump[key_first][key_second], Path):
dump[key_first][key_second] = str(dump[key_first][key_second])
with open(path, "w") as file:
toml.dump(dump, file)
@staticmethod
def data_dir() -> Path:
cwd = Path.cwd()
if environ.get("MATERIA_DEBUG"):
return cwd / "temp"
else:
return cwd