Files
han-app/codebase/backend/api-backend/app/cli/seed_settings.py
T

116 lines
4.4 KiB
Python

from __future__ import annotations
import argparse
import asyncio
from pathlib import Path
from typing import Any
import yaml
from sqlalchemy import func, or_
from sqlalchemy.dialects.postgresql import insert
from app.db import AppSetting, Database
from app.settings import get_settings
VALUE_TYPES = {"boolean", "integer", "string", "string_list"}
def load_seed(path: Path) -> list[dict[str, Any]]:
document = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(document, dict) or document.get("schema_version") != 1:
raise ValueError("settings file must have schema_version: 1")
settings = document.get("settings")
if not isinstance(settings, dict) or not settings:
raise ValueError("settings file must contain a non-empty settings mapping")
rows: list[dict[str, Any]] = []
for key, raw in settings.items():
if not isinstance(key, str) or not key.strip():
raise ValueError("setting keys must be non-empty strings")
if not isinstance(raw, dict):
raise ValueError(f"{key}: setting must be a mapping")
value_type = raw.get("type")
if value_type not in VALUE_TYPES:
raise ValueError(f"{key}: unsupported type {value_type!r}")
if not isinstance(raw.get("public"), bool):
raise ValueError(f"{key}: public must be a boolean")
description = raw.get("description")
if description is not None and not isinstance(description, str):
raise ValueError(f"{key}: description must be a string")
rows.append(
{
"setting_key": key,
"setting_value": serialize_value(key, value_type, raw.get("value")),
"value_type": value_type,
"is_public": raw["public"],
"description": description,
"record_status": "A",
}
)
return rows
def serialize_value(key: str, value_type: str, value: Any) -> str:
if value_type == "boolean":
if not isinstance(value, bool):
raise ValueError(f"{key}: boolean value expected")
return str(value).lower()
if value_type == "integer":
if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"{key}: integer value expected")
return str(value)
if value_type == "string_list":
if isinstance(value, list) and all(isinstance(item, str) for item in value):
return ",".join(value)
if isinstance(value, str):
return value
raise ValueError(f"{key}: string or list of strings expected")
if not isinstance(value, str):
raise ValueError(f"{key}: string value expected")
return value
async def seed(path: Path) -> int:
rows = load_seed(path)
database = Database(get_settings().database_url)
try:
async with database.sessions() as session:
for row in rows:
statement = insert(AppSetting).values(**row)
excluded = statement.excluded
statement = statement.on_conflict_do_update(
index_elements=[AppSetting.setting_key],
set_={
"setting_value": excluded.setting_value,
"value_type": excluded.value_type,
"is_public": excluded.is_public,
"description": excluded.description,
"record_status": "A",
"updated_at": func.now(),
},
where=or_(
AppSetting.setting_value.is_distinct_from(excluded.setting_value),
AppSetting.value_type.is_distinct_from(excluded.value_type),
AppSetting.is_public.is_distinct_from(excluded.is_public),
AppSetting.description.is_distinct_from(excluded.description),
AppSetting.record_status != "A",
),
)
await session.execute(statement)
await session.commit()
finally:
await database.close()
return len(rows)
def main() -> None:
parser = argparse.ArgumentParser(description="Idempotently seed application settings")
parser.add_argument("--file", required=True, type=Path)
args = parser.parse_args()
count = asyncio.run(seed(args.file))
print(f"Application settings seeded: {count}")
if __name__ == "__main__":
main()