Metadata-Version: 2.4
Name: gridrunner
Version: 0.8.0
Summary: Producer SDK for GRIDRUNNER — fire-and-forget job progress events
License: MIT
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# GRIDRUNNER Python SDK

Standard-library-only client for reporting jobs, service pings, and
operator alerts to GRIDRUNNER. Calls never block or raise into your code.
The [SDK overview](../README.md) explains when to use a job, a ping, or an
alert.

## Setup

```python
import gridrunner as gr

gr.init(token=read_secret("gridrunner_produce_token"),
        url="https://gridrunner.example.com")
gr.init()   # local dev core at http://127.0.0.1:7077
```

A token without `url=` raises `ValueError`. The SDK reads no environment
variables or config files.

## Jobs

Declare the chunk plan, then open each chunk as you process it. A clean
exit from the block completes it; an exception fails it and is re-raised.

```python
chunks = [
    {"chunk_id": f"orders:{month}", "item": "orders", "label": month,
     "units": estimated_rows}
    for month, estimated_rows in monthly_estimates.items()
]

with gr.job("etl.orders.monthly.v1", chunks=chunks,
            label="Export orders", heartbeat_s=30) as job:
    for spec in chunks:
        with job.chunk(spec["chunk_id"], worker=worker_name):
            export_month(spec["label"])
```

Chunk plans can also be a mapping of id to units, or a list of ids (one
unit each):

```python
chunks = {"users:0": 50_000, "users:1": 50_000}
chunks = ["load", "fit", "write"]
```

### Manual lifecycle

When a context manager does not fit your framework:

```python
job = gr.job("model.forecast.v3",
             chunks=[{"chunk_id": "load", "item": "prepare", "units": 1},
                     {"chunk_id": "fit", "item": "model", "units": 10},
                     {"chunk_id": "write", "item": "output", "units": 1}],
             expected_silence_s=300).register()
try:
    with job.chunk("fit"):
        fit_model()
    job.complete()
except Exception as exc:
    job.fail(f"{type(exc).__name__}: {exc}")
    raise
```

### Indivisible steps

For one step that cannot be split, report a measured fraction:

```python
with job.chunk("fit") as chunk:
    for completed, total in train():
        chunk.progress(completed / total)
```

### Job options

| Option | Meaning |
|---|---|
| `label` | Name shown on the card (defaults to `job_type_id`) |
| `meta` | Free-form JSON attached to the run |
| `job_id` | Your own stable run id (default: generated) |
| `heartbeat_s` | Send heartbeats at this interval; a silent process is failed |
| `expected_silence_s` | How long the job may legitimately stay quiet |
| `total_units` | Override the sum of chunk units |

`job.chunk(chunk_id, units=None, worker=None, item=None)` accepts chunks
not in the plan and adds them on the fly.

## Pings

One call per cycle of a recurring process. Report errors with a
description; declare the next due time when the schedule is too sparse to
learn (weekly, monthly).

```python
def daily_cycle():
    try:
        run_export()
        gr.ping("daily-export")
    except Exception as exc:
        gr.ping("daily-export", status="error",
                description=f"{type(exc).__name__}: {exc}")
        raise

gr.ping("monthly-report", expected_next_ts=next_run_at)   # epoch ms or datetime
```

## Alerts

A one-line message for operators under a stable `category`. `status` is
`info` (default), `warning`, or `error`. `short` is an optional few-word
version — the least a reader needs to know what happened.

```python
gr.alert("storage", "Disk at 91% on db-1", status="warning")
gr.alert("billing", "Stripe webhook rejected 3 events", status="error",
         short="stripe: 3 rejected")
```

## One-shot calls for scripts

Cron scripts can send a single ping or alert without `init`. They return
immediately by default; `wait=True` blocks and returns whether delivery
succeeded. Neither ever raises.

```python
gr.ping_once("daily-export", token=tok, url=core_url)
gr.alert_once("deploy", "v2.3.1 live", token=tok, url=core_url)
ok = gr.ping_once("daily-export", token=tok, url=core_url, wait=True)
```

## Simulation runs

Simulation producers publish a manifest once and stream each run tick by
tick:

```python
gr.manifest(manifest_dict)

run = gr.run("mkt.spend_optimizer", levers=levers, seed=7)
for t, values in engine.simulate(levers):
    run.tick(t, values)
run.done(metrics={"net_revenue_total": total}, objective=total)
```

`run.fail(error)` ends a run unsuccessfully; `run.best(objective)` marks
a search's new best candidate.

## Advanced

`gr.Client(url, token=...)` is an independent client for applications
that cannot use module-level state; call `close()` on shutdown to flush
pending events. `gr.emit(event)` queues a raw event for lifecycles the
structured API does not cover. `init` also accepts `flush_interval_s`,
`queue_size`, and `retry_interval_s`; the defaults suit nearly everyone.
