feat: create backend skeleton

This commit is contained in:
h
2026-05-29 16:11:41 +02:00
parent e08d26dd10
commit 62aac0bf32
37 changed files with 1591 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
from .env import env
from .logging import logger, setup_logging
from .storage import ContentAddressedStorage
__all__ = ["ContentAddressedStorage", "env", "logger", "setup_logging"]
+3
View File
@@ -0,0 +1,3 @@
from . import models
__all__ = ["models"]
+50
View File
@@ -0,0 +1,50 @@
from datetime import datetime
from typing import Any
from sqlalchemy import BigInteger, Column, DateTime, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import Field, SQLModel
class Account(SQLModel, table=True):
__tablename__ = "accounts"
account_id: int | None = Field(default=None, primary_key=True)
tg_user_id: int | None = Field(
default=None, sa_column=Column(BigInteger, unique=True)
)
label: str | None = None
phone: str | None = None
session_name: str
is_active: bool = True
raw: dict[str, Any] = Field(
default_factory=dict, sa_column=Column(JSONB, nullable=False)
)
created_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
)
updated_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
onupdate=func.now(),
)
)
class Message(SQLModel, table=True):
__tablename__ = "messages"
account_id: int = Field(primary_key=True)
chat_id: int = Field(sa_column=Column(BigInteger, primary_key=True))
message_id: int = Field(sa_column=Column(BigInteger, primary_key=True))
date: datetime = Field(sa_column=Column(DateTime(timezone=True), primary_key=True))
raw: dict[str, Any] = Field(
default_factory=dict, sa_column=Column(JSONB, nullable=False)
)
deleted_at: datetime | None = Field(
default=None, sa_column=Column(DateTime(timezone=True))
)
+63
View File
@@ -0,0 +1,63 @@
import os
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class DatabaseSettings(BaseSettings):
host: str = "postgres"
port: int = 5432
user: str = "beavergram"
password: SecretStr = SecretStr("beavergram")
db_name: str = "beavergram"
min_pool_size: int = 5
max_pool_size: int = 20
scripts_connection_url: str = (
"postgresql://beavergram:beavergram@localhost:5433/beavergram"
)
@property
def connection_url(self) -> str:
if os.getenv("RUN_ENVIRONMENT") != "prod":
return self.scripts_connection_url
return (
f"postgresql://{self.user}:{self.password.get_secret_value()}"
f"@{self.host}:{self.port}/{self.db_name}"
)
class TelegramSettings(BaseSettings):
session_name: str = "beavergram"
sessions_dir: str = "sessions"
class ApiSettings(BaseSettings):
host: str = "0.0.0.0" # noqa: S104
port: int = 8080
class StorageSettings(BaseSettings):
root: str = "storage"
shard_depth: int = 2
class LogSettings(BaseSettings):
level: str = "INFO"
level_external: str = "WARNING"
show_time: bool = False
console_width: int = 150
class Settings(BaseSettings):
db: DatabaseSettings = Field(default_factory=DatabaseSettings)
tg: TelegramSettings = Field(default_factory=TelegramSettings)
api: ApiSettings = Field(default_factory=ApiSettings)
storage: StorageSettings = Field(default_factory=StorageSettings)
log: LogSettings = Field(default_factory=LogSettings)
model_config = SettingsConfigDict(
case_sensitive=False, env_file=".env", env_nested_delimiter="__", extra="ignore"
)
env = Settings()
+33
View File
@@ -0,0 +1,33 @@
import logging
from rich.console import Console
from rich.logging import RichHandler
from rich.traceback import install
from .env import env
console = Console(width=env.log.console_width, color_system="auto", force_terminal=True)
def setup_logging() -> None:
logging.basicConfig(
level=env.log.level_external,
format="",
datefmt=None,
handlers=[
RichHandler(
console=console,
markup=True,
rich_tracebacks=True,
enable_link_path=False,
tracebacks_show_locals=True,
omit_repeated_times=False,
show_time=env.log.show_time,
)
],
)
install(console=console, show_locals=True)
logger = logging.getLogger("beavergram")
logger.setLevel(env.log.level)
+29
View File
@@ -0,0 +1,29 @@
import hashlib
from pathlib import Path
class ContentAddressedStorage:
def __init__(self, root: str | Path, shard_depth: int = 2) -> None:
self._root = Path(root)
self._shard_depth = shard_depth
def _path(self, key: str) -> Path:
shards = [key[i * 2 : i * 2 + 2] for i in range(self._shard_depth)]
return self._root.joinpath(*shards, key)
def put(self, data: bytes) -> str:
key = hashlib.sha256(data).hexdigest()
path = self._path(key)
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
return key
def get(self, key: str) -> bytes:
return self._path(key).read_bytes()
def exists(self, key: str) -> bool:
return self._path(key).exists()
def url(self, key: str) -> str:
return str(self._path(key))