feat: move report artifacts to OSS storage
This commit is contained in:
@@ -13,7 +13,8 @@ from typing import Any, Callable, Mapping, Optional, Protocol, Sequence
|
||||
|
||||
from arr_database import controlled_connect
|
||||
from monthly_reports.contracts import ErrorCode
|
||||
from monthly_reports.publishing import ArtifactToolBuilder, AtomicReportPublisher
|
||||
from arr_web.processing_runtime import ObjectStoreRuntime, compose_object_store
|
||||
from monthly_reports.publishing import OpenpyxlWorkbookBuilder, AtomicReportPublisher
|
||||
from monthly_reports.repository import (
|
||||
DatabaseConfig,
|
||||
DerivedMonthlyRequest,
|
||||
@@ -273,10 +274,17 @@ class MonthlyOutboxWorker:
|
||||
outbox: OutboxRepository,
|
||||
requests: MonthlyRequestRepository,
|
||||
service: MonthlyReportService,
|
||||
storage_runtime: Optional[ObjectStoreRuntime] = None,
|
||||
) -> None:
|
||||
self._outbox = outbox
|
||||
self._requests = requests
|
||||
self._service = service
|
||||
self._storage_runtime = storage_runtime
|
||||
|
||||
def close(self) -> None:
|
||||
if self._storage_runtime is not None:
|
||||
self._storage_runtime.close()
|
||||
self._storage_runtime = None
|
||||
|
||||
@staticmethod
|
||||
def _daily_version_id(event: OutboxEvent) -> int:
|
||||
@@ -362,8 +370,6 @@ def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="arr-monthly-worker")
|
||||
parser.add_argument("--db-config", type=Path)
|
||||
parser.add_argument("--driver-path", type=Path)
|
||||
parser.add_argument("--node-binary", type=Path)
|
||||
parser.add_argument("--artifact-tool-module", type=Path)
|
||||
parser.add_argument("--output-root", type=Path)
|
||||
parser.add_argument("--poll-seconds", type=float, default=2.0)
|
||||
parser.add_argument("--lease-seconds", type=int, default=DEFAULT_LEASE_SECONDS)
|
||||
@@ -391,18 +397,27 @@ def _runtime(args: argparse.Namespace) -> MonthlyOutboxWorker:
|
||||
output_root.relative_to(PROJECT_ROOT)
|
||||
except ValueError:
|
||||
raise ValueError("worker output root must be inside the project") from None
|
||||
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,
|
||||
)
|
||||
service = MonthlyReportService(
|
||||
repository,
|
||||
builder,
|
||||
AtomicReportPublisher(PROJECT_ROOT, output_root),
|
||||
output_root / ".staging",
|
||||
)
|
||||
return MonthlyOutboxWorker(outbox, repository, service)
|
||||
storage_runtime = compose_object_store()
|
||||
try:
|
||||
service = MonthlyReportService(
|
||||
repository,
|
||||
OpenpyxlWorkbookBuilder(),
|
||||
AtomicReportPublisher(
|
||||
PROJECT_ROOT,
|
||||
output_root,
|
||||
object_store=storage_runtime.object_store,
|
||||
),
|
||||
output_root / ".staging",
|
||||
)
|
||||
return MonthlyOutboxWorker(
|
||||
outbox,
|
||||
repository,
|
||||
service,
|
||||
storage_runtime=storage_runtime,
|
||||
)
|
||||
except Exception:
|
||||
storage_runtime.close()
|
||||
raise
|
||||
|
||||
|
||||
def _emit(outcome: WorkerOutcome) -> None:
|
||||
@@ -415,32 +430,35 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
if args.poll_seconds < 0.1 or args.poll_seconds > 60:
|
||||
raise SystemExit("poll interval must be between 0.1 and 60 seconds")
|
||||
worker = _runtime(args)
|
||||
if args.once:
|
||||
try:
|
||||
outcome = worker.process_next()
|
||||
except WorkerError as error:
|
||||
outcome = WorkerOutcome(status="worker_error", error_code=error.code)
|
||||
_emit(outcome)
|
||||
return 0 if outcome.status in {"idle", "published"} else 2
|
||||
|
||||
stopping = False
|
||||
|
||||
def stop(_signum: int, _frame: object) -> None:
|
||||
nonlocal stopping
|
||||
stopping = True
|
||||
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
while not stopping:
|
||||
try:
|
||||
outcome = worker.process_next()
|
||||
except WorkerError as error:
|
||||
outcome = WorkerOutcome(status="worker_error", error_code=error.code)
|
||||
if outcome.status != "idle":
|
||||
try:
|
||||
if args.once:
|
||||
try:
|
||||
outcome = worker.process_next()
|
||||
except WorkerError as error:
|
||||
outcome = WorkerOutcome(status="worker_error", error_code=error.code)
|
||||
_emit(outcome)
|
||||
if outcome.status in {"idle", "worker_error"} and not stopping:
|
||||
time.sleep(args.poll_seconds)
|
||||
return 0
|
||||
return 0 if outcome.status in {"idle", "published"} else 2
|
||||
|
||||
stopping = False
|
||||
|
||||
def stop(_signum: int, _frame: object) -> None:
|
||||
nonlocal stopping
|
||||
stopping = True
|
||||
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
while not stopping:
|
||||
try:
|
||||
outcome = worker.process_next()
|
||||
except WorkerError as error:
|
||||
outcome = WorkerOutcome(status="worker_error", error_code=error.code)
|
||||
if outcome.status != "idle":
|
||||
_emit(outcome)
|
||||
if outcome.status in {"idle", "worker_error"} and not stopping:
|
||||
time.sleep(args.poll_seconds)
|
||||
return 0
|
||||
finally:
|
||||
worker.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user