Skip to content

REST API Tutorial

PyWMP includes an optional FastAPI server that lets you submit simulations remotely, stream progress in real time, and retrieve results — all over HTTP and WebSocket. This enables integration into web applications, cloud workflows, and CI pipelines.

Installation

pip install "pywmp[api]"

Starting the server

uvicorn pywmp.api.app:app --host 0.0.0.0 --port 8000

For development with auto-reload:

uvicorn pywmp.api.app:app --reload

With Docker:

docker compose up

The server is available at http://localhost:8000. Interactive API docs are at http://localhost:8000/docs.


Endpoints

Method Path Description
GET /health Server health check
POST /simulate/hms Submit an HMS simulation
POST /simulate/cascade Submit a Cascade 2001 simulation
GET /results/{run_id} Poll for results
GET /runs List all in-memory runs
POST /upload/dat Upload a .dat file
WS /ws/{run_id} Stream progress events

POST /simulate/hms — Submit an HMS simulation

Accepts a JSON body describing the basin model and returns a run_id immediately. The simulation runs in a background thread.

Request body

{
  "basin_name": "Hillsborough",
  "units": "USC",
  "subbasins": [
    {
      "name": "Upper",
      "area_mi2": 2.5,
      "loss_method": "scs",
      "loss_params": {
        "curve_number": 78,
        "initial_abstraction": 0.0,
        "amc": 2
      },
      "transform_method": "clark",
      "transform_params": {
        "time_of_concentration_hr": 1.8,
        "storage_coefficient_hr": 1.4
      }
    },
    {
      "name": "Lower",
      "area_mi2": 1.8,
      "loss_method": "scs",
      "loss_params": {"curve_number": 68},
      "transform_method": "scs",
      "transform_params": {"lag_hr": 1.1}
    }
  ],
  "reaches": [
    {
      "name": "R1",
      "routing_method": "muskingum",
      "routing_params": {"K_hr": 0.8, "x": 0.2, "steps": 3}
    }
  ],
  "junctions": [],
  "connections": [
    {"upstream": "Upper",  "downstream": "R1"},
    {"upstream": "R1",     "downstream": "Lower"}
  ],
  "rainfall": {
    "Upper": {
      "times":  [0.0, 6.0, 10.0, 12.0, 18.0, 24.0],
      "values": [0.0, 0.3,  0.9,  1.2,  0.6,  0.0],
      "dt": 0.1,
      "units": "in/hr"
    },
    "Lower": {
      "times":  [0.0, 6.0, 10.0, 12.0, 18.0, 24.0],
      "values": [0.0, 0.3,  0.9,  1.2,  0.6,  0.0],
      "dt": 0.1,
      "units": "in/hr"
    }
  }
}

Field reference

Field Type Description
basin_name string Label for the simulation
units "USC" | "SI" Unit system (default "USC")
subbasins array List of subbasin specs (see below)
reaches array List of reach specs
junctions array List of junction names [{"name": "J1"}, ...]
connections array Upstream/downstream pairs
rainfall object Time-series per subbasin name

Subbasin spec:

Field Type Options
loss_method string "scs", "green_ampt", "initial_constant", "sma"
transform_method string "scs", "clark", "snyder", "user"

Response

{"run_id": "a3f2c1b0-4e5d-4a6f-b7c8-9d0e1f2a3b4c"}

POST /simulate/cascade — Submit a Cascade 2001 simulation

{
  "dat_path": "/data/my_project.dat"
}

The .dat file must be accessible on the server filesystem. Use POST /upload/dat to upload one first. Returns {"run_id": "..."}.


GET /results/{run_id} — Poll for results

curl http://localhost:8000/results/a3f2c1b0-4e5d-4a6f-b7c8-9d0e1f2a3b4c

While running:

{"run_id": "a3f2...", "status": "running", "result": null}

When complete:

{
  "run_id": "a3f2...",
  "status": "done",
  "outflows": {
    "Upper": {
      "times": [0.0, 0.1, 0.2, ...],
      "values": [0.0, 0.0, 0.3, ...],
      "dt": 0.1,
      "units": "cfs"
    },
    "Lower": {
      "times": [0.0, 0.1, 0.2, ...],
      "values": [0.0, 0.0, 0.1, ...],
      "dt": 0.1,
      "units": "cfs"
    }
  }
}

On error:

{"run_id": "a3f2...", "status": "error", "error": "ValueError: negative C0 ..."}


GET /runs — List all runs

curl http://localhost:8000/runs
[
  {"run_id": "a3f2...", "status": "done",    "created_at": 1720000100},
  {"run_id": "b9e1...", "status": "running", "created_at": 1720000142},
  {"run_id": "c4d3...", "status": "error",   "created_at": 1720000088}
]

