Files
wyndham-ARR/arr_web/run.py
2026-07-29 16:38:05 +08:00

264 lines
9.5 KiB
Python

"""Local entry point. Production injects private OSS and processing adapters."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Sequence
from arr_web.app import PortalApplication, RuntimeHealth
from arr_web.agent_writeback_runtime import (
AgentWritebackRuntime,
compose_oss_agent_writeback,
)
from arr_web.company_jobs import (
PersistentCompanyReportCoordinator,
ProgramCompanyReportExecutor,
)
from arr_web.downloads import ControlledProjectArtifactReader
from arr_web.repository import PostgresPortalRepository, UnavailablePortalRepository
from arr_web.processing_runtime import (
ProcessingInputRuntime,
compose_oss_processing_input,
)
from arr_web.server import serve
from arr_web.services import ProgramMonthlyCoordinator
from monthly_reports.publishing import ArtifactToolBuilder, AtomicReportPublisher
from monthly_reports.repository import DatabaseConfig, PostgresReportRepository
from monthly_reports.service import MonthlyReportService
from company_reports.publishing import (
ArtifactToolBuilder as CompanyArtifactToolBuilder,
AtomicReportPublisher as AtomicCompanyReportPublisher,
)
from company_reports.repository import (
DatabaseConfig as CompanyDatabaseConfig,
PostgresReportRepository as PostgresCompanyReportRepository,
)
from company_reports.service import CompanyReportService
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="arr-web")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8765)
parser.add_argument(
"--db-config",
type=Path,
help="local controlled ARR_DB_* env file; values are never logged",
)
parser.add_argument(
"--driver-path",
type=Path,
help="optional local directory containing the PostgreSQL driver",
)
parser.add_argument(
"--enable-monthly-generation",
action="store_true",
help="enable the database monthly-report mutation endpoint",
)
parser.add_argument(
"--enable-company-reports",
action="store_true",
help="enable Bangkok-gated five-company channel-detail jobs",
)
parser.add_argument(
"--enable-agent-writeback",
action="store_true",
help="enable signed Super Agent result callbacks backed by private OSS",
)
parser.add_argument(
"--enable-processing",
action="store_true",
help="enable private OSS upload and Open Agent dispatch",
)
parser.add_argument(
"--secure-cookies",
action="store_true",
help="mark browser session cookies Secure for an HTTPS deployment",
)
parser.add_argument("--node-binary", type=Path)
parser.add_argument("--artifact-tool-module", type=Path)
return parser
def _read_controlled_database(path: Path) -> Dict[str, object]:
values: Dict[str, str] = {}
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
required = ("ARR_DB_HOST", "ARR_DB_PORT", "ARR_DB_USER", "ARR_DB_PASSWORD", "ARR_DB_NAME")
if any(not values.get(key) for key in required):
raise ValueError("controlled database configuration is incomplete")
return {
"host": values["ARR_DB_HOST"],
"port": int(values["ARR_DB_PORT"]),
"user": values["ARR_DB_USER"],
"password": values["ARR_DB_PASSWORD"],
"dbname": values["ARR_DB_NAME"],
}
def _controlled_connect(
config_path: Path,
driver_path: Optional[Path],
) -> Callable[[str], Any]:
if driver_path is not None:
resolved = driver_path.expanduser().resolve()
if not resolved.is_dir():
raise ValueError("database driver path is unavailable")
sys.path.insert(0, str(resolved))
import psycopg # type: ignore[import-not-found]
parameters = _read_controlled_database(config_path.expanduser().resolve())
def connect(_dsn: str) -> Any:
return psycopg.connect(**parameters, autocommit=False)
return connect
def main(argv: Optional[Sequence[str]] = None) -> int:
args = _parser().parse_args(argv)
connect: Optional[Callable[[str], Any]] = None
try:
if args.db_config:
connect = _controlled_connect(args.db_config, args.driver_path)
repository = PostgresPortalRepository("controlled", connect=connect)
else:
repository = PostgresPortalRepository.from_environment()
repository.list_months()
database_ready = True
except Exception:
repository = UnavailablePortalRepository()
database_ready = False
monthly = None
monthly_ready = False
if args.enable_monthly_generation and database_ready:
try:
report_repository = PostgresReportRepository(
DatabaseConfig("controlled"),
connect=connect,
) if connect is not None else PostgresReportRepository(DatabaseConfig.from_environment())
builder = ArtifactToolBuilder(
PROJECT_ROOT / "monthly_reports" / "xlsx" / "build_workbook.mjs",
node_binary=str(args.node_binary) if args.node_binary else None,
artifact_tool_module=args.artifact_tool_module,
)
output_root = PROJECT_ROOT / "outputs" / "monthly_reports"
monthly = ProgramMonthlyCoordinator(
MonthlyReportService(
report_repository,
builder,
AtomicReportPublisher(PROJECT_ROOT, output_root),
output_root / ".staging",
)
)
monthly_ready = True
except Exception:
monthly = None
monthly_ready = False
company_reports = None
company_reports_ready = False
if args.enable_company_reports and database_ready:
try:
company_repository = (
PostgresCompanyReportRepository(
CompanyDatabaseConfig("controlled"),
connect=connect,
)
if connect is not None
else PostgresCompanyReportRepository(
CompanyDatabaseConfig.from_environment()
)
)
company_builder = CompanyArtifactToolBuilder(
PROJECT_ROOT / "company_reports" / "xlsx" / "build_workbook.mjs",
node_binary=str(args.node_binary) if args.node_binary else None,
artifact_tool_module=args.artifact_tool_module,
)
company_output_root = PROJECT_ROOT / "outputs" / "company_reports"
company_service = CompanyReportService(
company_repository,
company_builder,
AtomicCompanyReportPublisher(PROJECT_ROOT, company_output_root),
company_output_root / ".staging",
)
company_reports = PersistentCompanyReportCoordinator(
PROJECT_ROOT,
company_output_root,
ProgramCompanyReportExecutor(company_service),
)
company_reports_ready = True
except Exception:
company_reports = None
company_reports_ready = False
agent_writeback: Optional[AgentWritebackRuntime] = None
agent_writeback_ready = False
if args.enable_agent_writeback and database_ready:
try:
agent_writeback = compose_oss_agent_writeback(
project_root=PROJECT_ROOT,
connect=connect,
)
agent_writeback_ready = True
except Exception:
agent_writeback = None
agent_writeback_ready = False
processing_input: Optional[ProcessingInputRuntime] = None
processing_ready = False
if args.enable_processing and database_ready:
try:
processing_input = compose_oss_processing_input(
project_root=PROJECT_ROOT,
connect=connect,
)
processing_ready = True
except Exception:
processing_input = None
processing_ready = False
application = PortalApplication(
repository=repository,
upload=(
processing_input.coordinator if processing_input is not None else None
),
agent_results=(
agent_writeback.coordinator if agent_writeback is not None else None
),
monthly=monthly,
company_reports=company_reports,
artifact_reader=(
ControlledProjectArtifactReader(PROJECT_ROOT) if database_ready else None
),
health=RuntimeHealth(
database_ready=database_ready,
upload_ready=processing_ready,
monthly_ready=monthly_ready,
download_ready=database_ready,
company_reports_ready=company_reports_ready,
agent_writeback_ready=agent_writeback_ready,
),
secure_cookies=args.secure_cookies,
)
try:
serve(application, args.host, args.port)
finally:
if company_reports is not None:
company_reports.close()
if agent_writeback is not None:
agent_writeback.close()
if processing_input is not None:
processing_input.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())