# Streamline Gateway

A FastAPI-based reverse proxy for hosting multiple Streamlit applications in a single Docker container.

## Features

- **Auto-discovery**: Automatically discovers Streamlit apps from the `/apps` directory
- **Dynamic routing**: Each app gets its own URL path (`/app1/`, `/app2/`, etc.)
- **Central navigation**: Landing page with links to all available apps
- **WebSocket support**: Full support for Streamlit's real-time features
- **Type-safe**: Fully typed Python code using modern type hints
- **Modular dependencies**: Each app defines its own dependencies using `pyproject.toml`

## Project Structure

```
.
├── proxy/                    # FastAPI reverse proxy
│   ├── __init__.py
│   ├── main.py              # Main FastAPI application
│   ├── app_discovery.py     # App discovery logic
│   └── pyproject.toml       # Proxy dependencies
├── apps/                    # Streamlit applications
│   ├── app1/
│   │   ├── app.py           # App 1 code
│   │   └── pyproject.toml   # App 1 dependencies & metadata
│   └── app2/
│       ├── app.py           # App 2 code
│       └── pyproject.toml   # App 2 dependencies & metadata
├── scripts/
│   └── start.py             # Startup script for launching all services
├── pyproject.toml           # Workspace configuration
└── Dockerfile               # Container definition
```

## Getting Started

### Build the Docker Image

```bash
docker build -t streamline-gateway .
```

### Run the Container

```bash
docker run -p 8000:8000 streamline-gateway
```

### Access the Gateway

Open your browser and navigate to:
- **Navigation page**: http://localhost:8000
- **App 1**: http://localhost:8000/app1/
- **App 2**: http://localhost:8000/app2/

## Adding New Apps

The gateway supports both **Streamlit** and **FastAPI/uvicorn** applications.

### Adding a Streamlit App

1. Create a new directory under `apps/`:
   ```bash
   mkdir apps/my-new-app
   ```

2. Create `app.py` with your Streamlit code:
   ```python
   import streamlit as st

   st.title("My New App")
   st.write("Hello, world!")
   ```

3. Create `pyproject.toml` with dependencies and metadata:
   ```toml
   [project]
   name = "my-new-app"
   version = "0.1.0"
   description = "Description of my app"
   requires-python = ">=3.11"
   dependencies = [
       "streamlit>=1.31.0",
   ]

   [tool.streamline]
   type = "streamlit"
   display_name = "My New App"
   description = "A brief description for the navigation page"
   emoji = "✨"

   [tool.hatch.build.targets.wheel]
   packages = ["."]

   [build-system]
   requires = ["hatchling"]
   build-backend = "hatchling.build"
   ```

4. **For Docker**: Rebuild the Docker image and run:
   ```bash
   docker build -t streamline-gateway .
   docker run -p 8000:8000 streamline-gateway
   ```

5. **For local development**: Install the new app dependencies and restart:
   ```bash
   uv sync
   uv run python scripts/start.py
   ```

Your new app will automatically appear in the navigation page! No need to modify the Dockerfile or any other files.

### Adding a FastAPI/Uvicorn App

1. Create a new directory under `apps/`:
   ```bash
   mkdir apps/my-api
   ```

2. Create `main.py` (or any other module name) with your FastAPI code:
   ```python
   from fastapi import FastAPI

   app = FastAPI(title="My API")

   @app.get("/")
   async def root():
       return {"message": "Hello from My API!"}

   @app.get("/health")
   async def health():
       return {"status": "healthy"}
   ```

3. Create `pyproject.toml` with dependencies and metadata:
   ```toml
   [project]
   name = "my-api"
   version = "0.1.0"
   description = "My FastAPI application"
   requires-python = ">=3.11"
   dependencies = [
       "fastapi>=0.109.0",
   ]

   [tool.streamline]
   type = "uvicorn"
   module = "main:app"  # Format: "module_name:app_object"
   display_name = "My API"
   description = "A RESTful API service"
   emoji = "⚡"

   [tool.hatch.build.targets.wheel]
   packages = ["."]

   [build-system]
   requires = ["hatchling"]
   build-backend = "hatchling.build"
   ```

   **Important:** The `module` field specifies the module and app object in the format `module:app`. For example:
   - `main:app` - imports `app` from `main.py`
   - `server:application` - imports `application` from `server.py`

4. **For Docker**: Rebuild the Docker image and run:
   ```bash
   docker build -t streamline-gateway .
   docker run -p 8000:8000 streamline-gateway
   ```

5. **For local development**: Install the new app dependencies and restart:
   ```bash
   uv sync
   uv run python scripts/start.py
   ```

Your API will be accessible at `http://localhost:8000/my-api/` with all FastAPI features including automatic OpenAPI docs at `http://localhost:8000/my-api/docs`.

## Configuration

### App Metadata

Each app's `pyproject.toml` must include a `[tool.streamline]` section with the following fields:

**Required fields:**
- `type`: App type - either `"streamlit"` or `"uvicorn"` (required)
- `module`: For uvicorn apps only - module path like `"main:app"` (required for uvicorn)

**Optional fields:**
- `display_name`: Name shown in the navigation page (defaults to project name)
- `description`: Brief description shown in the navigation page (defaults to project description)
- `emoji`: Emoji icon for the app (defaults to 🔹)

### Managing App Dependencies

This project uses **uv workspaces** to manage dependencies across multiple apps. Each Streamlit app declares its dependencies in its own `pyproject.toml` file, and uv resolves all dependencies together into a unified virtual environment.