POST /upload/dat — Upload a Cascade .dat file

curl -X POST http://localhost:8000/upload/dat \
  -F "file=@my_project.dat"

Response:

{"server_path": "/data/my_project.dat"}

Use the returned server_path as the dat_path in a /simulate/cascade request.


WS /ws/{run_id} — Real-time progress stream

Connect to the WebSocket after submitting a run to receive live progress events.

Event types:

event Fields Description
"progress" pct, element Percentage complete + element being processed
"done" pct=100 Simulation finished
"error" msg Error message

Example messages:

{"event": "progress", "run_id": "a3f2...", "pct": 15.0, "element": "Upper"}
{"event": "progress", "run_id": "a3f2...", "pct": 52.5, "element": "R1"}
{"event": "progress", "run_id": "a3f2...", "pct": 87.0, "element": "Lower"}
{"event": "done",     "run_id": "a3f2...", "pct": 100.0}

Python client example

Full submit-and-poll client with WebSocket progress tracking:

import asyncio
import json
import requests
import websockets

BASE = "http://localhost:8000"

payload = {
    "basin_name": "Demo",
    "units": "USC",
    "subbasins": [
        {
            "name": "A",
            "area_mi2": 1.5,
            "loss_method": "scs",
            "loss_params": {"curve_number": 75},
            "transform_method": "scs",
            "transform_params": {"lag_hr": 0.9},
        }
    ],
    "reaches": [],
    "junctions": [],
    "connections": [],
    "rainfall": {
        "A": {
            "times": [0.0, 12.0, 24.0],
            "values": [0.0, 0.8, 0.0],
            "dt": 0.1,
            "units": "in/hr",
        }
    },
}

# Submit
resp = requests.post(f"{BASE}/simulate/hms", json=payload)
resp.raise_for_status()
run_id = resp.json()["run_id"]
print(f"Submitted: {run_id}")


# Stream progress via WebSocket
async def stream_progress(run_id: str):
    uri = f"ws://localhost:8000/ws/{run_id}"
    async with websockets.connect(uri) as ws:
        async for raw in ws:
            msg = json.loads(raw)
            event = msg.get("event")
            if event == "progress":
                print(f"  {msg['pct']:5.1f}%  {msg.get('element', '')}")
            elif event == "done":
                print("  Done!")
                break
            elif event == "error":
                print(f"  Error: {msg['msg']}")
                break

asyncio.run(stream_progress(run_id))

# Retrieve results
result = requests.get(f"{BASE}/results/{run_id}").json()
for name, ts in result["outflows"].items():
    peak = max(ts["values"])
    print(f"{name}: peak = {peak:.1f} cfs")

Async client example

For production integrations, use httpx with an async HTTP client:

import asyncio
import httpx
import websockets
import json

async def run_simulation(payload: dict) -> dict:
    async with httpx.AsyncClient(base_url="http://localhost:8000") as client:
        r = await client.post("/simulate/hms", json=payload, timeout=10)
        r.raise_for_status()
        run_id = r.json()["run_id"]

        # wait for completion via WebSocket
        async with websockets.connect(f"ws://localhost:8000/ws/{run_id}") as ws:
            async for raw in ws:
                msg = json.loads(raw)
                if msg["event"] in ("done", "error"):
                    break

        r2 = await client.get(f"/results/{run_id}")
        return r2.json()

result = asyncio.run(run_simulation(payload))

Docker deployment

A Dockerfile and docker-compose.yml are included in the repo root.

# Start the API server in a container
docker compose up

# The API is available at http://localhost:8000
# Mount a local data directory for .dat files and GIS outputs:
#   volumes:
#     - ./data:/data

The container sets PYWMP_DATA_DIR=/data so uploaded .dat files and GIS outputs are persisted to the mounted volume.

Production deployment notes: - Add a reverse proxy (nginx, Caddy) in front of uvicorn for TLS and rate limiting. - Scale workers with --workers N for multi-process concurrency: uvicorn pywmp.api.app:app --workers 4. - The in-memory run store is per-process — use a Redis backend for multi-worker setups.


Error handling

HTTP status Cause Action
422 Invalid request body (Pydantic validation) Check field names and types against the schema above
404 run_id not found Verify the run_id; runs are in-memory and lost on server restart
500 Simulation error Check /results/{run_id} for the error field

Timeout note: /simulate/hms returns immediately with a run_id. Long simulations should use the WebSocket stream rather than polling /results/{run_id} on a tight loop.