feat: config model and yaml loader

Add Pydantic config models (SqlServerConfig, AccessConfig, RuntimeConfig,
FileMapping, SyncConfig) and a YAML loader (load_config). FileMapping provides
source_path() and target_table() helpers; number-typed YAML keys/values (e.g.
root: 2026) are coerced to str via coerce_numbers_to_str. Includes
config.example.yaml template (config.yaml with real credentials stays
gitignored) and pyproject.toml pytest config (pythonpath=src).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-07-14 11:53:20 +08:00
parent fb025bb061
commit 7c6cb10b91
4 changed files with 142 additions and 0 deletions

50
src/sync/config.py Normal file
View File

@@ -0,0 +1,50 @@
from __future__ import annotations
from pathlib import Path
import yaml
from pydantic import BaseModel, ConfigDict, Field
class SqlServerConfig(BaseModel):
conn_str: str
sync_queue_table: str = "dbo.SyncQueue"
class AccessConfig(BaseModel):
model_config = ConfigDict(coerce_numbers_to_str=True)
driver: str
roots: dict[str, str]
class RuntimeConfig(BaseModel):
poll_interval_seconds: int = 10
capture_batch_size: int = 500
apply_batch_size: int = 200
max_retries: int = 5
retry_backoff_seconds: int = 30
cleanup_batch_size: int = 200
cleanup_lock_retries: int = 3
class FileMapping(BaseModel):
model_config = ConfigDict(coerce_numbers_to_str=True)
file: str
root: str
schema: str
year_suffix: str = ""
exclude_tables: list[str] = Field(default_factory=list)
include_tables: list[str] | None = None
def source_path(self, cfg: "SyncConfig") -> str:
base = cfg.access.roots[self.root]
return f"{base}\\{self.file}"
def target_table(self, access_table: str) -> str:
return f"{access_table}{self.year_suffix}"
class SyncConfig(BaseModel):
sql_server: SqlServerConfig
access: AccessConfig
runtime: RuntimeConfig
files: list[FileMapping]
logging: dict | None = None
def load_config(path: str) -> SyncConfig:
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return SyncConfig(**data)