feat: initial commit
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from core.automation.scheduler import run_automation_scheduler_scan
|
||||
from core.config.initial.init_data import initialize_data
|
||||
from core.config.settings import config
|
||||
from core.logging import get_logger
|
||||
|
||||
logger = get_logger("core.runtime.cli")
|
||||
|
||||
|
||||
def _resolve_alembic_path() -> Path:
|
||||
"""Resolve alembic.ini path relative to project root."""
|
||||
project_root = Path(__file__).parents[3]
|
||||
alembic_path = project_root / "alembic" / "alembic.ini"
|
||||
if not alembic_path.exists():
|
||||
raise FileNotFoundError(f"Alembic config not found at {alembic_path}")
|
||||
return alembic_path
|
||||
|
||||
|
||||
def _redact_sensitive(text: str) -> str:
|
||||
"""Redact sensitive information from log output."""
|
||||
import re
|
||||
|
||||
SENSITIVE_KEYS = ("password", "token", "secret", "api_key")
|
||||
pattern = r"(?i)(" + "|".join(SENSITIVE_KEYS) + r")\s*[:=]\s*[\"']?([^\"',\n]+)"
|
||||
redacted = re.sub(pattern, r"\1=***", text)
|
||||
|
||||
auth_pattern = r"(?i)(authorization)\s*[:=]\s*[^\n]+"
|
||||
redacted = re.sub(auth_pattern, r"\1=***", redacted)
|
||||
|
||||
redacted = re.sub(r"://[^:]+:[^@]+@", "://***:***@", redacted)
|
||||
return redacted
|
||||
|
||||
|
||||
def run_migrations() -> bool:
|
||||
"""Run alembic migrations in a subprocess to avoid event loop conflicts."""
|
||||
import os
|
||||
|
||||
logger.info("Running alembic migrations")
|
||||
try:
|
||||
config_path = _resolve_alembic_path()
|
||||
logger.info("Using alembic config", path=str(config_path))
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = "backend/src"
|
||||
|
||||
result = subprocess.run(
|
||||
["uv", "run", "alembic", "-c", str(config_path), "upgrade", "head"],
|
||||
cwd=Path(__file__).parents[3],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(
|
||||
"Migration failed",
|
||||
returncode=result.returncode,
|
||||
stderr=_redact_sensitive(result.stderr),
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info("Migrations completed successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Migration failed", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def run_init_data() -> bool:
|
||||
"""Initialize bootstrap data."""
|
||||
logger.info("Running init-data")
|
||||
try:
|
||||
result = await initialize_data()
|
||||
if result:
|
||||
logger.info("Init-data completed successfully")
|
||||
else:
|
||||
logger.error("Init-data returned False")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Init-data failed", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
async def bootstrap() -> bool:
|
||||
"""Run migrations followed by init-data."""
|
||||
logger.info("Starting bootstrap (migrate + init-data)")
|
||||
|
||||
if not run_migrations():
|
||||
logger.error("Bootstrap aborted: migrations failed")
|
||||
return False
|
||||
|
||||
if not await run_init_data():
|
||||
logger.error("Bootstrap aborted: init-data failed")
|
||||
return False
|
||||
|
||||
logger.info("Bootstrap completed successfully")
|
||||
return True
|
||||
|
||||
|
||||
async def run_automation_scheduler_forever() -> None:
|
||||
if not config.automation_scheduler.enabled:
|
||||
logger.info("Automation scheduler disabled by config")
|
||||
return
|
||||
|
||||
interval_seconds = int(config.automation_scheduler.interval_seconds)
|
||||
batch_limit = int(config.automation_scheduler.batch_limit)
|
||||
logger.info(
|
||||
"Starting automation scheduler",
|
||||
interval_seconds=interval_seconds,
|
||||
batch_limit=batch_limit,
|
||||
)
|
||||
|
||||
async def scan_job() -> None:
|
||||
try:
|
||||
await run_automation_scheduler_scan(limit=batch_limit)
|
||||
except Exception as exc:
|
||||
logger.exception("Automation scheduler scan failed", error=str(exc))
|
||||
|
||||
scheduler = AsyncIOScheduler()
|
||||
scheduler.add_job(
|
||||
scan_job,
|
||||
trigger=IntervalTrigger(seconds=interval_seconds),
|
||||
id="automation_scheduler_scan",
|
||||
name="Automation scheduler scan",
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
)
|
||||
scheduler.start()
|
||||
|
||||
stop_event = asyncio.Event()
|
||||
try:
|
||||
await stop_event.wait()
|
||||
finally:
|
||||
scheduler.shutdown(wait=False)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""CLI entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
logger.error("No command provided")
|
||||
logger.info("Usage: python -m core.runtime.cli <command>")
|
||||
logger.info(
|
||||
"Available commands: migrate, init-data, bootstrap, automation-scheduler"
|
||||
)
|
||||
return 1
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "migrate":
|
||||
success = run_migrations()
|
||||
elif command == "init-data":
|
||||
success = asyncio.run(run_init_data())
|
||||
elif command == "bootstrap":
|
||||
success = asyncio.run(bootstrap())
|
||||
elif command == "automation-scheduler":
|
||||
asyncio.run(run_automation_scheduler_forever())
|
||||
return 0
|
||||
else:
|
||||
logger.error("Unknown command", command=command)
|
||||
logger.info(
|
||||
"Available commands: migrate, init-data, bootstrap, automation-scheduler"
|
||||
)
|
||||
return 1
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user