202 lines
5.7 KiB
Python
202 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
"""Orchestrate the OSM station/track import and load pipeline."""
|
|
|
|
import argparse
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Callable, Sequence
|
|
|
|
from backend.app.core.osm_config import DEFAULT_REGIONS
|
|
from backend.scripts import (
|
|
stations_import,
|
|
stations_load,
|
|
tracks_import,
|
|
tracks_load,
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Stage:
|
|
label: str
|
|
runner: Callable[[list[str] | None], int]
|
|
args: list[str]
|
|
input_path: Path | None = None
|
|
output_path: Path | None = None
|
|
|
|
|
|
def build_argument_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run the station and track import/load workflow in sequence.",
|
|
)
|
|
parser.add_argument(
|
|
"--region",
|
|
choices=[region.name for region in DEFAULT_REGIONS] + ["all"],
|
|
default="all",
|
|
help="Region selector forwarded to the import scripts (default: all).",
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default=Path("data"),
|
|
help="Directory where intermediate JSON payloads are stored (default: data/).",
|
|
)
|
|
parser.add_argument(
|
|
"--stations-json",
|
|
type=Path,
|
|
help="Existing station JSON file to load; defaults to <output-dir>/osm_stations.json.",
|
|
)
|
|
parser.add_argument(
|
|
"--tracks-json",
|
|
type=Path,
|
|
help="Existing track JSON file to load; defaults to <output-dir>/osm_tracks.json.",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-station-import",
|
|
action="store_true",
|
|
help="Skip the station import step (expects --stations-json to point to data).",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-station-load",
|
|
action="store_true",
|
|
help="Skip loading stations into PostGIS.",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-track-import",
|
|
action="store_true",
|
|
help="Skip the track import step (expects --tracks-json to point to data).",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-track-load",
|
|
action="store_true",
|
|
help="Skip loading tracks into PostGIS.",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print the planned stages without invoking Overpass or mutating the database.",
|
|
)
|
|
parser.add_argument(
|
|
"--commit",
|
|
dest="commit",
|
|
action="store_true",
|
|
default=True,
|
|
help="Commit database changes produced by the load steps (default).",
|
|
)
|
|
parser.add_argument(
|
|
"--no-commit",
|
|
dest="commit",
|
|
action="store_false",
|
|
help="Rollback database changes after load steps (dry run).",
|
|
)
|
|
return parser
|
|
|
|
|
|
def _build_stage_plan(args: argparse.Namespace) -> list[Stage]:
|
|
station_json = args.stations_json or args.output_dir / "osm_stations.json"
|
|
track_json = args.tracks_json or args.output_dir / "osm_tracks.json"
|
|
|
|
stages: list[Stage] = []
|
|
|
|
if not args.skip_station_import:
|
|
stages.append(
|
|
Stage(
|
|
label="Import stations",
|
|
runner=stations_import.main,
|
|
args=["--output", str(station_json), "--region", args.region],
|
|
output_path=station_json,
|
|
)
|
|
)
|
|
|
|
if not args.skip_station_load:
|
|
load_args = [str(station_json)]
|
|
if not args.commit:
|
|
load_args.append("--no-commit")
|
|
stages.append(
|
|
Stage(
|
|
label="Load stations",
|
|
runner=stations_load.main,
|
|
args=load_args,
|
|
input_path=station_json,
|
|
)
|
|
)
|
|
|
|
if not args.skip_track_import:
|
|
stages.append(
|
|
Stage(
|
|
label="Import tracks",
|
|
runner=tracks_import.main,
|
|
args=["--output", str(track_json), "--region", args.region],
|
|
output_path=track_json,
|
|
)
|
|
)
|
|
|
|
if not args.skip_track_load:
|
|
load_args = [str(track_json)]
|
|
if not args.commit:
|
|
load_args.append("--no-commit")
|
|
stages.append(
|
|
Stage(
|
|
label="Load tracks",
|
|
runner=tracks_load.main,
|
|
args=load_args,
|
|
input_path=track_json,
|
|
)
|
|
)
|
|
|
|
return stages
|
|
|
|
|
|
def _describe_plan(stages: Sequence[Stage]) -> None:
|
|
if not stages:
|
|
print("No stages selected; nothing to do.")
|
|
return
|
|
|
|
print("Selected stages:")
|
|
for stage in stages:
|
|
detail = " ".join(stage.args) if stage.args else "<no args>"
|
|
print(f" - {stage.label}: {detail}")
|
|
|
|
|
|
def _execute_stage(stage: Stage) -> None:
|
|
print(f"\n>>> {stage.label}")
|
|
|
|
if stage.output_path is not None:
|
|
stage.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if stage.input_path is not None and not stage.input_path.exists():
|
|
raise RuntimeError(
|
|
f"Expected input file {stage.input_path} for {stage.label}; run the import step first or provide an existing file."
|
|
)
|
|
|
|
try:
|
|
exit_code = stage.runner(stage.args)
|
|
except SystemExit as exc: # argparse.error exits via SystemExit
|
|
exit_code = int(exc.code or 0)
|
|
|
|
if exit_code:
|
|
raise RuntimeError(f"{stage.label} failed with exit code {exit_code}.")
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = build_argument_parser()
|
|
args = parser.parse_args(argv)
|
|
|
|
stages = _build_stage_plan(args)
|
|
|
|
if args.dry_run:
|
|
print("Dry run: the following stages would run in order.")
|
|
_describe_plan(stages)
|
|
return 0
|
|
|
|
for stage in stages:
|
|
_execute_stage(stage)
|
|
|
|
print("\nOSM refresh pipeline completed successfully.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|