commoncompute 0.3.0__tar.gz → 0.3.1__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.
- {commoncompute-0.3.0 → commoncompute-0.3.1}/.gitignore +1 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/LICENSE +1 -1
- {commoncompute-0.3.0 → commoncompute-0.3.1}/PKG-INFO +2 -2
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/__init__.py +1 -1
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_async_client.py +3 -1
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_generated_workloads.py +7 -4
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/aws_batch.py +5 -6
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/cli.py +38 -7
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/client.py +19 -1
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/tasks.py +27 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/pyproject.toml +1 -1
- {commoncompute-0.3.0 → commoncompute-0.3.1}/README.md +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_async_tasks.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_config.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_transport.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/compat/__init__.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/compat/openai.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/connect.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/errors.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/models.py +0 -0
- {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/py.typed +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: commoncompute
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Official Python SDK for Common Compute — the batch AI bill you shouldn't be paying.
|
|
5
5
|
Project-URL: Homepage, https://commoncompute.ai
|
|
6
6
|
Project-URL: Documentation, https://commoncompute.ai/docs
|
|
7
|
-
Project-URL: Source, https://github.com/
|
|
7
|
+
Project-URL: Source, https://github.com/commoncompute/sdk
|
|
8
8
|
Project-URL: Changelog, https://commoncompute.ai/changelog
|
|
9
9
|
Author-email: Common Compute <support@commoncompute.ai>
|
|
10
10
|
License: MIT
|
|
@@ -30,7 +30,7 @@ in the catalog — the API declines them with `workload_not_available`
|
|
|
30
30
|
# `commoncompute.__version__` at import time for the User-Agent header,
|
|
31
31
|
# and the previous ordering (`__version__` defined at the bottom) made
|
|
32
32
|
# every clean install of the SDK fail with a circular ImportError.
|
|
33
|
-
__version__ = "0.3.
|
|
33
|
+
__version__ = "0.3.1"
|
|
34
34
|
|
|
35
35
|
from .client import Client, AsyncClient
|
|
36
36
|
from .errors import (
|
|
@@ -278,8 +278,10 @@ class _KeysAsync:
|
|
|
278
278
|
async def list(self) -> list[dict[str, Any]]:
|
|
279
279
|
return (await self._t.request("GET", "/v1/me/keys")).get("keys", [])
|
|
280
280
|
|
|
281
|
-
async def create(self, *, name: str, mode: str = "live", scopes: "Optional[Sequence[str]]" = None) -> dict[str, Any]:
|
|
281
|
+
async def create(self, *, name: str, mode: str = "live", scopes: "Optional[Sequence[str]]" = None, password: "Optional[str]" = None) -> dict[str, Any]:
|
|
282
282
|
body: dict[str, Any] = {"name": name, "mode": mode}
|
|
283
|
+
if password:
|
|
284
|
+
body["password"] = password
|
|
283
285
|
if scopes:
|
|
284
286
|
body["scopes"] = scopes
|
|
285
287
|
return await self._t.request("POST", "/v1/me/keys", json_body=body)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
# @generated
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# @generated from Common Compute's internal workload catalog — DO NOT EDIT.
|
|
2
|
+
#
|
|
3
|
+
# This file is emitted by an internal codegen step and is not regenerable from
|
|
4
|
+
# this repository. Edits here will be overwritten; the catalog is the source of
|
|
5
|
+
# truth and drift is caught before release.
|
|
6
|
+
#
|
|
7
|
+
# Contains workload IDs and image-name routing heuristics only.
|
|
5
8
|
from __future__ import annotations
|
|
6
9
|
|
|
7
10
|
# Every workload id in the catalog. Used to decide whether a jobDefinition head
|
|
@@ -103,11 +103,10 @@ def _to_iso(ms: Optional[int]) -> Optional[str]:
|
|
|
103
103
|
# Heuristic: anything containing one of these workload ids gets routed
|
|
104
104
|
# directly. Otherwise we default to ``coreml_embed`` (the most common
|
|
105
105
|
# batch workload migrated from AWS Batch + bge-* embeddings pipelines).
|
|
106
|
-
# Workload identity comes from the generated catalog mirror
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# npx tsx packages/workloads/codegen/emit-sdk-workloads.ts
|
|
106
|
+
# Workload identity comes from the generated catalog mirror
|
|
107
|
+
# (``_generated_workloads.py``) so this shim resolves jobDefinitions
|
|
108
|
+
# identically to the server side and to the Node SDK, instead of drifting
|
|
109
|
+
# from a hand-maintained list.
|
|
111
110
|
from ._generated_workloads import IMAGE_PATTERNS as _IMAGE_PATTERN_SOURCES
|
|
112
111
|
from ._generated_workloads import KNOWN_WORKLOAD_IDS as _KNOWN_WORKLOADS
|
|
113
112
|
|
|
@@ -403,7 +402,7 @@ class BatchClient:
|
|
|
403
402
|
"""Mirror AWS describe_jobs — returns ``{"jobs": [...]}``.
|
|
404
403
|
|
|
405
404
|
Just-submitted jobs may not be queryable for ~100ms while the
|
|
406
|
-
underlying
|
|
405
|
+
underlying row propagates across the backing datastore's read replicas.
|
|
407
406
|
We absorb a single 404 by retrying once after 250ms, which is
|
|
408
407
|
enough to mask the eventual-consistency window in practice."""
|
|
409
408
|
out: list[dict[str, Any]] = []
|
|
@@ -435,10 +435,46 @@ def _wait_job(client: Client, job_id: str, timeout: float, *, quiet: bool):
|
|
|
435
435
|
return client.wait(job_id, timeout=timeout, on_progress=_tick)
|
|
436
436
|
|
|
437
437
|
|
|
438
|
+
def _parse_payload_arg(payload: str) -> object:
|
|
439
|
+
"""Resolve --payload: @- (stdin), @path or bare path (file), or inline JSON.
|
|
440
|
+
|
|
441
|
+
The help has always said "path to JSON payload file", but the code only
|
|
442
|
+
honoured that with an @ prefix — a bare path fell through to json.loads()
|
|
443
|
+
and died with a raw JSONDecodeError traceback. Accept the documented
|
|
444
|
+
form, keep both @ forms, and turn every failure into a clean CLI error.
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
if payload == "@-":
|
|
448
|
+
return json.load(sys.stdin)
|
|
449
|
+
if payload.startswith("@"):
|
|
450
|
+
return json.loads(Path(payload[1:]).read_text())
|
|
451
|
+
p = Path(payload)
|
|
452
|
+
if p.exists():
|
|
453
|
+
return json.loads(p.read_text())
|
|
454
|
+
# Not a file on disk — treat as inline JSON. A path-like string that
|
|
455
|
+
# simply doesn't exist gets a hint instead of a JSON parse error.
|
|
456
|
+
try:
|
|
457
|
+
return json.loads(payload)
|
|
458
|
+
except json.JSONDecodeError:
|
|
459
|
+
if payload.endswith(".json") or "/" in payload:
|
|
460
|
+
err_console.print(f"[red]Error:[/red] payload file not found: {payload}")
|
|
461
|
+
else:
|
|
462
|
+
err_console.print(f"[red]Error:[/red] --payload is not a file, not valid inline JSON: {payload!r}")
|
|
463
|
+
raise typer.Exit(2)
|
|
464
|
+
except typer.Exit:
|
|
465
|
+
raise
|
|
466
|
+
except json.JSONDecodeError as e:
|
|
467
|
+
err_console.print(f"[red]Error:[/red] invalid JSON in payload ({e.msg} at line {e.lineno}, column {e.colno})")
|
|
468
|
+
raise typer.Exit(2)
|
|
469
|
+
except OSError as e:
|
|
470
|
+
err_console.print(f"[red]Error:[/red] cannot read payload: {e}")
|
|
471
|
+
raise typer.Exit(2)
|
|
472
|
+
|
|
473
|
+
|
|
438
474
|
@app.command()
|
|
439
475
|
def submit(
|
|
440
476
|
workload: str = typer.Argument(..., help="Workload id."),
|
|
441
|
-
payload: Optional[str] = typer.Option(None, "--payload", "-f", help="Path to JSON payload file,
|
|
477
|
+
payload: Optional[str] = typer.Option(None, "--payload", "-f", help="Path to a JSON payload file, @- for stdin, or an inline JSON string."),
|
|
442
478
|
model: Optional[str] = typer.Option(None, "--model", "-m"),
|
|
443
479
|
priority: str = typer.Option("standard", "--priority", "-p"),
|
|
444
480
|
wait: bool = typer.Option(False, "--wait", help="Block until the job finishes."),
|
|
@@ -448,12 +484,7 @@ def submit(
|
|
|
448
484
|
"""Submit a native Common Compute job."""
|
|
449
485
|
body: object = None
|
|
450
486
|
if payload:
|
|
451
|
-
|
|
452
|
-
body = json.load(sys.stdin)
|
|
453
|
-
elif payload.startswith("@"):
|
|
454
|
-
body = json.loads(Path(payload[1:]).read_text())
|
|
455
|
-
else:
|
|
456
|
-
body = json.loads(payload)
|
|
487
|
+
body = _parse_payload_arg(payload)
|
|
457
488
|
c = _client()
|
|
458
489
|
try:
|
|
459
490
|
job = c.jobs.submit(workload, payload=body, model_id=model, priority=priority)
|
|
@@ -263,10 +263,28 @@ class _KeysSync:
|
|
|
263
263
|
def list(self) -> list[dict[str, Any]]:
|
|
264
264
|
return self._t.request("GET", "/v1/me/keys").get("keys", [])
|
|
265
265
|
|
|
266
|
-
def create(
|
|
266
|
+
def create(
|
|
267
|
+
self,
|
|
268
|
+
*,
|
|
269
|
+
name: str,
|
|
270
|
+
mode: str = "live",
|
|
271
|
+
scopes: "Optional[Sequence[str]]" = None,
|
|
272
|
+
password: "Optional[str]" = None,
|
|
273
|
+
) -> dict[str, Any]:
|
|
274
|
+
"""Create an API key.
|
|
275
|
+
|
|
276
|
+
Requires a signed-in *session* plus the account password. An API key
|
|
277
|
+
cannot create another API key (the server returns 403
|
|
278
|
+
``api_key_cannot_mint``) — otherwise revoking a leaked key would not
|
|
279
|
+
revoke the keys it had spawned. Since this SDK normally authenticates
|
|
280
|
+
with an API key, most callers should create keys in the dashboard at
|
|
281
|
+
/app/api-keys instead.
|
|
282
|
+
"""
|
|
267
283
|
body: dict[str, Any] = {"name": name, "mode": mode}
|
|
268
284
|
if scopes:
|
|
269
285
|
body["scopes"] = scopes
|
|
286
|
+
if password:
|
|
287
|
+
body["password"] = password
|
|
270
288
|
return self._t.request("POST", "/v1/me/keys", json_body=body)
|
|
271
289
|
|
|
272
290
|
def revoke(self, key_id: str) -> None:
|
|
@@ -27,12 +27,31 @@ from .models import Job, Quote, Receipt, TaskResult
|
|
|
27
27
|
|
|
28
28
|
FileInput = Union[str, Path, bytes, io.BufferedReader]
|
|
29
29
|
|
|
30
|
+
# Schemes recognised as "this is a URI, not a local path". Only https:// can
|
|
31
|
+
# actually be fetched: the provider's download gate hard-rejects every other
|
|
32
|
+
# scheme (TrustedDownload.swift — "only https"), and http:// specifically is
|
|
33
|
+
# refused because a job input must not travel in the clear.
|
|
34
|
+
#
|
|
35
|
+
# The others stay in this tuple ON PURPOSE. Dropping them would make an s3://
|
|
36
|
+
# URI fall through to the local-path branch and raise a confusing
|
|
37
|
+
# "No such file: s3://bucket/key"; keeping them lets _file_payload reject the
|
|
38
|
+
# scheme by name, at submit time, instead of the job dispatching and failing
|
|
39
|
+
# on someone's Mac minutes later (2026-08-01 audit — the SDKs documented all
|
|
40
|
+
# five schemes and only one ever worked).
|
|
30
41
|
_URI_SCHEMES = ("http://", "https://", "s3://", "r2://", "gs://")
|
|
42
|
+
_FETCHABLE_URI_SCHEMES = ("https://",)
|
|
31
43
|
|
|
32
44
|
|
|
33
45
|
def _file_payload(f: FileInput, *, allowed_suffixes: Optional[Sequence[str]] = None) -> dict:
|
|
34
46
|
"""Normalise a file input into the job-payload contract."""
|
|
35
47
|
if isinstance(f, str) and f.startswith(_URI_SCHEMES):
|
|
48
|
+
if not f.startswith(_FETCHABLE_URI_SCHEMES):
|
|
49
|
+
scheme = f.split("://", 1)[0]
|
|
50
|
+
raise UnsupportedFormatError(
|
|
51
|
+
f"{scheme}:// inputs are not supported — providers can only fetch https:// URLs. "
|
|
52
|
+
f"Pass a local path (the file is uploaded for you) or a presigned https:// URL.",
|
|
53
|
+
code="unsupported_input_scheme",
|
|
54
|
+
)
|
|
36
55
|
return {"input_uri": f}
|
|
37
56
|
if isinstance(f, (str, Path)):
|
|
38
57
|
p = Path(f)
|
|
@@ -367,6 +386,14 @@ class Video(_TaskBase):
|
|
|
367
386
|
max_spend_usd: Optional[float] = None,
|
|
368
387
|
dry_run: bool = False,
|
|
369
388
|
) -> Union[Job, Quote]:
|
|
389
|
+
"""Transcode a video with VideoToolbox hardware encoders.
|
|
390
|
+
|
|
391
|
+
``output_format`` selects the codec (``"h264"`` / ``"hevc"`` /
|
|
392
|
+
``"prores"``); ``codec=`` overrides it if both are given.
|
|
393
|
+
``resolution`` is not yet implemented server-side — a job that sets
|
|
394
|
+
it fails fast with a clear error instead of silently encoding at
|
|
395
|
+
the source's native size.
|
|
396
|
+
"""
|
|
370
397
|
workload_id, payload, units, model_id = _transcode_request(
|
|
371
398
|
input, output_format, resolution=resolution, codec=codec, output_minutes=output_minutes,
|
|
372
399
|
)
|
|
@@ -38,7 +38,7 @@ dependencies = [
|
|
|
38
38
|
[project.urls]
|
|
39
39
|
Homepage = "https://commoncompute.ai"
|
|
40
40
|
Documentation = "https://commoncompute.ai/docs"
|
|
41
|
-
Source = "https://github.com/
|
|
41
|
+
Source = "https://github.com/commoncompute/sdk"
|
|
42
42
|
Changelog = "https://commoncompute.ai/changelog"
|
|
43
43
|
|
|
44
44
|
[project.scripts]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|