gridlinegpu 0.1.0__tar.gz → 0.1.2__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.
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/PKG-INFO +6 -3
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/README.md +5 -2
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/pyproject.toml +1 -1
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu/client.py +15 -6
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu.egg-info/PKG-INFO +6 -3
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/tests/test_client.py +32 -1
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/setup.cfg +0 -0
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu/__init__.py +0 -0
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu/errors.py +0 -0
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu.egg-info/SOURCES.txt +0 -0
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu.egg-info/dependency_links.txt +0 -0
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu.egg-info/requires.txt +0 -0
- {gridlinegpu-0.1.0 → gridlinegpu-0.1.2}/src/gridlinegpu.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gridlinegpu
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python client for Gridline YAML workload routing
|
|
5
5
|
Author: Gridline GPU
|
|
6
6
|
License: Proprietary
|
|
@@ -22,7 +22,7 @@ fresh plan when you supply a cost ceiling and idempotency key.
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
python -m pip install gridlinegpu
|
|
25
|
+
python -m pip install 'gridlinegpu>=0.1.2,<0.2'
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Create a scoped token using the Gridline CLI after browser sign-in. The token stays in a
|
|
@@ -48,7 +48,8 @@ print(plan.review) # Check selected GPU, exclusions, estimate, and maximum.
|
|
|
48
48
|
# Launch only after reviewing the fresh plan and accepting its maximum spend.
|
|
49
49
|
launch = client.launch(plan, max_cost_usd="5.00", idempotency_key="my-job-2026-09-27")
|
|
50
50
|
job_id = launch["jobId"]
|
|
51
|
-
|
|
51
|
+
ready = client.wait(job_id, timeout=7200)
|
|
52
|
+
print(ready["job"]["outputs"], ready["job"]["outcome"])
|
|
52
53
|
print(client.outputs(job_id, "results"))
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -57,3 +58,5 @@ changes a plan or retries an ambiguous paid request. If launch outcome is unknow
|
|
|
57
58
|
inspect `LaunchOutcomeUnknown.workload_id` with the same idempotency key; do not use a
|
|
58
59
|
new key. `outputs()` verifies byte count and SHA-256 before creating private files.
|
|
59
60
|
Cancellation requests stop; cleanup and final provider billing can remain pending.
|
|
61
|
+
For finite jobs, `wait()` returns when outputs are complete, even if cleanup and billing
|
|
62
|
+
leave `outcome` pending. Check `status()` later for final settlement.
|
|
@@ -7,7 +7,7 @@ fresh plan when you supply a cost ceiling and idempotency key.
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
python -m pip install gridlinegpu
|
|
10
|
+
python -m pip install 'gridlinegpu>=0.1.2,<0.2'
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Create a scoped token using the Gridline CLI after browser sign-in. The token stays in a
|
|
@@ -33,7 +33,8 @@ print(plan.review) # Check selected GPU, exclusions, estimate, and maximum.
|
|
|
33
33
|
# Launch only after reviewing the fresh plan and accepting its maximum spend.
|
|
34
34
|
launch = client.launch(plan, max_cost_usd="5.00", idempotency_key="my-job-2026-09-27")
|
|
35
35
|
job_id = launch["jobId"]
|
|
36
|
-
|
|
36
|
+
ready = client.wait(job_id, timeout=7200)
|
|
37
|
+
print(ready["job"]["outputs"], ready["job"]["outcome"])
|
|
37
38
|
print(client.outputs(job_id, "results"))
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -42,3 +43,5 @@ changes a plan or retries an ambiguous paid request. If launch outcome is unknow
|
|
|
42
43
|
inspect `LaunchOutcomeUnknown.workload_id` with the same idempotency key; do not use a
|
|
43
44
|
new key. `outputs()` verifies byte count and SHA-256 before creating private files.
|
|
44
45
|
Cancellation requests stop; cleanup and final provider billing can remain pending.
|
|
46
|
+
For finite jobs, `wait()` returns when outputs are complete, even if cleanup and billing
|
|
47
|
+
leave `outcome` pending. Check `status()` later for final settlement.
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import time
|
|
13
|
+
import uuid
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from datetime import datetime, timezone
|
|
15
16
|
from decimal import Decimal
|
|
@@ -29,6 +30,7 @@ _KEY = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
|
|
29
30
|
_SHA = re.compile(r"^[0-9a-f]{64}$")
|
|
30
31
|
_MAX_YAML = 65_536
|
|
31
32
|
_MAX_DOWNLOAD = 8 * 1024 * 1024
|
|
33
|
+
_USER_AGENT = "GridlinePythonSDK/1"
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class _NoRedirect(HTTPRedirectHandler):
|
|
@@ -97,7 +99,10 @@ class GridlineClient:
|
|
|
97
99
|
"POST", f"{_API}/validate", {"schemaVersion": 1, "yaml": source, "attachmentBindings": []}
|
|
98
100
|
)
|
|
99
101
|
|
|
100
|
-
def plan(self, yaml_path: str | Path) -> PlannedWorkload:
|
|
102
|
+
def plan(self, yaml_path: str | Path, *, submission_key: str | None = None) -> PlannedWorkload:
|
|
103
|
+
"""Create a fresh submission; reuse submission_key only to retry an uncertain submit."""
|
|
104
|
+
if submission_key is not None and not _KEY.fullmatch(submission_key):
|
|
105
|
+
raise ValueError("submission_key must contain 1–128 safe characters")
|
|
101
106
|
path, source, document = _read_yaml(yaml_path)
|
|
102
107
|
validated = self.validate(path)
|
|
103
108
|
available = self.availability()
|
|
@@ -113,8 +118,9 @@ class GridlineClient:
|
|
|
113
118
|
raise GridlineError("Invalid server attachment limit", code="remote_contract_invalid")
|
|
114
119
|
bindings = [self._stage(path.parent, name, item, ceiling) for name, item in inputs]
|
|
115
120
|
body = {"schemaVersion": 1, "yaml": source, "attachmentBindings": bindings}
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
submitted = self._json(
|
|
122
|
+
"POST", f"{_API}/submit", body, key=submission_key or f"sdk-submit-{uuid.uuid4()}"
|
|
123
|
+
)
|
|
118
124
|
workload_id = _id(submitted.get("workloadId"), "workloadId")
|
|
119
125
|
result = self._json("POST", f"{_API}/{quote(workload_id)}/plan")
|
|
120
126
|
if result.get("workloadId") != workload_id or result.get("workloadHash") != submitted.get("workloadHash"):
|
|
@@ -205,13 +211,16 @@ class GridlineClient:
|
|
|
205
211
|
return result
|
|
206
212
|
|
|
207
213
|
def wait(self, job_id: str, *, timeout: float = 3600, interval: float = 5) -> dict[str, Any]:
|
|
214
|
+
"""Wait for finite-job outputs or a terminal outcome; billing may finish later."""
|
|
208
215
|
if timeout <= 0 or interval <= 0:
|
|
209
216
|
raise ValueError("timeout and interval must be positive")
|
|
210
217
|
deadline = time.monotonic() + timeout
|
|
211
218
|
while True:
|
|
212
219
|
result = self.status(job_id)
|
|
213
|
-
|
|
214
|
-
if
|
|
220
|
+
job = result["job"]
|
|
221
|
+
if job.get("outcome") in ("succeeded", "failed", "cancelled", "timed_out") or (
|
|
222
|
+
job.get("execution", {}).get("mode") == "finite" and job.get("outputs") == "complete"
|
|
223
|
+
):
|
|
215
224
|
return result
|
|
216
225
|
if time.monotonic() >= deadline:
|
|
217
226
|
raise TimeoutError(f"Job {job_id} did not finish before timeout")
|
|
@@ -307,7 +316,7 @@ class GridlineClient:
|
|
|
307
316
|
headers: dict[str, str] | None = None, maximum: int) -> bytes:
|
|
308
317
|
if not isinstance(url, str) or urlparse(url).scheme != "https":
|
|
309
318
|
raise GridlineError("Grant URL must use HTTPS", code="remote_contract_invalid")
|
|
310
|
-
request = Request(url, data=data, headers=headers or {}, method=method)
|
|
319
|
+
request = Request(url, data=data, headers={"User-Agent": _USER_AGENT, **(headers or {})}, method=method)
|
|
311
320
|
try:
|
|
312
321
|
with self._opener.open(request, timeout=self._timeout) as response:
|
|
313
322
|
raw = response.read(maximum + 1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gridlinegpu
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python client for Gridline YAML workload routing
|
|
5
5
|
Author: Gridline GPU
|
|
6
6
|
License: Proprietary
|
|
@@ -22,7 +22,7 @@ fresh plan when you supply a cost ceiling and idempotency key.
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
python -m pip install gridlinegpu
|
|
25
|
+
python -m pip install 'gridlinegpu>=0.1.2,<0.2'
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Create a scoped token using the Gridline CLI after browser sign-in. The token stays in a
|
|
@@ -48,7 +48,8 @@ print(plan.review) # Check selected GPU, exclusions, estimate, and maximum.
|
|
|
48
48
|
# Launch only after reviewing the fresh plan and accepting its maximum spend.
|
|
49
49
|
launch = client.launch(plan, max_cost_usd="5.00", idempotency_key="my-job-2026-09-27")
|
|
50
50
|
job_id = launch["jobId"]
|
|
51
|
-
|
|
51
|
+
ready = client.wait(job_id, timeout=7200)
|
|
52
|
+
print(ready["job"]["outputs"], ready["job"]["outcome"])
|
|
52
53
|
print(client.outputs(job_id, "results"))
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -57,3 +58,5 @@ changes a plan or retries an ambiguous paid request. If launch outcome is unknow
|
|
|
57
58
|
inspect `LaunchOutcomeUnknown.workload_id` with the same idempotency key; do not use a
|
|
58
59
|
new key. `outputs()` verifies byte count and SHA-256 before creating private files.
|
|
59
60
|
Cancellation requests stop; cleanup and final provider billing can remain pending.
|
|
61
|
+
For finite jobs, `wait()` returns when outputs are complete, even if cleanup and billing
|
|
62
|
+
leave `outcome` pending. Check `status()` later for final settlement.
|
|
@@ -31,6 +31,7 @@ class FakeOpener:
|
|
|
31
31
|
def __init__(self):
|
|
32
32
|
self.calls = []
|
|
33
33
|
self.changed = False
|
|
34
|
+
self.job = {"jobId": "job_1", "outcome": "succeeded"}
|
|
34
35
|
self.expiry = (datetime.now(timezone.utc) + timedelta(minutes=2)).isoformat()
|
|
35
36
|
|
|
36
37
|
def open(self, request, timeout):
|
|
@@ -55,7 +56,7 @@ class FakeOpener:
|
|
|
55
56
|
elif path == "/api/workloads/yaml/yaml_1/launch":
|
|
56
57
|
result = {"workloadId": "yaml_1", "planHash": "c" * 64, "jobId": "job_1"}
|
|
57
58
|
elif path == "/api/workloads/yaml/jobs/job_1":
|
|
58
|
-
result = {"job":
|
|
59
|
+
result = {"job": self.job}
|
|
59
60
|
elif path == "/api/workloads/yaml/jobs/job_1/output":
|
|
60
61
|
result = {"jobId": "job_1", "state": "available", "manifest": {"outputs": [{
|
|
61
62
|
"name": "result.json", "artifactId": "output_1", "sha256": self.output_digest,
|
|
@@ -103,6 +104,21 @@ class ClientTest(unittest.TestCase):
|
|
|
103
104
|
self.client.launch(plan, max_cost_usd="5", idempotency_key="same-key")
|
|
104
105
|
self.assertFalse(any(path.endswith("/launch") for _, path, _, _ in self.opener.calls))
|
|
105
106
|
|
|
107
|
+
def test_identical_yaml_creates_fresh_submission_unless_retry_key_is_explicit(self):
|
|
108
|
+
for _ in range(2):
|
|
109
|
+
self.client.plan(self.path)
|
|
110
|
+
keys = [next(value for name, value in headers.items() if name.lower() == "idempotency-key")
|
|
111
|
+
for _, path, headers, _ in self.opener.calls
|
|
112
|
+
if path.endswith("/submit")]
|
|
113
|
+
self.assertEqual(len(keys), 2)
|
|
114
|
+
self.assertNotEqual(keys[0], keys[1])
|
|
115
|
+
self.client.plan(self.path, submission_key="retry-this-submit")
|
|
116
|
+
self.client.plan(self.path, submission_key="retry-this-submit")
|
|
117
|
+
keys = [next(value for name, value in headers.items() if name.lower() == "idempotency-key")
|
|
118
|
+
for _, path, headers, _ in self.opener.calls
|
|
119
|
+
if path.endswith("/submit")]
|
|
120
|
+
self.assertEqual(keys[-2:], ["retry-this-submit", "retry-this-submit"])
|
|
121
|
+
|
|
106
122
|
def test_cost_ceiling_and_yaml_change_cannot_launch(self):
|
|
107
123
|
plan = self.client.plan(self.path)
|
|
108
124
|
with self.assertRaisesRegex(GridlineError, "ceiling"):
|
|
@@ -111,6 +127,21 @@ class ClientTest(unittest.TestCase):
|
|
|
111
127
|
with self.assertRaisesRegex(GridlineError, "changed"):
|
|
112
128
|
self.client.launch(plan, max_cost_usd="5", idempotency_key="same-key")
|
|
113
129
|
|
|
130
|
+
def test_requests_identify_sdk_to_production_edge(self):
|
|
131
|
+
self.client.availability()
|
|
132
|
+
headers = self.opener.calls[-1][2]
|
|
133
|
+
self.assertEqual(next(value for name, value in headers.items() if name.lower() == "user-agent"),
|
|
134
|
+
"GridlinePythonSDK/1")
|
|
135
|
+
|
|
136
|
+
def test_wait_returns_when_finite_outputs_are_complete_before_billing(self):
|
|
137
|
+
self.opener.job = {"jobId": "job_1", "outcome": "pending", "execution": {"mode": "finite"},
|
|
138
|
+
"outputs": "complete", "cleanup": "pending", "billing": "pending"}
|
|
139
|
+
result = self.client.wait("job_1", timeout=1)
|
|
140
|
+
self.assertEqual(result["job"]["outcome"], "pending")
|
|
141
|
+
self.assertEqual(result["job"]["outputs"], "complete")
|
|
142
|
+
self.assertEqual(self.client.outputs("job_1", Path(self.folder.name) / "results")["state"],
|
|
143
|
+
"available")
|
|
144
|
+
|
|
114
145
|
|
|
115
146
|
if __name__ == "__main__":
|
|
116
147
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|