"""Auto-discovery of apps from the apps directory.""" from pathlib import Path from typing import Literal, TypedDict try: import tomllib # Python 3.11+ except ImportError: import tomli as tomllib # type: ignore class AppMetadata(TypedDict): """Metadata for a discovered app (Streamlit or FastAPI/uvicorn).""" name: str display_name: str description: str emoji: str port: int type: Literal["streamlit", "uvicorn"] module: str # For uvicorn apps, e.g., "main:app". Empty for streamlit. def discover_apps(apps_dir: Path = Path("/app/apps")) -> dict[str, AppMetadata]: """ Auto-discover apps (Streamlit or FastAPI/uvicorn) from the apps directory. Scans the apps directory for subdirectories containing pyproject.toml files. Extracts app metadata from the pyproject.toml [tool.streamline] section. Args: apps_dir: Path to the directory containing app subdirectories Returns: Dictionary mapping app names to their metadata, with auto-assigned ports Raises: ValueError: If app configuration is invalid (missing type, invalid type, etc.) """ apps: dict[str, AppMetadata] = {} port = 8501 if not apps_dir.exists(): return apps for app_path in sorted(apps_dir.iterdir()): if not app_path.is_dir(): continue pyproject_path = app_path / "pyproject.toml" if not pyproject_path.exists(): continue try: with open(pyproject_path, "rb") as f: data = tomllib.load(f) project = data.get("project", {}) app_config = data.get("tool", {}).get("streamline", {}) # Validate required fields app_type = app_config.get("type") if not app_type: raise ValueError( "Missing required field 'type' in [tool.streamline] section. " "Must be 'streamlit' or 'uvicorn'" ) if app_type not in ("streamlit", "uvicorn"): raise ValueError(f"Invalid type '{app_type}'. Must be 'streamlit' or 'uvicorn'") # For uvicorn apps, module is required module = app_config.get("module", "") if app_type == "uvicorn" and not module: raise ValueError( "Uvicorn apps require 'module' field in [tool.streamline] section. " 'Example: module = "main:app"' ) app_name = app_path.name apps[app_name] = AppMetadata( name=app_name, display_name=app_config.get("display_name", project.get("name", app_name.title())), description=app_config.get("description", project.get("description", "")), emoji=app_config.get("emoji", "🔹"), port=port, type=app_type, module=module, ) port += 1 except Exception as e: print(f"Warning: Failed to load app {app_path.name}: {e}") continue return apps