Metadata-Version: 2.5
Name: gravi-model-client
Version: 0.11.0
Summary: Python client for Gravitate's Demand & Supply optimization models
Project-URL: Homepage, https://gravitate.energy
Project-URL: Repository, https://github.com/gravitate-energy/demand-only-model
Author-email: Gravitate Energy <engineering@gravitate.energy>
License: Proprietary
Keywords: api-client,demand-model,optimization,supply-model
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.9.0
Requires-Dist: pyhumps>=3.8.0
Description-Content-Type: text/markdown

# gravi-model-client

Python client for Gravitate's demand and supply optimization models.

## Installation

```bash
pip install gravi-model-client
```

## Usage

```python
from gravi_model_client import GraviModelClient, ModelRunRequest, OptimizationInput

async with GraviModelClient(api_key="your-api-key", base_url="https://...") as client:
    # Health check
    await client.health_check()

    # Run demand model inline — blocks for the whole solve
    result = await client.run_demand_model(request)

    # Run TSD (supply) model inline — blocks for the whole solve
    result = await client.run_tsd_model(optimization)
```

`run_demand_model` and `run_tsd_model` hold the HTTP request open for the length of
the solve. They raise `ModelRequestInvalid` when the server rejects the body, with
one readable line per field error in `.issues` and the error body verbatim in
`.payload` — read `payload["detail"][i]["ctx"]` for the structured detail behind a
rejection, such as the store, tank, period and volume on an over-capacity delivery.

### Queue-based models

Every model can also be run through the job queue, where a worker picks the run up
and the client polls for it. The `run_*_model_async` methods handle the whole
queue → poll → fetch lifecycle and block until the result is ready:

```python
from gravi_model_client import GraviModelClient

async with GraviModelClient(api_key="your-api-key") as client:
    demand = await client.run_demand_model_async(demand_request)  # -> ModelResultResponse
    ltl = await client.run_ltl_model_async(ltl_request)  # -> LtlModelResultResponse
    vrp = await client.run_vrp_model_async(vrp_request)  # -> VrpModelResultResponse
    osa = await client.run_osa_model_async(osa_input)  # -> OsaAPIResponse
    tsd = await client.run_tsd_model_async(tsd_input)  # -> OptimizationAPIResponse
```

They raise `ModelRunError` if the run fails or is cancelled, and `ModelRunTimeout`
if it doesn't finish within `timeout` (default 300s, 900s for TSD; tune
`poll_interval` too).

For fire-and-forget / webhook workflows, use the primitives instead. Each model has
the same trio:

| Model  | Queue                | Status                  | Fetch result                     |
|--------|----------------------|-------------------------|----------------------------------|
| Demand | `queue_demand_model` | `get_demand_job_status` | `get_demand_model_run_response`  |
| LTL    | `queue_ltl_model`    | `get_ltl_job_status`    | `get_ltl_model_run_response`     |
| VRP    | `queue_vrp_model`    | `get_vrp_job_status`    | `get_vrp_model_run_response`     |
| OSA    | `queue_osa_model`    | `get_osa_job_status`    | `get_osa_model_run_response`     |
| TSD    | `queue_tsd_model`    | `get_tsd_job_status`    | `get_tsd_model_run_response`     |

The result fetch raises `ModelResultNotReady` while a run is still queued or
running, and `ModelRunError` once it has ended failed or cancelled — so a caller
polling the fetch directly can tell "come back later" from "this will never
succeed".

### Job administration

`get_job_details`, `get_ltl_job_details` and `get_tsd_job_details` return a
`JobDetails` with the job's status alongside the request and response stored on the
server — that is where to read back the input a finished run was given.
`cancel_demand_jobs`, `cancel_ltl_jobs`, `cancel_osa_jobs` and `cancel_tsd_jobs`
cancel every job under a `history_id`; `get_queue_stats`, `get_ltl_queue_stats` and
`get_tsd_queue_stats` report queue depth; `clear_files` and `clear_ltl_files` delete
the server's temporary request and response files.

### Retries

Every request retries transient failures — connection errors, timeouts, 429 and
5xx — with jittered exponential backoff:

```python
GraviModelClient(api_key, retries=3, retry_backoff=1.0, retry_backoff_max=20.0)
```

`retries=0` disables it. The queue endpoints are deliberately more conservative:
a failure that may have reached the server is *not* replayed, because that would
enqueue the same solve twice. Only a connection failure, which provably never
arrived, is retried there.

The result fetch additionally retries a 404. A server without the 202/409 readiness
codes answers 404 for the whole window between the worker writing the result file and
the API pod seeing it; anywhere else a 404 is taken at face value.

## Models

This package is the single source of truth for all pydantic models used by both
the client and the server. Import models directly:

```python
from gravi_model_client.demand import ModelRunRequest, StoreRequest, TankRequest, VolumeRange
from gravi_model_client.tsd import OptimizationInput, Directive, Site, Tank
```
