gridlinegpu 0.1.2__tar.gz → 0.1.4__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.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: gridlinegpu
3
+ Version: 0.1.4
4
+ Summary: Python client for Gridline YAML workload routing
5
+ Author: Gridline GPU
6
+ License: Proprietary
7
+ Project-URL: Documentation, https://gridlinegpu.com/cli
8
+ Project-URL: Repository, https://github.com/GridlineGPU/gridline-gpu
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: PyYAML<7,>=6.0.2
15
+
16
+ # Gridline Python SDK
17
+
18
+ Submit one YAML workload through Gridline's authenticated API. The Python client stages
19
+ files referenced by YAML, returns a route for review, then launches only that exact
20
+ fresh plan when you supply a cost ceiling and idempotency key. The package also installs
21
+ the `gridline` command in the same Python environment on macOS and Linux.
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ python -m pip install 'gridlinegpu>=0.1.4,<0.2'
27
+ ```
28
+
29
+ Run browser setup once. It creates a 30-day scoped token in a private file, attempts to
30
+ revoke the temporary browser session, and never prints the token. Keep the token file out of
31
+ source control and AI chats.
32
+
33
+ ```sh
34
+ gridline setup --apply
35
+ ```
36
+
37
+ Use a YAML file beside its referenced inputs. Task, model, engine, providers, output,
38
+ budget, and deadline belong in YAML rather than Python arguments.
39
+ Server-advertised artifact limits govern each submission. Newer Gridline deployments
40
+ use direct, signed resumable Storage uploads for large local inputs; older deployments
41
+ may still cap inputs at 40 MiB and outputs at 8 MiB.
42
+
43
+ ```python
44
+ from gridlinegpu import GridlineClient
45
+
46
+ client = GridlineClient()
47
+ plan = client.plan("workload.yaml")
48
+ print(plan.review) # Check selected GPU, exclusions, estimate, and maximum.
49
+
50
+ # Launch only after reviewing the fresh plan and accepting its maximum spend.
51
+ launch = client.launch(plan, max_cost_usd="5.00", idempotency_key="my-job-2026-09-27")
52
+ job_id = launch["jobId"]
53
+ ready = client.wait(job_id, timeout=7200)
54
+ print(ready["job"]["outputs"], ready["job"]["outcome"])
55
+ print(client.outputs(job_id, "results"))
56
+ ```
57
+
58
+ The same installed package supports a terminal flow:
59
+
60
+ ```sh
61
+ gridline workload validate --file workload.yaml
62
+ gridline workload plan --file workload.yaml
63
+ gridline workload run --file workload.yaml --apply --max-cost-usd 5 \
64
+ --idempotency-key my-job-2026-09-27
65
+ gridline workload status JOB_ID
66
+ gridline workload outputs JOB_ID --directory results
67
+ ```
68
+
69
+ The paid `run` command plans a fresh quote, prints it, and asks for explicit confirmation.
70
+ For an approved noninteractive run, add `--yes` and explicit `--profile production`
71
+ alongside `--apply`, a cost ceiling, and an idempotency key. Cancellation similarly
72
+ requires `--apply` and confirmation. A custom
73
+ token path may be passed as global `--token-file` or through `GRIDLINE_API_TOKEN_FILE`.
74
+ Workload commands also honor `GRIDLINE_API_TOKEN` when no token file is selected.
75
+ The existing standalone Bun CLI remains available during transition; check which
76
+ `gridline` executable your shell selects when both are installed. Run
77
+ `python -m gridlinegpu` to choose this package's command explicitly.
78
+ Use `gridline auth token list` to find a token ID and
79
+ `gridline auth token revoke TOKEN_ID --apply` to revoke it. Both require fresh browser
80
+ approval; the Python CLI never stores a privileged refresh token.
81
+ After revoking a token, remove its local file before running setup again; setup never
82
+ overwrites an existing file.
83
+
84
+ `plan()` stages input bytes but does not allocate a GPU. `launch()` never silently
85
+ changes a plan or retries an ambiguous paid request. If launch outcome is unknown,
86
+ inspect `LaunchOutcomeUnknown.workload_id` with the same idempotency key; do not use a
87
+ new key. `outputs()` verifies byte count and SHA-256 before creating private files.
88
+ Cancellation requests stop; cleanup and final provider billing can remain pending.
89
+ For finite jobs, `wait()` returns when outputs are complete, even if cleanup and billing
90
+ leave `outcome` pending. Check `status()` later for final settlement.
91
+
92
+ When worker progress is enabled on the hosted API/control service and supported by the
93
+ deployed worker image, `status()` also returns optional `progress`:
94
+
95
+ ```python
96
+ status = client.status(job_id)
97
+ progress = status.get("progress")
98
+ if progress:
99
+ for event in progress["events"]:
100
+ print(event["stage"], event["observedAt"])
101
+ ```
102
+
103
+ Events are bounded, ordered worker reports for `progress["attemptId"]` and its fence.
104
+ Stages include input staging, model preparation/loading, inference start/completion,
105
+ and output upload. `observedAt` is the server's receipt time. Poll no faster than every
106
+ five seconds; deduplicate by attempt/fence/stage. Missing progress or skipped stages
107
+ mean unavailable evidence, not failure or an inferred stage. Inference completion
108
+ does not imply stored outputs, job success, cleanup, or settled billing. `wait()` keeps
109
+ its existing completion behavior and does not stream progress callbacks.
@@ -0,0 +1,94 @@
1
+ # Gridline Python SDK
2
+
3
+ Submit one YAML workload through Gridline's authenticated API. The Python client stages
4
+ files referenced by YAML, returns a route for review, then launches only that exact
5
+ fresh plan when you supply a cost ceiling and idempotency key. The package also installs
6
+ the `gridline` command in the same Python environment on macOS and Linux.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ python -m pip install 'gridlinegpu>=0.1.4,<0.2'
12
+ ```
13
+
14
+ Run browser setup once. It creates a 30-day scoped token in a private file, attempts to
15
+ revoke the temporary browser session, and never prints the token. Keep the token file out of
16
+ source control and AI chats.
17
+
18
+ ```sh
19
+ gridline setup --apply
20
+ ```
21
+
22
+ Use a YAML file beside its referenced inputs. Task, model, engine, providers, output,
23
+ budget, and deadline belong in YAML rather than Python arguments.
24
+ Server-advertised artifact limits govern each submission. Newer Gridline deployments
25
+ use direct, signed resumable Storage uploads for large local inputs; older deployments
26
+ may still cap inputs at 40 MiB and outputs at 8 MiB.
27
+
28
+ ```python
29
+ from gridlinegpu import GridlineClient
30
+
31
+ client = GridlineClient()
32
+ plan = client.plan("workload.yaml")
33
+ print(plan.review) # Check selected GPU, exclusions, estimate, and maximum.
34
+
35
+ # Launch only after reviewing the fresh plan and accepting its maximum spend.
36
+ launch = client.launch(plan, max_cost_usd="5.00", idempotency_key="my-job-2026-09-27")
37
+ job_id = launch["jobId"]
38
+ ready = client.wait(job_id, timeout=7200)
39
+ print(ready["job"]["outputs"], ready["job"]["outcome"])
40
+ print(client.outputs(job_id, "results"))
41
+ ```
42
+
43
+ The same installed package supports a terminal flow:
44
+
45
+ ```sh
46
+ gridline workload validate --file workload.yaml
47
+ gridline workload plan --file workload.yaml
48
+ gridline workload run --file workload.yaml --apply --max-cost-usd 5 \
49
+ --idempotency-key my-job-2026-09-27
50
+ gridline workload status JOB_ID
51
+ gridline workload outputs JOB_ID --directory results
52
+ ```
53
+
54
+ The paid `run` command plans a fresh quote, prints it, and asks for explicit confirmation.
55
+ For an approved noninteractive run, add `--yes` and explicit `--profile production`
56
+ alongside `--apply`, a cost ceiling, and an idempotency key. Cancellation similarly
57
+ requires `--apply` and confirmation. A custom
58
+ token path may be passed as global `--token-file` or through `GRIDLINE_API_TOKEN_FILE`.
59
+ Workload commands also honor `GRIDLINE_API_TOKEN` when no token file is selected.
60
+ The existing standalone Bun CLI remains available during transition; check which
61
+ `gridline` executable your shell selects when both are installed. Run
62
+ `python -m gridlinegpu` to choose this package's command explicitly.
63
+ Use `gridline auth token list` to find a token ID and
64
+ `gridline auth token revoke TOKEN_ID --apply` to revoke it. Both require fresh browser
65
+ approval; the Python CLI never stores a privileged refresh token.
66
+ After revoking a token, remove its local file before running setup again; setup never
67
+ overwrites an existing file.
68
+
69
+ `plan()` stages input bytes but does not allocate a GPU. `launch()` never silently
70
+ changes a plan or retries an ambiguous paid request. If launch outcome is unknown,
71
+ inspect `LaunchOutcomeUnknown.workload_id` with the same idempotency key; do not use a
72
+ new key. `outputs()` verifies byte count and SHA-256 before creating private files.
73
+ Cancellation requests stop; cleanup and final provider billing can remain pending.
74
+ For finite jobs, `wait()` returns when outputs are complete, even if cleanup and billing
75
+ leave `outcome` pending. Check `status()` later for final settlement.
76
+
77
+ When worker progress is enabled on the hosted API/control service and supported by the
78
+ deployed worker image, `status()` also returns optional `progress`:
79
+
80
+ ```python
81
+ status = client.status(job_id)
82
+ progress = status.get("progress")
83
+ if progress:
84
+ for event in progress["events"]:
85
+ print(event["stage"], event["observedAt"])
86
+ ```
87
+
88
+ Events are bounded, ordered worker reports for `progress["attemptId"]` and its fence.
89
+ Stages include input staging, model preparation/loading, inference start/completion,
90
+ and output upload. `observedAt` is the server's receipt time. Poll no faster than every
91
+ five seconds; deduplicate by attempt/fence/stage. Missing progress or skipped stages
92
+ mean unavailable evidence, not failure or an inferred stage. Inference completion
93
+ does not imply stored outputs, job success, cleanup, or settled billing. `wait()` keeps
94
+ its existing completion behavior and does not stream progress callbacks.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gridlinegpu"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Python client for Gridline YAML workload routing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -17,6 +17,9 @@ classifiers = [
17
17
  "Operating System :: OS Independent",