#### How UV Workspace Works

The root `pyproject.toml` defines the workspace:
```toml
[tool.uv.workspace]
members = ["proxy", "apps/*"]
```

When you run `uv sync`, it:
1. Discovers all workspace members (proxy + all apps in `apps/`)
2. Collects dependencies from all `pyproject.toml` files
3. Resolves all dependencies together to find compatible versions
4. Installs everything into a single shared virtual environment

**Benefits:**
- ✅ Automatic dependency resolution across all apps
- ✅ Conflicts detected at build time (not runtime)
- ✅ Guaranteed compatibility between all components
- ✅ Single command (`uv sync`) installs everything

#### Adding Dependencies to an App

To add a new package to a specific app, add it to the `dependencies` array in that app's `pyproject.toml`:

```toml
[project]
name = "my-data-app"
version = "0.1.0"
dependencies = [
    "streamlit>=1.31.0",
    "pandas>=2.0.0",           # Add data analysis library
    "plotly>=5.18.0",          # Add interactive plotting
    "scikit-learn>=1.3.0",     # Add machine learning
]
```

**For local development:**
```bash
# After modifying pyproject.toml, sync dependencies
uv sync

# Restart the gateway
uv run python scripts/start.py
```

**For Docker:**
```bash
# Rebuild the image to install new dependencies
docker build -t streamline-gateway .
docker run -p 8000:8000 streamline-gateway
```

#### Example: Data Analysis App with Multiple Dependencies

```toml
[project]
name = "analytics-dashboard"
version = "0.1.0"
description = "Real-time analytics dashboard"
requires-python = ">=3.11"
dependencies = [
    "streamlit>=1.31.0",
    "pandas>=2.0.0",
    "numpy>=1.26.0",
    "plotly>=5.18.0",
    "scikit-learn>=1.3.0",
    "sqlalchemy>=2.0.0",
    "psycopg2-binary>=2.9.0",
]

[tool.streamline]
display_name = "Analytics Dashboard"
description = "Real-time data analytics and visualization"
emoji = "📊"

[tool.hatch.build.targets.wheel]
packages = ["."]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

#### Dependency Resolution

All apps share the same virtual environment with unified dependency resolution:

**What this means:**
- ✅ Each app declares its dependencies independently in its own `pyproject.toml`
- ✅ UV resolves all dependencies together to ensure compatibility
- ✅ All apps use the same version of shared libraries (e.g., pandas, numpy)
- ⚠️ If two apps require incompatible versions, `uv sync` will fail with a clear error
- ✅ Conflicts are caught at build time, preventing runtime issues

**Example scenario:**
- App1 requires `pandas>=2.0.0`
- App2 requires `pandas>=1.5.0,<2.0.0`
- Result: `uv sync` will fail, prompting you to resolve the conflict before deployment

This is a **feature, not a limitation** - it ensures your entire gateway is consistent and reliable.

#### Best Practices

1. **Use flexible version constraints**: Allow uv to resolve compatible versions
   ```toml
   dependencies = [
       "pandas>=2.0.0",        # Good: flexible, allows uv to find compatible version
       "numpy>=1.26.0",        # Good: minimum version specified
   ]
   ```

2. **Avoid overly restrictive pinning**: Let workspace resolution do its job
   ```toml
   # ❌ Too restrictive - may conflict with other apps
   dependencies = ["pandas==2.1.0"]

   # ✅ Better - allows flexibility
   dependencies = ["pandas>=2.1.0"]
   ```

3. **Test dependency resolution early**:
   ```bash
   # Add your dependencies to pyproject.toml, then test
   uv sync  # Will fail immediately if there are conflicts

   # If successful, test the app
   uv run python scripts/start.py
   ```

4. **Handle conflicts proactively**: If `uv sync` fails:
   - Check the error message for conflicting packages
   - Adjust version constraints in affected apps
   - Consider if all apps truly need different versions
   - Update to compatible versions across apps

5. **Keep dependencies minimal**: Only include packages you actually use in each app

6. **Document special requirements**: If an app needs system dependencies (e.g., PostgreSQL client libraries), document them in the app's code comments

### Port Assignment

Apps are automatically assigned ports starting from 8501, in alphabetical order by directory name.

## Development

### Local Development (without Docker)

1. Install uv package manager (if not already installed):
   ```bash
   pip install uv
   ```

2. Install all dependencies:
   ```bash
   uv sync
   ```

3. Run the startup script:
   ```bash
   uv run python scripts/start.py
   ```

   Or use the Makefile:
   ```bash
   make dev
   ```

4. The gateway will be available at http://localhost:8000

**Note:** Apps hot-reload automatically when you modify their code during local development!

### Makefile Commands

The project includes a Makefile for common tasks:

```bash
make help      # Show all available commands
make install   # Install all dependencies
make fmt       # Format and lint code with ruff
make check     # Check code formatting
make build     # Build Docker image
make run       # Run Docker container
make dev       # Run locally for development
make clean     # Clean up cache files
```

### Health Check

The proxy provides a health check endpoint:

```bash
curl http://localhost:8000/health
```

## Technical Details

- **Python**: 3.11+
- **Package Manager**: uv (fast, modern Python package manager) with workspace support
- **Dependency Management**: UV workspaces for unified dependency resolution
- **Proxy**: FastAPI with httpx for HTTP and websockets for WebSocket support
- **Apps**: Streamlit 1.31+
- **Container**: Python 3.11-slim base image
- **Code Quality**: Ruff for formatting and linting

## License

MIT
