Проект разделен на два репозитория
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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.chat_settings import CHAT_MESSAGE_MAX_LENGTH_KEY, validate_chat_settings
|
||||
from app.db import AppSetting, Database
|
||||
from app.otp_settings import OTP_SETTING_KEYS, validate_otp_settings
|
||||
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 key in OTP_SETTING_KEYS and value_type != "integer":
|
||||
raise ValueError(f"{key}: type must be integer")
|
||||
if key == CHAT_MESSAGE_MAX_LENGTH_KEY and value_type != "integer":
|
||||
raise ValueError(f"{key}: type must be integer")
|
||||
if not isinstance(raw.get("public"), bool):
|
||||
raise ValueError(f"{key}: public must be a boolean")
|
||||
if key in OTP_SETTING_KEYS and raw["public"]:
|
||||
raise ValueError(f"{key}: OTP setting must not be public")
|
||||
if key == CHAT_MESSAGE_MAX_LENGTH_KEY and not raw["public"]:
|
||||
raise ValueError(f"{key}: setting must be public")
|
||||
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",
|
||||
}
|
||||
)
|
||||
validate_otp_settings({row["setting_key"]: row["setting_value"] for row in rows})
|
||||
validate_chat_settings({row["setting_key"]: row["setting_value"] for row in rows})
|
||||
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()
|
||||
Reference in New Issue
Block a user