"""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. runtime: technical runtime used to launch the app ("streamlit" or "uvicorn"). semantic_type: semantic classification ("demo" or "example"). """ name: str display_name: str description: str emoji: str port: int runtime: Literal["streamlit", "uvicorn"] module: str # For uvicorn apps, e.g., "main:app". Empty for streamlit. semantic_type: str | None 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 = 8601 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", {}) # Backward compatibility: # Old configs used 'type' for runtime. New configs should use 'runtime'. # If both present, 'runtime' wins and 'type' can be treated as semantic_type. raw_type = app_config.get("type") raw_runtime = app_config.get("runtime") # Determine runtime and semantic_type. runtime = raw_runtime or raw_type if runtime in ("streamlit", "uvicorn"): # semantic_type is meaningful only if an explicit non-runtime value provided if raw_runtime and raw_type and raw_type not in ("streamlit", "uvicorn"): semantic_type = raw_type elif raw_runtime and not raw_type: semantic_type = None elif not raw_runtime and raw_type and raw_type in ("streamlit", "uvicorn"): # legacy: only runtime stored in type semantic_type = None else: semantic_type = None else: # Invalid runtime — will be caught below, but keep semantic_type None semantic_type = None if not runtime: raise ValueError( "Missing required field 'runtime' (or legacy 'type') in [tool.streamline]. " "Expected one of: 'streamlit', 'uvicorn'" ) if runtime not in ("streamlit", "uvicorn"): raise ValueError(f"Invalid runtime '{runtime}'. Must be 'streamlit' or 'uvicorn'") # For uvicorn apps, module is required module = app_config.get("module", "") if runtime == "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, runtime=runtime, module=module, semantic_type=semantic_type, ) port += 1 except Exception as e: print(f"Warning: Failed to load app {app_path.name}: {e}") continue return apps