18
18
  ]
19
19
 
20
+ [project.scripts]
21
+ gridline = "gridlinegpu.cli:main"
22
+
20
23
  [project.urls]
21
24
  Documentation = "https://gridlinegpu.com/cli"
22
25
  Repository = "https://github.com/GridlineGPU/gridline-gpu"
@@ -0,0 +1,6 @@
1
+ """Run the packaged customer CLI with ``python -m gridlinegpu``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,198 @@
1
+ """Customer workload command installed by the gridlinegpu Python package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import re
8
+ import sys
9
+ from decimal import Decimal, InvalidOperation
10
+ from importlib.metadata import PackageNotFoundError, version
11
+ from pathlib import Path
12
+ from typing import Any, Sequence, TextIO
13
+
14
+ from .cli_auth import DeviceAuth, default_token_file
15
+ from .client import GridlineClient
16
+ from .errors import GridlineError, LaunchOutcomeUnknown
17
+
18
+ _KEY = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\Z")
19
+
20
+
21
+ def _version() -> str:
22
+ try:
23
+ return version("gridlinegpu")
24
+ except PackageNotFoundError:
25
+ return "development"
26
+
27
+
28
+ def _parser() -> argparse.ArgumentParser:
29
+ parser = argparse.ArgumentParser(
30
+ prog="gridline", description="Gridline Python SDK and workload CLI")
31
+ parser.add_argument("--version", action="version", version=f"gridline Python CLI {_version()}")
32
+ parser.add_argument("--profile", choices=["production"],
33
+ help="Explicit production target, required with --yes")
34
+ parser.add_argument("--token-file", type=Path,
35
+ help="Private scoped token file (default: GRIDLINE_API_TOKEN_FILE or ~/.config/gridline/python-token)")
36
+ commands = parser.add_subparsers(dest="command", required=True)
37
+
38
+ setup = commands.add_parser("setup", help="Approve browser login and create a private workload token")
39
+ setup.add_argument("--output-file", type=Path, help="New token file; never overwrites")
40
+ setup.add_argument("--name", default="python-cli", help="Revocable token name")
41
+ setup.add_argument("--apply", action="store_true", help="Create scoped token after browser approval")
42
+
43
+ auth = commands.add_parser("auth", help="List or revoke scoped workload tokens")
44
+ auth_actions = auth.add_subparsers(dest="auth_action", required=True)
45
+ tokens = auth_actions.add_parser("token", help="Manage revocable scoped tokens")
46
+ token_actions = tokens.add_subparsers(dest="token_action", required=True)
47
+ token_actions.add_parser("list", help="List tokens after browser approval")
48
+ revoke = token_actions.add_parser("revoke", help="Revoke token after browser approval")
49
+ revoke.add_argument("token_id")
50
+ revoke.add_argument("--apply", action="store_true")
51
+
52
+ workload = commands.add_parser("workload", help="Validate, plan, launch, and inspect YAML jobs")
53
+ actions = workload.add_subparsers(dest="action", required=True)
54
+ for name, help_text in [
55
+ ("validate", "Validate YAML without allocating a GPU"),
56
+ ("plan", "Stage inputs and review a route without allocating a GPU"),
57
+ ("run", "Plan and explicitly launch the same fresh quote"),
58
+ ]:
59
+ action = actions.add_parser(name, help=help_text)
60
+ action.add_argument("--file", type=Path, required=True)
61
+ if name == "run":
62
+ action.add_argument("--max-cost-usd", required=True)
63
+ action.add_argument("--idempotency-key", required=True)
64
+ action.add_argument("--apply", action="store_true")
65
+ action.add_argument("--yes", action="store_true",
66
+ help="Confirm reviewed plan in a noninteractive terminal")
67
+ for name, help_text in [
68
+ ("status", "Read job state"),
69
+ ("wait", "Wait for job output or terminal outcome"),
70
+ ("outputs", "Download verified output files"),
71
+ ("cancel", "Request job cancellation"),
72
+ ]:
73
+ action = actions.add_parser(name, help=help_text)
74
+ action.add_argument("job_id")
75
+ if name == "outputs":
76
+ action.add_argument("--directory", type=Path, required=True)
77
+ if name == "cancel":
78
+ action.add_argument("--apply", action="store_true")
79
+ action.add_argument("--yes", action="store_true")
80
+ actions.add_parser("availability", help="Show account planning and execution gates")
81
+ return parser
82
+
83
+
84
+ def _emit(value: Any, output: TextIO) -> None:
85
+ print(json.dumps(value, indent=2, sort_keys=True, default=str), file=output)
86
+
87
+
88
+ def _warn_unmeasured_duration(review: dict[str, Any], errors: TextIO) -> None:
89
+ quote = review.get("result", {}).get("quote", {})
90
+ selected = quote.get("selected", {}) if isinstance(quote, dict) else {}
91
+ duration = selected.get("expectedDurationMs", {}) if isinstance(selected, dict) else {}
92
+ if isinstance(duration, dict) and duration.get("source") == "customer_declared":
93
+ print("Gridline: task-cost ranking uses customer-declared duration, not measured "
94
+ "GPU-specific performance; estimate is not guaranteed.", file=errors)
95
+
96
+
97
+ def _confirm(message: str, *, yes: bool, input_stream: TextIO,
98
+ output: TextIO) -> None:
99
+ if yes:
100
+ return
101
+ if not input_stream.isatty():
102
+ raise GridlineError("Interactive confirmation required; use --yes only after reviewing the plan",
103
+ code="confirmation_required")
104
+ print(f"{message} Type yes to continue: ", end="", flush=True, file=output)
105
+ if input_stream.readline().strip().lower() != "yes":
106
+ raise GridlineError("Operation was not approved", code="not_approved")
107
+
108
+
109
+ def _cost(value: str) -> Decimal:
110
+ try:
111
+ amount = Decimal(value)
112
+ except InvalidOperation as error:
113
+ raise ValueError("--max-cost-usd must be a positive amount") from error
114
+ if not amount.is_finite() or amount <= 0:
115
+ raise ValueError("--max-cost-usd must be a positive amount")
116
+ return amount
117
+
118
+
119
+ def main(argv: Sequence[str] | None = None, *, input_stream: TextIO = sys.stdin,
120
+ output: TextIO = sys.stdout, errors: TextIO = sys.stderr) -> int:
121
+ args = _parser().parse_args(argv)
122
+ token_file = args.token_file or default_token_file()
123
+ try:
124
+ if args.command == "setup":
125
+ if not args.apply:
126
+ raise GridlineError("setup creates a scoped token; pass --apply to continue",
127
+ code="apply_required")
128
+ result = DeviceAuth().setup(args.output_file or token_file,
129
+ name=args.name, announce=lambda line: print(line, file=output))
130
+ _emit(result, output)
131
+ return 0
132
+ if args.command == "auth":
133
+ auth = DeviceAuth()
134
+ announce = lambda line: print(line, file=output)
135
+ if args.token_action == "list":
136
+ result = auth.tokens(announce=announce)
137
+ else:
138
+ if not args.apply:
139
+ raise GridlineError("Token revocation requires --apply", code="apply_required")
140
+ result = auth.revoke(args.token_id, announce=announce)
141
+ _emit(result, output)
142
+ return 0
143
+
144
+ client = GridlineClient(token_file=args.token_file) if args.token_file else GridlineClient()
145
+ action = args.action
146
+ if action == "availability":
147
+ result = client.availability()
148
+ elif action == "validate":
149
+ result = client.validate(args.file)
150
+ elif action == "plan":
151
+ result = client.plan(args.file).review
152
+ _warn_unmeasured_duration(result, errors)
153
+ elif action == "run":
154
+ if not args.apply:
155
+ raise GridlineError("Paid launch requires --apply", code="apply_required")
156
+ if args.yes and args.profile != "production":
157
+ raise GridlineError("Noninteractive launch requires --profile production",
158
+ code="target_confirmation_required")
159
+ amount = _cost(args.max_cost_usd)
160
+ if not _KEY.fullmatch(args.idempotency_key):
161
+ raise ValueError("--idempotency-key must contain 1–128 safe characters")
162
+ planned = client.plan(args.file)
163
+ _emit(planned.review, output)
164
+ _warn_unmeasured_duration(planned.review, errors)
165
+ if planned.quote is None:
166
+ raise GridlineError("Router abstained; inspect plan coverage", code="plan_abstained")
167
+ _confirm(f"Launch with approved ceiling ${amount}?", yes=args.yes,
168
+ input_stream=input_stream, output=output)
169
+ result = client.launch(planned, max_cost_usd=amount,
170
+ idempotency_key=args.idempotency_key)
171
+ elif action == "status":
172
+ result = client.status(args.job_id)
173
+ elif action == "wait":
174
+ result = client.wait(args.job_id)
175
+ elif action == "outputs":
176
+ result = client.outputs(args.job_id, args.directory)
177
+ elif action == "cancel":
178
+ if not args.apply:
179
+ raise GridlineError("Cancellation requires --apply", code="apply_required")
180
+ if args.yes and args.profile != "production":
181
+ raise GridlineError("Noninteractive cancellation requires --profile production",
182
+ code="target_confirmation_required")
183
+ _confirm(f"Cancel job {args.job_id}?", yes=args.yes,
184
+ input_stream=input_stream, output=output)
185
+ result = client.cancel(args.job_id)
186
+ else:
187
+ raise ValueError("Unknown workload action")
188
+ _emit(result, output)
189
+ return 0
190
+ except LaunchOutcomeUnknown as error:
191
+ print(f"{error}. Check workload status before any retry.", file=errors)
192
+ except (GridlineError, ValueError, OSError) as error:
193
+ print(f"Gridline: {error}", file=errors)
194
+ return 1
195
+
196
+
197
+ if __name__ == "__main__":
198
+ raise SystemExit(main())