Spaces:
Running
Running
| """Unified CLI for Court Scheduling System. | |
| This module provides a single entry point for key court scheduling operations: | |
| - EDA pipeline execution | |
| - Case generation | |
| - Simulation runs | |
| - Full workflow orchestration | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| import typer | |
| from rich.console import Console | |
| from rich.progress import Progress, SpinnerColumn, TextColumn | |
| from cli import __version__ | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| sys.stderr.reconfigure(encoding="utf-8") | |
| except Exception: | |
| pass | |
| # Initialize Typer app and console | |
| app = typer.Typer( | |
| name="court-scheduler", | |
| help="Court Scheduling System for Karnataka High Court", | |
| add_completion=False, | |
| ) | |
| # Use force_terminal=False to avoid legacy Windows rendering issues with Unicode | |
| console = Console(legacy_windows=False) | |
| def eda( | |
| skip_clean: bool = typer.Option( | |
| False, "--skip-clean", help="Skip data loading and cleaning" | |
| ), | |
| skip_viz: bool = typer.Option( | |
| False, "--skip-viz", help="Skip visualization generation" | |
| ), | |
| skip_params: bool = typer.Option( | |
| False, "--skip-params", help="Skip parameter extraction" | |
| ), | |
| ) -> None: | |
| """Run the EDA pipeline (load, explore, extract parameters).""" | |
| console.print("[bold blue]Running EDA Pipeline[/bold blue]") | |
| try: | |
| # Import here to avoid loading heavy dependencies if not needed | |
| from eda.exploration import run_exploration | |
| from eda.load_clean import run_load_and_clean | |
| from eda.parameters import run_parameter_export | |
| with Progress( | |
| TextColumn("[progress.description]{task.description}"), | |
| console=console, | |
| ) as progress: | |
| if not skip_clean: | |
| task = progress.add_task("Step 1/3: Load and clean data...", total=None) | |
| run_load_and_clean() | |
| progress.update(task, completed=True) | |
| console.print("Data loaded and cleaned") | |
| if not skip_viz: | |
| task = progress.add_task( | |
| "Step 2/3: Generate visualizations...", total=None | |
| ) | |
| run_exploration() | |
| progress.update(task, completed=True) | |
| console.print("Visualizations generated") | |
| if not skip_params: | |
| task = progress.add_task("Step 3/3: Extract parameters...", total=None) | |
| run_parameter_export() | |
| progress.update(task, completed=True) | |
| console.print("Parameters extracted") | |
| console.print("\n[bold]EDA Pipeline Complete[/bold]") | |
| console.print("Outputs: reports/figures/") | |
| except Exception as e: | |
| console.print(f"[bold red]Error:[/bold red] {e}") | |
| raise typer.Exit(code=1) | |
| def generate( | |
| config: Path = typer.Option( # noqa: B008 | |
| None, | |
| "--config", | |
| exists=True, | |
| dir_okay=False, | |
| readable=True, | |
| help="Path to config (.toml or .json)", | |
| ), | |
| interactive: bool = typer.Option( | |
| False, "--interactive", help="Prompt for parameters interactively" | |
| ), | |
| n_cases: int = typer.Option( | |
| 10000, "--cases", "-n", help="Number of cases to generate" | |
| ), | |
| start_date: str = typer.Option( | |
| "2022-01-01", "--start", help="Start date (YYYY-MM-DD)" | |
| ), | |
| end_date: str = typer.Option("2023-12-31", "--end", help="End date (YYYY-MM-DD)"), | |
| output: str = typer.Option( | |
| "data/generated/cases.csv", "--output", "-o", help="Output CSV file" | |
| ), | |
| seed: int = typer.Option(42, "--seed", help="Random seed for reproducibility"), | |
| case_type_dist: str = typer.Option( | |
| None, | |
| "--case-type-dist", | |
| help=( | |
| 'Custom case type distribution. Accepts JSON (e.g., \'{"Writ":0.6,"Civil":0.4}\') ' | |
| "or comma-separated pairs 'Writ:0.6,Civil:0.4'. Defaults to historical distribution." | |
| ), | |
| ), | |
| ) -> None: | |
| """Generate synthetic test cases for simulation.""" | |
| console.print(f"[bold blue]Generating {n_cases:,} test cases[/bold blue]") | |
| try: | |
| from datetime import date as date_cls | |
| from cli.config import GenerateConfig, load_generate_config | |
| from src.data.case_generator import CaseGenerator | |
| # Resolve parameters: config -> interactive -> flags | |
| if config: | |
| cfg = load_generate_config(config) | |
| else: | |
| if interactive: | |
| n_cases = typer.prompt("Number of cases", default=n_cases) | |
| start_date = typer.prompt("Start date (YYYY-MM-DD)", default=start_date) | |
| end_date = typer.prompt("End date (YYYY-MM-DD)", default=end_date) | |
| output = typer.prompt("Output CSV path", default=output) | |
| seed = typer.prompt("Random seed", default=seed) | |
| cfg = GenerateConfig( | |
| n_cases=n_cases, | |
| start=date_cls.fromisoformat(start_date), | |
| end=date_cls.fromisoformat(end_date), | |
| output=Path(output), | |
| seed=seed, | |
| ) | |
| start = cfg.start | |
| end = cfg.end | |
| output_path = cfg.output | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| with Progress( | |
| TextColumn("[progress.description]{task.description}"), | |
| console=console, | |
| ) as progress: | |
| task = progress.add_task("Generating cases...", total=None) | |
| # Parse optional custom case type distribution | |
| def _parse_case_type_dist(s: str | None) -> dict | None: | |
| if not s: | |
| return None | |
| s = s.strip() | |
| try: | |
| import json | |
| obj = json.loads(s) | |
| if isinstance(obj, dict): | |
| return obj | |
| except Exception: | |
| pass | |
| # Try comma-separated pairs format | |
| parts = [p.strip() for p in s.split(",") if p.strip()] | |
| dist: dict[str, float] = {} | |
| for part in parts: | |
| if ":" not in part: | |
| continue | |
| k, v = part.split(":", 1) | |
| k = k.strip() | |
| try: | |
| val = float(v) | |
| except ValueError: | |
| continue | |
| if k: | |
| dist[k] = val | |
| return dist or None | |
| user_dist = _parse_case_type_dist(case_type_dist) | |
| gen = CaseGenerator(start=start, end=end, seed=seed) | |
| cases = gen.generate( | |
| n_cases, stage_mix_auto=True, case_type_distribution=user_dist | |
| ) | |
| # Write primary cases file | |
| CaseGenerator.to_csv(cases, output_path) | |
| # Also write detailed hearings history alongside, for the dashboard/classifier | |
| hearings_path = output_path.parent / "hearings.csv" | |
| CaseGenerator.to_hearings_csv(cases, hearings_path) | |
| progress.update(task, completed=True) | |
| console.print(f"Generated {len(cases):,} cases") | |
| console.print(f"Saved to: {output_path}") | |
| console.print(f"Hearings history: {hearings_path}") | |
| except Exception as e: | |
| console.print(f"[bold red]Error:[/bold red] {e}") | |
| raise typer.Exit(code=1) | |
| def simulate( | |
| config: Path = typer.Option( | |
| None, | |
| "--config", | |
| exists=True, | |
| dir_okay=False, | |
| readable=True, | |
| help="Path to config (.toml or .json)", | |
| ), | |
| interactive: bool = typer.Option( | |
| False, "--interactive", help="Prompt for parameters interactively" | |
| ), | |
| cases_csv: str = typer.Option( | |
| "data/generated/cases.csv", "--cases", help="Input cases CSV" | |
| ), | |
| days: int = typer.Option( | |
| 384, "--days", "-d", help="Number of working days to simulate" | |
| ), | |
| start_date: str = typer.Option( | |
| None, "--start", help="Simulation start date (YYYY-MM-DD)" | |
| ), | |
| policy: str = typer.Option( | |
| "readiness", "--policy", "-p", help="Scheduling policy (fifo/age/readiness)" | |
| ), | |
| seed: int = typer.Option(42, "--seed", help="Random seed"), | |
| log_dir: str = typer.Option( | |
| None, "--log-dir", "-o", help="Output directory for logs" | |
| ), | |
| ) -> None: | |
| """Run court scheduling simulation.""" | |
| console.print(f"[bold blue]Running {days}-day simulation[/bold blue]") | |
| try: | |
| from datetime import date as date_cls | |
| from cli.config import SimulateConfig, load_simulate_config | |
| from src.core.case import CaseStatus | |
| from src.data.case_generator import CaseGenerator | |
| from src.metrics.basic import gini | |
| from src.simulation.engine import CourtSim, CourtSimConfig | |
| # Resolve parameters: config -> interactive -> flags | |
| if config: | |
| scfg = load_simulate_config(config) | |
| # CLI flags override config if provided | |
| scfg = scfg.model_copy( | |
| update={ | |
| "cases": Path(cases_csv) if cases_csv else scfg.cases, | |
| "days": days if days else scfg.days, | |
| "start": ( | |
| date_cls.fromisoformat(start_date) if start_date else scfg.start | |
| ), | |
| "policy": policy if policy else scfg.policy, | |
| "seed": seed if seed else scfg.seed, | |
| "log_dir": (Path(log_dir) if log_dir else scfg.log_dir), | |
| } | |
| ) | |
| else: | |
| if interactive: | |
| cases_csv = typer.prompt("Cases CSV", default=cases_csv) | |
| days = typer.prompt("Days to simulate", default=days) | |
| start_date = ( | |
| typer.prompt( | |
| "Start date (YYYY-MM-DD) or blank", default=start_date or "" | |
| ) | |
| or None | |
| ) | |
| policy = typer.prompt("Policy [readiness|fifo|age]", default=policy) | |
| seed = typer.prompt("Random seed", default=seed) | |
| log_dir = ( | |
| typer.prompt("Log dir (or blank)", default=log_dir or "") or None | |
| ) | |
| scfg = SimulateConfig( | |
| cases=Path(cases_csv), | |
| days=days, | |
| start=(date_cls.fromisoformat(start_date) if start_date else None), | |
| policy=policy, | |
| seed=seed, | |
| log_dir=(Path(log_dir) if log_dir else None), | |
| ) | |
| # Load cases | |
| path = scfg.cases | |
| if path.exists(): | |
| cases = CaseGenerator.from_csv(path) | |
| start = scfg.start or ( | |
| max(c.filed_date for c in cases) if cases else date_cls.today() | |
| ) | |
| else: | |
| console.print( | |
| f"[yellow]Warning:[/yellow] {path} not found. Generating test cases..." | |
| ) | |
| start = scfg.start or date_cls.today().replace(day=1) | |
| gen = CaseGenerator(start=start, end=start.replace(day=28), seed=scfg.seed) | |
| cases = gen.generate(n_cases=5 * 151) | |
| # Run simulation | |
| cfg = CourtSimConfig( | |
| start=start, | |
| days=scfg.days, | |
| seed=scfg.seed, | |
| policy=scfg.policy, | |
| duration_percentile="median", | |
| log_dir=scfg.log_dir, | |
| ) | |
| with Progress( | |
| SpinnerColumn(), | |
| TextColumn("[progress.description]{task.description}"), | |
| console=console, | |
| ) as progress: | |
| task = progress.add_task(f"Simulating {days} days...", total=None) | |
| sim = CourtSim(cfg, cases) | |
| res = sim.run() | |
| progress.update(task, completed=True) | |
| # Display results | |
| console.print("\n[bold green]Simulation Complete![/bold green]") | |
| console.print(f"\nHorizon: {cfg.start} -> {res.end_date} ({days} days)") | |
| console.print("\n[bold]Hearing Metrics:[/bold]") | |
| console.print(f" Total: {res.hearings_total:,}") | |
| console.print( | |
| f" Heard: {res.hearings_heard:,} ({res.hearings_heard / max(1, res.hearings_total):.1%})" | |
| ) | |
| console.print( | |
| f" Adjourned: {res.hearings_adjourned:,} ({res.hearings_adjourned / max(1, res.hearings_total):.1%})" | |
| ) | |
| disp_times = [ | |
| (c.disposal_date - c.filed_date).days | |
| for c in cases | |
| if c.disposal_date is not None and c.status == CaseStatus.DISPOSED | |
| ] | |
| gini_disp = gini(disp_times) if disp_times else 0.0 | |
| console.print("\n[bold]Disposal Metrics:[/bold]") | |
| console.print( | |
| f" Cases disposed: {res.disposals:,} ({res.disposals / len(cases):.1%})" | |
| ) | |
| console.print(f" Gini coefficient: {gini_disp:.3f}") | |
| console.print("\n[bold]Efficiency:[/bold]") | |
| console.print(f" Utilization: {res.utilization:.1%}") | |
| console.print(f" Avg hearings/day: {res.hearings_total / days:.1f}") | |
| if log_dir: | |
| console.print("\n[bold cyan]Output Files:[/bold cyan]") | |
| console.print(f" - {log_dir}/report.txt") | |
| console.print(f" - {log_dir}/metrics.csv") | |
| console.print(f" - {log_dir}/events.csv") | |
| except Exception as e: | |
| console.print(f"[bold red]Error:[/bold red] {e}") | |
| raise typer.Exit(code=1) | |
| def workflow( | |
| n_cases: int = typer.Option( | |
| 10000, "--cases", "-n", help="Number of cases to generate" | |
| ), | |
| sim_days: int = typer.Option(384, "--days", "-d", help="Simulation days"), | |
| output_dir: str = typer.Option( | |
| "data/workflow_run", "--output", "-o", help="Output directory" | |
| ), | |
| seed: int = typer.Option(42, "--seed", help="Random seed"), | |
| ) -> None: | |
| """Run full workflow: EDA -> Generate -> Simulate -> Report.""" | |
| console.print("[bold blue]Running Full Workflow[/bold blue]\n") | |
| output_path = Path(output_dir) | |
| output_path.mkdir(parents=True, exist_ok=True) | |
| try: | |
| # Step 1: EDA (skip if already done recently) | |
| console.print("[bold]Step 1/3:[/bold] EDA Pipeline") | |
| console.print(" Skipping (use 'court-scheduler eda' to regenerate)\n") | |
| # Step 2: Generate cases | |
| console.print("[bold]Step 2/3:[/bold] Generate Cases") | |
| cases_file = output_path / "cases.csv" | |
| from datetime import date as date_cls | |
| from src.data.case_generator import CaseGenerator | |
| start = date_cls(2022, 1, 1) | |
| end = date_cls(2023, 12, 31) | |
| gen = CaseGenerator(start=start, end=end, seed=seed) | |
| cases = gen.generate(n_cases, stage_mix_auto=True) | |
| CaseGenerator.to_csv(cases, cases_file) | |
| console.print(f" Generated {len(cases):,} cases\n") | |
| # Step 3: Run simulation | |
| console.print("[bold]Step 3/3:[/bold] Run Simulation") | |
| from src.simulation.engine import CourtSim, CourtSimConfig | |
| sim_start = max(c.filed_date for c in cases) | |
| cfg = CourtSimConfig( | |
| start=sim_start, | |
| days=sim_days, | |
| seed=seed, | |
| policy="readiness", | |
| log_dir=output_path, | |
| ) | |
| sim = CourtSim(cfg, cases) | |
| sim.run() | |
| console.print(" Simulation complete\n") | |
| # Summary | |
| console.print("[bold]Workflow Complete[/bold]") | |
| console.print(f"\nResults: {output_path}/") | |
| console.print(f" - cases.csv ({len(cases):,} cases)") | |
| console.print(" - report.txt (simulation summary)") | |
| console.print(" - metrics.csv (daily metrics)") | |
| console.print(" - events.csv (event log)") | |
| except Exception as e: | |
| console.print(f"[bold red]Error:[/bold red] {e}") | |
| raise typer.Exit(code=1) | |
| def dashboard( | |
| port: int = typer.Option(8501, "--port", "-p", help="Port to run dashboard on"), | |
| host: str = typer.Option("localhost", "--host", help="Host address to bind to"), | |
| ) -> None: | |
| """Launch interactive dashboard.""" | |
| console.print("[bold blue]Launching Interactive Dashboard[/bold blue]") | |
| console.print(f"Dashboard will be available at: http://{host}:{port}") | |
| console.print("Press Ctrl+C to stop the dashboard\n") | |
| try: | |
| import subprocess | |
| import sys | |
| # Get path to dashboard app | |
| app_path = Path(__file__).parent.parent / "scheduler" / "dashboard" / "app.py" | |
| if not app_path.exists(): | |
| console.print( | |
| f"[bold red]Error:[/bold red] Dashboard app not found at {app_path}" | |
| ) | |
| raise typer.Exit(code=1) | |
| # Run streamlit | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "streamlit", | |
| "run", | |
| str(app_path), | |
| "--server.port", | |
| str(port), | |
| "--server.address", | |
| host, | |
| "--browser.gatherUsageStats", | |
| "false", | |
| ] | |
| subprocess.run(cmd) | |
| except KeyboardInterrupt: | |
| console.print("\n[yellow]Dashboard stopped[/yellow]") | |
| except Exception as e: | |
| console.print(f"[bold red]Error:[/bold red] {e}") | |
| raise typer.Exit(code=1) | |
| def version() -> None: | |
| """Show version information.""" | |
| console.print(f"Court Scheduler CLI v{__version__}") | |
| console.print("Court Scheduling System for Karnataka High Court") | |
| def main() -> None: | |
| """Entry point for CLI.""" | |
| app() | |
| if __name__ == "__main__": | |
| main() | |