gridrunner 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gridrunner-0.6.0/LICENSE +21 -0
- gridrunner-0.6.0/PKG-INFO +284 -0
- gridrunner-0.6.0/README.md +271 -0
- gridrunner-0.6.0/gridrunner/__init__.py +25 -0
- gridrunner-0.6.0/gridrunner/ids.py +29 -0
- gridrunner-0.6.0/gridrunner/sdk.py +624 -0
- gridrunner-0.6.0/gridrunner.egg-info/PKG-INFO +284 -0
- gridrunner-0.6.0/gridrunner.egg-info/SOURCES.txt +10 -0
- gridrunner-0.6.0/gridrunner.egg-info/dependency_links.txt +1 -0
- gridrunner-0.6.0/gridrunner.egg-info/top_level.txt +1 -0
- gridrunner-0.6.0/pyproject.toml +20 -0
- gridrunner-0.6.0/setup.cfg +4 -0
gridrunner-0.6.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GRIDRUNNER authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gridrunner
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Producer SDK for GRIDRUNNER — fire-and-forget job progress events
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# GRIDRUNNER Python SDK
|
|
15
|
+
|
|
16
|
+
Standard-library-only producer client for reporting bounded jobs and
|
|
17
|
+
service status pings — super-light health monitoring for services and
|
|
18
|
+
recurring tasks — to GRIDRUNNER. See the [repository README](../README.md)
|
|
19
|
+
for the job/item/chunk/unit model, the ping mechanic, authentication,
|
|
20
|
+
delivery guarantees, and installation.
|
|
21
|
+
|
|
22
|
+
## Configure
|
|
23
|
+
|
|
24
|
+
Initialize once near process startup with explicit credentials — the SDK
|
|
25
|
+
reads no environment variables and no config files:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import gridrunner as gr
|
|
29
|
+
|
|
30
|
+
client = gr.init(token=read_secret("gridrunner_produce_token"),
|
|
31
|
+
url="https://gridrunner.example.com")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A token without `url=` raises `ValueError`. With neither, `init()` targets
|
|
35
|
+
the local unauthenticated dev core:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
client = gr.init() # http://127.0.0.1:7077
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The token is also your identity: the name your producer was registered
|
|
42
|
+
with when the token was minted becomes the service name of every ping you
|
|
43
|
+
send.
|
|
44
|
+
|
|
45
|
+
All options:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
client = gr.init(
|
|
49
|
+
token=token,
|
|
50
|
+
url="https://gridrunner.example.com",
|
|
51
|
+
flush_interval_s=0.2,
|
|
52
|
+
queue_size=20_000,
|
|
53
|
+
retry_interval_s=5.0,
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Preferred usage
|
|
58
|
+
|
|
59
|
+
Declare chunks up front and use context managers. Successful exits emit
|
|
60
|
+
completion; exceptions emit failure and are re-raised.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import gridrunner as gr
|
|
64
|
+
|
|
65
|
+
gr.init(token=read_secret("gridrunner_produce_token"),
|
|
66
|
+
url="https://gridrunner.example.com")
|
|
67
|
+
|
|
68
|
+
chunks = [
|
|
69
|
+
{
|
|
70
|
+
"chunk_id": f"orders:{month}",
|
|
71
|
+
"item": "orders",
|
|
72
|
+
"label": month,
|
|
73
|
+
"units": estimated_rows,
|
|
74
|
+
}
|
|
75
|
+
for month, estimated_rows in monthly_estimates.items()
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
with gr.job(
|
|
79
|
+
"etl.orders.monthly.v1",
|
|
80
|
+
chunks=chunks,
|
|
81
|
+
label="Export orders",
|
|
82
|
+
meta={"service": "billing-export"},
|
|
83
|
+
heartbeat_s=30,
|
|
84
|
+
) as job:
|
|
85
|
+
for spec in chunks:
|
|
86
|
+
with job.chunk(spec["chunk_id"], worker=worker_name):
|
|
87
|
+
export_month(spec["label"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Manual lifecycle
|
|
91
|
+
|
|
92
|
+
Use manual methods when a context manager does not match the host framework:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
job = gr.job(
|
|
96
|
+
"model.forecast.v3",
|
|
97
|
+
chunks=[
|
|
98
|
+
{"chunk_id": "load", "item": "prepare", "units": 1},
|
|
99
|
+
{"chunk_id": "fit", "item": "model", "units": 10},
|
|
100
|
+
{"chunk_id": "write", "item": "output", "units": 1},
|
|
101
|
+
],
|
|
102
|
+
expected_silence_s=300,
|
|
103
|
+
).register()
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
with job.chunk("fit"):
|
|
107
|
+
fit_model()
|
|
108
|
+
job.complete()
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
job.fail(f"{type(exc).__name__}: {exc}")
|
|
111
|
+
raise
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For one genuinely indivisible chunk, report a measured fraction:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
with job.chunk("fit") as chunk:
|
|
118
|
+
for completed, total in train():
|
|
119
|
+
chunk.progress(completed / total)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Service pings
|
|
123
|
+
|
|
124
|
+
For services and independent regular jobs — cron cycles, daemons,
|
|
125
|
+
schedulers — use the ping mechanic instead of jobs: one call per cycle,
|
|
126
|
+
and GRIDRUNNER learns the cadence and alarms on silence or errors by
|
|
127
|
+
itself.
|
|
128
|
+
|
|
129
|
+
The ping only names the recurring process (`ping_type` — an
|
|
130
|
+
understandable name like `daily-export` or `queue-sweep`). The service
|
|
131
|
+
name is resolved server-side from the produce token — the name the
|
|
132
|
+
producer was registered with — so producers can never contaminate each
|
|
133
|
+
other's series.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
def daily_cycle():
|
|
137
|
+
try:
|
|
138
|
+
run_export()
|
|
139
|
+
gr.ping("daily-export")
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
gr.ping("daily-export", status="error",
|
|
142
|
+
description=f"{type(exc).__name__}: {exc}")
|
|
143
|
+
raise
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
If the cadence is too sparse to learn quickly (weekly, monthly), declare
|
|
147
|
+
when the next ping is due — GRIDRUNNER alarms on that deadline instead of
|
|
148
|
+
waiting to learn the pattern:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
gr.ping("monthly-report", expected_next_ts=next_run_at) # epoch ms or datetime
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For cron scripts that shouldn't carry a client lifecycle at all, use the
|
|
155
|
+
one-shot call — token and ping in one line, no `init`, no queue:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
gr.ping_once("daily-export", token=tok, url=core_url)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
See [Service pings](../README.md#service-pings) in the repository README
|
|
162
|
+
for the full contract.
|
|
163
|
+
|
|
164
|
+
## Chunk plan formats
|
|
165
|
+
|
|
166
|
+
Full dictionaries preserve item grouping and weights:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
chunks = [
|
|
170
|
+
{"chunk_id": "users:0", "item": "users", "label": "0–49k", "units": 50_000},
|
|
171
|
+
{"chunk_id": "users:1", "item": "users", "label": "50k–99k", "units": 50_000},
|
|
172
|
+
]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Convenience forms are accepted:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
chunks = {"users:0": 50_000, "users:1": 50_000}
|
|
179
|
+
chunks = ["load", "fit", "write"] # each gets one unit
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## API
|
|
183
|
+
|
|
184
|
+
### `init(token=None, url=None, **client_options)`
|
|
185
|
+
|
|
186
|
+
Configures the module-level client used by `emit()`, `job()`, and
|
|
187
|
+
`ping()`. A token requires an explicit `url` (`ValueError` otherwise);
|
|
188
|
+
with neither it targets a local core at `http://127.0.0.1:7077`. The
|
|
189
|
+
target never changes behind the caller's back. `connect(url=None,
|
|
190
|
+
token=None, ...)` remains as a deprecated alias.
|
|
191
|
+
|
|
192
|
+
### `job(job_type_id, chunks=None, **options) -> Job`
|
|
193
|
+
|
|
194
|
+
Options:
|
|
195
|
+
|
|
196
|
+
| Option | Meaning |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `label` | Human-readable run label |
|
|
199
|
+
| `total_units` | Override sum of chunk units |
|
|
200
|
+
| `meta` | JSON metadata attached to registration |
|
|
201
|
+
| `job_id` | Stable caller-provided run ID; otherwise generated ULID |
|
|
202
|
+
| `heartbeat_s` | Automatic heartbeat interval |
|
|
203
|
+
| `expected_silence_s` | Expected quiet-work bound |
|
|
204
|
+
|
|
205
|
+
### `Job`
|
|
206
|
+
|
|
207
|
+
- `register()` — emits `job.registered` and starts heartbeat.
|
|
208
|
+
- `chunk(id, units=None, worker=None, item=None)` — returns a chunk context.
|
|
209
|
+
- `complete()` — stops heartbeat and emits `job.completed`.
|
|
210
|
+
- `fail(error="")` — stops heartbeat and emits `job.failed`.
|
|
211
|
+
|
|
212
|
+
### `Chunk`
|
|
213
|
+
|
|
214
|
+
- Context entry emits `chunk.started`.
|
|
215
|
+
- `progress(fraction)` emits a clamped measured fraction in `[0, 1]`.
|
|
216
|
+
- Normal context exit emits `chunk.completed` with duration.
|
|
217
|
+
- Exceptional context exit emits `chunk.failed` and re-raises.
|
|
218
|
+
|
|
219
|
+
### `ping(ping_type="default", status="ok", description=None, expected_next_ts=None)`
|
|
220
|
+
|
|
221
|
+
Reports a recurring status ping — a mechanic separate from jobs (see the
|
|
222
|
+
[repository README](../README.md#service-pings)). The service name is
|
|
223
|
+
resolved server-side from the produce token; `ping_type` names the
|
|
224
|
+
recurring process:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
gr.ping("daily-export")
|
|
228
|
+
gr.ping("daily-export", status="error", description="table locked")
|
|
229
|
+
gr.ping("monthly-report", expected_next_ts=next_run_at)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`expected_next_ts` (epoch ms or a `datetime`) optionally declares when the
|
|
233
|
+
next ping is due, for cadences too sparse to learn quickly.
|
|
234
|
+
|
|
235
|
+
### `ping_once(ping_type="default", *, token=None, status="ok", description=None, expected_next_ts=None, url=None, wait=False, timeout_s=5.0)`
|
|
236
|
+
|
|
237
|
+
One-shot stateless ping for cron scripts and one-liners: token and ping in
|
|
238
|
+
the same call, one direct HTTP POST — no `init`, no persistent client, no
|
|
239
|
+
queue, no backlog (outage-safe replay is what you give up).
|
|
240
|
+
|
|
241
|
+
Fire-and-forget by default: returns `None` immediately and delivers on a
|
|
242
|
+
short-lived non-daemon thread, so a script that exits right after pinging
|
|
243
|
+
doesn't lose the ping (the process lingers at most `timeout_s`).
|
|
244
|
+
`wait=True` performs the POST inline and returns `True`/`False`. Never
|
|
245
|
+
raises either way. `url` resolves like `init`: required alongside a token
|
|
246
|
+
(a token without `url` is logged and dropped), the local dev core without
|
|
247
|
+
one.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
gr.ping_once("daily-export", token=tok, url=core_url) # returns immediately
|
|
251
|
+
ok = gr.ping_once("daily-export", token=tok, url=core_url, wait=True) # opt-in: block → bool
|
|
252
|
+
gr.ping_once("daily-export", token=tok, url=core_url, status="error",
|
|
253
|
+
description="table locked")
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### `emit(event)`
|
|
257
|
+
|
|
258
|
+
Queues a raw event on the configured module client. Prefer the structured
|
|
259
|
+
job/chunk API unless integrating an unsupported lifecycle.
|
|
260
|
+
|
|
261
|
+
### `Client`
|
|
262
|
+
|
|
263
|
+
An independent client for applications that cannot use module-level state:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
client = gr.Client(url, token=token)
|
|
267
|
+
client.emit({"type": "job.heartbeat", "job_id": job_id})
|
|
268
|
+
client.close()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Call `close()` during graceful shutdown to flush the in-memory queue.
|
|
272
|
+
|
|
273
|
+
## Failure behavior
|
|
274
|
+
|
|
275
|
+
- Connection and HTTP errors never propagate into host work.
|
|
276
|
+
- Undelivered events are held in a bounded, liveness-scoped in-memory
|
|
277
|
+
backlog: live jobs keep compacted events (latest progress per chunk, no
|
|
278
|
+
heartbeats; ok pings compact to the newest per series, error pings are
|
|
279
|
+
all kept) and replay on reconnect; jobs that start and finish entirely
|
|
280
|
+
while the core is unreachable are discarded, never sent late.
|
|
281
|
+
- Events are assigned idempotency IDs before queueing.
|
|
282
|
+
- Non-finite floats are converted to JSON `null`.
|
|
283
|
+
- A full queue drops new events and logs at increasing thresholds; held
|
|
284
|
+
backlogs log a rate-limited warning (distinct message on 401).
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# GRIDRUNNER Python SDK
|
|
2
|
+
|
|
3
|
+
Standard-library-only producer client for reporting bounded jobs and
|
|
4
|
+
service status pings — super-light health monitoring for services and
|
|
5
|
+
recurring tasks — to GRIDRUNNER. See the [repository README](../README.md)
|
|
6
|
+
for the job/item/chunk/unit model, the ping mechanic, authentication,
|
|
7
|
+
delivery guarantees, and installation.
|
|
8
|
+
|
|
9
|
+
## Configure
|
|
10
|
+
|
|
11
|
+
Initialize once near process startup with explicit credentials — the SDK
|
|
12
|
+
reads no environment variables and no config files:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import gridrunner as gr
|
|
16
|
+
|
|
17
|
+
client = gr.init(token=read_secret("gridrunner_produce_token"),
|
|
18
|
+
url="https://gridrunner.example.com")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A token without `url=` raises `ValueError`. With neither, `init()` targets
|
|
22
|
+
the local unauthenticated dev core:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
client = gr.init() # http://127.0.0.1:7077
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The token is also your identity: the name your producer was registered
|
|
29
|
+
with when the token was minted becomes the service name of every ping you
|
|
30
|
+
send.
|
|
31
|
+
|
|
32
|
+
All options:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
client = gr.init(
|
|
36
|
+
token=token,
|
|
37
|
+
url="https://gridrunner.example.com",
|
|
38
|
+
flush_interval_s=0.2,
|
|
39
|
+
queue_size=20_000,
|
|
40
|
+
retry_interval_s=5.0,
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Preferred usage
|
|
45
|
+
|
|
46
|
+
Declare chunks up front and use context managers. Successful exits emit
|
|
47
|
+
completion; exceptions emit failure and are re-raised.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import gridrunner as gr
|
|
51
|
+
|
|
52
|
+
gr.init(token=read_secret("gridrunner_produce_token"),
|
|
53
|
+
url="https://gridrunner.example.com")
|
|
54
|
+
|
|
55
|
+
chunks = [
|
|
56
|
+
{
|
|
57
|
+
"chunk_id": f"orders:{month}",
|
|
58
|
+
"item": "orders",
|
|
59
|
+
"label": month,
|
|
60
|
+
"units": estimated_rows,
|
|
61
|
+
}
|
|
62
|
+
for month, estimated_rows in monthly_estimates.items()
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
with gr.job(
|
|
66
|
+
"etl.orders.monthly.v1",
|
|
67
|
+
chunks=chunks,
|
|
68
|
+
label="Export orders",
|
|
69
|
+
meta={"service": "billing-export"},
|
|
70
|
+
heartbeat_s=30,
|
|
71
|
+
) as job:
|
|
72
|
+
for spec in chunks:
|
|
73
|
+
with job.chunk(spec["chunk_id"], worker=worker_name):
|
|
74
|
+
export_month(spec["label"])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Manual lifecycle
|
|
78
|
+
|
|
79
|
+
Use manual methods when a context manager does not match the host framework:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
job = gr.job(
|
|
83
|
+
"model.forecast.v3",
|
|
84
|
+
chunks=[
|
|
85
|
+
{"chunk_id": "load", "item": "prepare", "units": 1},
|
|
86
|
+
{"chunk_id": "fit", "item": "model", "units": 10},
|
|
87
|
+
{"chunk_id": "write", "item": "output", "units": 1},
|
|
88
|
+
],
|
|
89
|
+
expected_silence_s=300,
|
|
90
|
+
).register()
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
with job.chunk("fit"):
|
|
94
|
+
fit_model()
|
|
95
|
+
job.complete()
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
job.fail(f"{type(exc).__name__}: {exc}")
|
|
98
|
+
raise
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For one genuinely indivisible chunk, report a measured fraction:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
with job.chunk("fit") as chunk:
|
|
105
|
+
for completed, total in train():
|
|
106
|
+
chunk.progress(completed / total)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Service pings
|
|
110
|
+
|
|
111
|
+
For services and independent regular jobs — cron cycles, daemons,
|
|
112
|
+
schedulers — use the ping mechanic instead of jobs: one call per cycle,
|
|
113
|
+
and GRIDRUNNER learns the cadence and alarms on silence or errors by
|
|
114
|
+
itself.
|
|
115
|
+
|
|
116
|
+
The ping only names the recurring process (`ping_type` — an
|
|
117
|
+
understandable name like `daily-export` or `queue-sweep`). The service
|
|
118
|
+
name is resolved server-side from the produce token — the name the
|
|
119
|
+
producer was registered with — so producers can never contaminate each
|
|
120
|
+
other's series.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
def daily_cycle():
|
|
124
|
+
try:
|
|
125
|
+
run_export()
|
|
126
|
+
gr.ping("daily-export")
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
gr.ping("daily-export", status="error",
|
|
129
|
+
description=f"{type(exc).__name__}: {exc}")
|
|
130
|
+
raise
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If the cadence is too sparse to learn quickly (weekly, monthly), declare
|
|
134
|
+
when the next ping is due — GRIDRUNNER alarms on that deadline instead of
|
|
135
|
+
waiting to learn the pattern:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
gr.ping("monthly-report", expected_next_ts=next_run_at) # epoch ms or datetime
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For cron scripts that shouldn't carry a client lifecycle at all, use the
|
|
142
|
+
one-shot call — token and ping in one line, no `init`, no queue:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
gr.ping_once("daily-export", token=tok, url=core_url)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
See [Service pings](../README.md#service-pings) in the repository README
|
|
149
|
+
for the full contract.
|
|
150
|
+
|
|
151
|
+
## Chunk plan formats
|
|
152
|
+
|
|
153
|
+
Full dictionaries preserve item grouping and weights:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
chunks = [
|
|
157
|
+
{"chunk_id": "users:0", "item": "users", "label": "0–49k", "units": 50_000},
|
|
158
|
+
{"chunk_id": "users:1", "item": "users", "label": "50k–99k", "units": 50_000},
|
|
159
|
+
]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Convenience forms are accepted:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
chunks = {"users:0": 50_000, "users:1": 50_000}
|
|
166
|
+
chunks = ["load", "fit", "write"] # each gets one unit
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## API
|
|
170
|
+
|
|
171
|
+
### `init(token=None, url=None, **client_options)`
|
|
172
|
+
|
|
173
|
+
Configures the module-level client used by `emit()`, `job()`, and
|
|
174
|
+
`ping()`. A token requires an explicit `url` (`ValueError` otherwise);
|
|
175
|
+
with neither it targets a local core at `http://127.0.0.1:7077`. The
|
|
176
|
+
target never changes behind the caller's back. `connect(url=None,
|
|
177
|
+
token=None, ...)` remains as a deprecated alias.
|
|
178
|
+
|
|
179
|
+
### `job(job_type_id, chunks=None, **options) -> Job`
|
|
180
|
+
|
|
181
|
+
Options:
|
|
182
|
+
|
|
183
|
+
| Option | Meaning |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `label` | Human-readable run label |
|
|
186
|
+
| `total_units` | Override sum of chunk units |
|
|
187
|
+
| `meta` | JSON metadata attached to registration |
|
|
188
|
+
| `job_id` | Stable caller-provided run ID; otherwise generated ULID |
|
|
189
|
+
| `heartbeat_s` | Automatic heartbeat interval |
|
|
190
|
+
| `expected_silence_s` | Expected quiet-work bound |
|
|
191
|
+
|
|
192
|
+
### `Job`
|
|
193
|
+
|
|
194
|
+
- `register()` — emits `job.registered` and starts heartbeat.
|
|
195
|
+
- `chunk(id, units=None, worker=None, item=None)` — returns a chunk context.
|
|
196
|
+
- `complete()` — stops heartbeat and emits `job.completed`.
|
|
197
|
+
- `fail(error="")` — stops heartbeat and emits `job.failed`.
|
|
198
|
+
|
|
199
|
+
### `Chunk`
|
|
200
|
+
|
|
201
|
+
- Context entry emits `chunk.started`.
|
|
202
|
+
- `progress(fraction)` emits a clamped measured fraction in `[0, 1]`.
|
|
203
|
+
- Normal context exit emits `chunk.completed` with duration.
|
|
204
|
+
- Exceptional context exit emits `chunk.failed` and re-raises.
|
|
205
|
+
|
|
206
|
+
### `ping(ping_type="default", status="ok", description=None, expected_next_ts=None)`
|
|
207
|
+
|
|
208
|
+
Reports a recurring status ping — a mechanic separate from jobs (see the
|
|
209
|
+
[repository README](../README.md#service-pings)). The service name is
|
|
210
|
+
resolved server-side from the produce token; `ping_type` names the
|
|
211
|
+
recurring process:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
gr.ping("daily-export")
|
|
215
|
+
gr.ping("daily-export", status="error", description="table locked")
|
|
216
|
+
gr.ping("monthly-report", expected_next_ts=next_run_at)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`expected_next_ts` (epoch ms or a `datetime`) optionally declares when the
|
|
220
|
+
next ping is due, for cadences too sparse to learn quickly.
|
|
221
|
+
|
|
222
|
+
### `ping_once(ping_type="default", *, token=None, status="ok", description=None, expected_next_ts=None, url=None, wait=False, timeout_s=5.0)`
|
|
223
|
+
|
|
224
|
+
One-shot stateless ping for cron scripts and one-liners: token and ping in
|
|
225
|
+
the same call, one direct HTTP POST — no `init`, no persistent client, no
|
|
226
|
+
queue, no backlog (outage-safe replay is what you give up).
|
|
227
|
+
|
|
228
|
+
Fire-and-forget by default: returns `None` immediately and delivers on a
|
|
229
|
+
short-lived non-daemon thread, so a script that exits right after pinging
|
|
230
|
+
doesn't lose the ping (the process lingers at most `timeout_s`).
|
|
231
|
+
`wait=True` performs the POST inline and returns `True`/`False`. Never
|
|
232
|
+
raises either way. `url` resolves like `init`: required alongside a token
|
|
233
|
+
(a token without `url` is logged and dropped), the local dev core without
|
|
234
|
+
one.
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
gr.ping_once("daily-export", token=tok, url=core_url) # returns immediately
|
|
238
|
+
ok = gr.ping_once("daily-export", token=tok, url=core_url, wait=True) # opt-in: block → bool
|
|
239
|
+
gr.ping_once("daily-export", token=tok, url=core_url, status="error",
|
|
240
|
+
description="table locked")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `emit(event)`
|
|
244
|
+
|
|
245
|
+
Queues a raw event on the configured module client. Prefer the structured
|
|
246
|
+
job/chunk API unless integrating an unsupported lifecycle.
|
|
247
|
+
|
|
248
|
+
### `Client`
|
|
249
|
+
|
|
250
|
+
An independent client for applications that cannot use module-level state:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
client = gr.Client(url, token=token)
|
|
254
|
+
client.emit({"type": "job.heartbeat", "job_id": job_id})
|
|
255
|
+
client.close()
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Call `close()` during graceful shutdown to flush the in-memory queue.
|
|
259
|
+
|
|
260
|
+
## Failure behavior
|
|
261
|
+
|
|
262
|
+
- Connection and HTTP errors never propagate into host work.
|
|
263
|
+
- Undelivered events are held in a bounded, liveness-scoped in-memory
|
|
264
|
+
backlog: live jobs keep compacted events (latest progress per chunk, no
|
|
265
|
+
heartbeats; ok pings compact to the newest per series, error pings are
|
|
266
|
+
all kept) and replay on reconnect; jobs that start and finish entirely
|
|
267
|
+
while the core is unreachable are discarded, never sent late.
|
|
268
|
+
- Events are assigned idempotency IDs before queueing.
|
|
269
|
+
- Non-finite floats are converted to JSON `null`.
|
|
270
|
+
- A full queue drops new events and logs at increasing thresholds; held
|
|
271
|
+
backlogs log a rate-limited warning (distinct message on 401).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""GRIDRUNNER producer SDK — fire-and-forget INGEST job events and the
|
|
2
|
+
SIMULATE run stream."""
|
|
3
|
+
|
|
4
|
+
from .ids import ulid
|
|
5
|
+
from .sdk import (
|
|
6
|
+
Chunk, Client, Job, Run, connect, emit, init, job, manifest, ping,
|
|
7
|
+
ping_once, run,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__version__ = "0.6.0"
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Chunk",
|
|
13
|
+
"Client",
|
|
14
|
+
"Job",
|
|
15
|
+
"Run",
|
|
16
|
+
"connect",
|
|
17
|
+
"emit",
|
|
18
|
+
"init",
|
|
19
|
+
"job",
|
|
20
|
+
"manifest",
|
|
21
|
+
"ping",
|
|
22
|
+
"ping_once",
|
|
23
|
+
"run",
|
|
24
|
+
"ulid",
|
|
25
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""ULID generation (stdlib-only)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
_ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
8
|
+
_lock = threading.Lock()
|
|
9
|
+
_last: list = [0, 0] # [ts_ms, randomness as int]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _encode(value: int, length: int) -> str:
|
|
13
|
+
out = []
|
|
14
|
+
for _ in range(length):
|
|
15
|
+
out.append(_ENCODING[value & 0x1F])
|
|
16
|
+
value >>= 5
|
|
17
|
+
return "".join(reversed(out))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ulid() -> str:
|
|
21
|
+
with _lock:
|
|
22
|
+
ts = int(time.time() * 1000)
|
|
23
|
+
if ts == _last[0]:
|
|
24
|
+
_last[1] += 1
|
|
25
|
+
else:
|
|
26
|
+
_last[0] = ts
|
|
27
|
+
_last[1] = int.from_bytes(os.urandom(10), "big")
|
|
28
|
+
rand = _last[1] & ((1 << 80) - 1)
|
|
29
|
+
return _encode(ts, 10) + _encode(rand, 16)
|