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.
Files changed (21) hide show
  1. {commoncompute-0.3.0 → commoncompute-0.3.1}/.gitignore +1 -0
  2. {commoncompute-0.3.0 → commoncompute-0.3.1}/LICENSE +1 -1
  3. {commoncompute-0.3.0 → commoncompute-0.3.1}/PKG-INFO +2 -2
  4. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/__init__.py +1 -1
  5. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_async_client.py +3 -1
  6. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_generated_workloads.py +7 -4
  7. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/aws_batch.py +5 -6
  8. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/cli.py +38 -7
  9. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/client.py +19 -1
  10. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/tasks.py +27 -0
  11. {commoncompute-0.3.0 → commoncompute-0.3.1}/pyproject.toml +1 -1
  12. {commoncompute-0.3.0 → commoncompute-0.3.1}/README.md +0 -0
  13. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_async_tasks.py +0 -0
  14. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_config.py +0 -0
  15. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/_transport.py +0 -0
  16. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/compat/__init__.py +0 -0
  17. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/compat/openai.py +0 -0
  18. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/connect.py +0 -0
  19. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/errors.py +0 -0
  20. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/models.py +0 -0
  21. {commoncompute-0.3.0 → commoncompute-0.3.1}/commoncompute/py.typed +0 -0
@@ -3,6 +3,7 @@ node_modules/
3
3
  dist/
4
4
  coverage/
5
5
  .wrangler/
6
+ .wrangler.drifted-backup-*/
6
7
  .pytest_cache/
7
8
  __pycache__/
8
9
  *.pyc
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Common Compute, Inc.
3
+ Copyright (c) 2026 Common Compute LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commoncompute
3
- Version: 0.3.0
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/Ikaikaalika/commoncomputeai
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.0"
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 by packages/workloads/codegen/emit-sdk-workloads.ts — DO NOT EDIT.
2
- # Regenerate: npx tsx packages/workloads/codegen/emit-sdk-workloads.ts
3
- # Source of truth: packages/workloads (the shared catalog). CI fails if this
4
- # file drifts from the catalog (see tests/unit/workloads-parity.test.ts).
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 so this shim
107
- # resolves jobDefinitions identically to the wire shim (apps/aws-shim) and the
108
- # Node SDK. The previous hand-maintained set had drifted to 12 of the catalog's
109
- # 24 workloads. Regenerate with:
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 D1 row propagates across the worker's read replicas.
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, or @- for stdin."),
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
- if payload == "@-":
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(self, *, name: str, mode: str = "live", scopes: "Optional[Sequence[str]]" = None) -> dict[str, Any]:
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/Ikaikaalika/commoncomputeai"
41
+ Source = "https://github.com/commoncompute/sdk"
42
42
  Changelog = "https://commoncompute.ai/changelog"
43
43
 
44
44
  [project.scripts]
File without changes