comfy-sdk 0.1.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.
Files changed (44) hide show
  1. comfy_sdk-0.1.0/.coderabbit.yaml +62 -0
  2. comfy_sdk-0.1.0/.coverage +0 -0
  3. comfy_sdk-0.1.0/.github/workflows/ci.yml +112 -0
  4. comfy_sdk-0.1.0/.github/workflows/publish.yml +157 -0
  5. comfy_sdk-0.1.0/.gitignore +5 -0
  6. comfy_sdk-0.1.0/PKG-INFO +235 -0
  7. comfy_sdk-0.1.0/README.md +212 -0
  8. comfy_sdk-0.1.0/pyproject.toml +61 -0
  9. comfy_sdk-0.1.0/scripts/check_drift.py +68 -0
  10. comfy_sdk-0.1.0/scripts/check_public_repo_hygiene.py +158 -0
  11. comfy_sdk-0.1.0/scripts/gen_models.sh +25 -0
  12. comfy_sdk-0.1.0/spec/README.md +1 -0
  13. comfy_sdk-0.1.0/spec/VERSION +1 -0
  14. comfy_sdk-0.1.0/spec/openapi.yaml +968 -0
  15. comfy_sdk-0.1.0/src/comfy_low/__init__.py +86 -0
  16. comfy_sdk-0.1.0/src/comfy_low/_multipart.py +99 -0
  17. comfy_sdk-0.1.0/src/comfy_low/errors.py +141 -0
  18. comfy_sdk-0.1.0/src/comfy_low/models/__init__.py +47 -0
  19. comfy_sdk-0.1.0/src/comfy_low/models/_generated.py +257 -0
  20. comfy_sdk-0.1.0/src/comfy_low/sse.py +70 -0
  21. comfy_sdk-0.1.0/src/comfy_low/transport.py +572 -0
  22. comfy_sdk-0.1.0/src/comfy_sdk/__init__.py +95 -0
  23. comfy_sdk-0.1.0/src/comfy_sdk/_core.py +93 -0
  24. comfy_sdk-0.1.0/src/comfy_sdk/_hashing.py +42 -0
  25. comfy_sdk-0.1.0/src/comfy_sdk/assets.py +303 -0
  26. comfy_sdk-0.1.0/src/comfy_sdk/client.py +177 -0
  27. comfy_sdk-0.1.0/src/comfy_sdk/events.py +126 -0
  28. comfy_sdk-0.1.0/src/comfy_sdk/exceptions.py +146 -0
  29. comfy_sdk-0.1.0/src/comfy_sdk/jobs.py +232 -0
  30. comfy_sdk-0.1.0/src/comfy_sdk/outputs.py +127 -0
  31. comfy_sdk-0.1.0/src/comfy_sdk/workflows.py +50 -0
  32. comfy_sdk-0.1.0/tests/conftest.py +360 -0
  33. comfy_sdk-0.1.0/tests/test_assets.py +104 -0
  34. comfy_sdk-0.1.0/tests/test_async.py +132 -0
  35. comfy_sdk-0.1.0/tests/test_auth_headers.py +34 -0
  36. comfy_sdk-0.1.0/tests/test_content_redirect_security.py +61 -0
  37. comfy_sdk-0.1.0/tests/test_download_and_workflows.py +66 -0
  38. comfy_sdk-0.1.0/tests/test_event_types.py +41 -0
  39. comfy_sdk-0.1.0/tests/test_events.py +68 -0
  40. comfy_sdk-0.1.0/tests/test_jobs.py +152 -0
  41. comfy_sdk-0.1.0/tests/test_low_decoding.py +66 -0
  42. comfy_sdk-0.1.0/tests/test_spec_coverage.py +45 -0
  43. comfy_sdk-0.1.0/tests/test_transport_security.py +45 -0
  44. comfy_sdk-0.1.0/tests/test_workflows.py +69 -0
@@ -0,0 +1,62 @@
1
+ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2
+ language: "en-US"
3
+ early_access: false
4
+ tone_instructions: |
5
+ This is a public Python SDK (ComfyPythonSDK). CI already runs
6
+ `ruff check`, `ruff format --check`, and `mypy` on every PR — do not repeat
7
+ their findings or nitpick style/formatting. Focus on public API stability
8
+ (this SDK is consumed by external users, so flag breaking changes to
9
+ public method signatures or types), correct error handling for network
10
+ calls, and clear type hints on the public surface. Only comment on issues
11
+ introduced by this PR's changes; do not flag pre-existing problems in
12
+ moved, re-indented, or reformatted code.
13
+
14
+ reviews:
15
+ profile: "assertive"
16
+ request_changes_workflow: true
17
+ high_level_summary: true
18
+ poem: false
19
+ review_status: true
20
+ commit_status: true
21
+ collapse_walkthrough: true
22
+
23
+ auto_review:
24
+ enabled: true
25
+ auto_incremental_review: true
26
+ drafts: false
27
+ ignore_title_keywords:
28
+ - "WIP"
29
+ - "DO NOT MERGE"
30
+
31
+ path_filters:
32
+ - "!**/__pycache__/**"
33
+ - "!**/*.pyc"
34
+ - "!**/*.egg-info/**"
35
+ - "!.venv/**"
36
+
37
+ path_instructions:
38
+ - path: "src/comfy_sdk/**"
39
+ instructions: |
40
+ Public SDK surface. Focus on:
41
+ - Backward compatibility of public classes/functions (this is a
42
+ published SDK; breaking a signature breaks every consumer)
43
+ - Correct error handling and clear exceptions for network/API failures
44
+ - Type hints kept accurate and exported where part of the public API
45
+ - path: "tests/**"
46
+ instructions: |
47
+ Verify tests exercise the described behavior rather than just
48
+ asserting current output (no change-detector tests).
49
+
50
+ tools:
51
+ ruff:
52
+ enabled: false
53
+ gitleaks:
54
+ enabled: true
55
+ github-checks:
56
+ enabled: true
57
+ timeout_ms: 90000
58
+ ast-grep:
59
+ essential_rules: true
60
+
61
+ chat:
62
+ auto_reply: true
Binary file
@@ -0,0 +1,112 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ test:
18
+ name: Test (py${{ matrix.python-version }})
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ['3.10', '3.11', '3.12', '3.13']
24
+ steps:
25
+ - name: Check out repository
26
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27
+
28
+ - name: Set up Python ${{ matrix.python-version }}
29
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ cache: 'pip'
33
+ cache-dependency-path: pyproject.toml
34
+
35
+ - name: Install package with dev dependencies
36
+ run: pip install -e .[dev]
37
+
38
+ - name: Lint with ruff
39
+ run: ruff check .
40
+
41
+ - name: Check formatting with ruff
42
+ run: ruff format --check .
43
+
44
+ - name: Type check with mypy
45
+ run: mypy src
46
+
47
+ - name: Run tests
48
+ run: pytest -v
49
+
50
+ codegen-drift:
51
+ name: comfy_low codegen drift
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - name: Check out repository
55
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
56
+
57
+ - name: Set up Python
58
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
59
+ with:
60
+ python-version: '3.12'
61
+ cache: 'pip'
62
+ cache-dependency-path: pyproject.toml
63
+
64
+ - name: Install codegen dependencies
65
+ run: pip install -e .[codegen]
66
+
67
+ - name: Regenerate models and fail on drift
68
+ run: python scripts/check_drift.py
69
+
70
+ # NEW -- publish dry run: catches a broken sdist/wheel in PR CI instead of
71
+ # at actual release time. publish.yml itself stays disabled (if: false);
72
+ # this job only builds and inspects the artifact, it never uploads it.
73
+ build-check:
74
+ name: build-check (publish dry run)
75
+ runs-on: ubuntu-latest
76
+ steps:
77
+ - name: Check out repository
78
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
79
+
80
+ - name: Set up Python
81
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
82
+ with:
83
+ python-version: '3.12'
84
+ cache: 'pip'
85
+ cache-dependency-path: pyproject.toml
86
+
87
+ - name: Install build tooling
88
+ run: pip install build twine
89
+
90
+ - name: Build wheel and sdist
91
+ run: python -m build
92
+
93
+ - name: Verify distribution metadata (publish dry run)
94
+ run: twine check dist/*
95
+
96
+ # NEW -- regression guard for the internal-reference leak this repo already
97
+ # had once (see scripts/check_public_repo_hygiene.py for what it looks for
98
+ # and why). Public repo, so this stays a permanent gate, not a one-time fix.
99
+ public-repo-hygiene:
100
+ name: public-repo-hygiene
101
+ runs-on: ubuntu-latest
102
+ steps:
103
+ - name: Check out repository
104
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
105
+
106
+ - name: Set up Python
107
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
108
+ with:
109
+ python-version: '3.12'
110
+
111
+ - name: Scan for internal-only references
112
+ run: python3 scripts/check_public_repo_hygiene.py
@@ -0,0 +1,157 @@
1
+ # Publishes `comfy-sdk` to PyPI.
2
+ #
3
+ # Tag-driven versioning: the release tag (vX.Y.Z) is the single source of truth
4
+ # for the published version. It is injected into pyproject.toml at build time,
5
+ # so the committed version there is just a placeholder — to release a version,
6
+ # create a GitHub Release with the tag you want; no version-bump commit needed.
7
+ #
8
+ # Trigger: a GitHub Release being published (tag vX.Y.Z). That's the only
9
+ # path that reaches the `publish` job below. `workflow_dispatch` is a dry
10
+ # run only — it exercises `build` (compile + `twine check`) but never
11
+ # reaches `publish`, so it's safe to run against any branch to sanity-check
12
+ # the pipeline before cutting a real release.
13
+ #
14
+ # Auth: PyPI Trusted Publishing (OIDC) — no PYPI_TOKEN / API token secret is
15
+ # stored in this repo. See "Maintainer setup" in the PR description / repo
16
+ # docs for the one-time PyPI-side configuration this depends on.
17
+ name: Publish to PyPI
18
+
19
+ on:
20
+ release:
21
+ types: [published]
22
+ workflow_dispatch:
23
+ inputs:
24
+ ref:
25
+ description: 'Git ref to build for a dry run (build + twine check only — this input never reaches the publish job)'
26
+ required: false
27
+ type: string
28
+ default: ''
29
+ publish_to_testpypi:
30
+ description: 'Also upload the built dist to TestPyPI (requires the optional "testpypi" environment + Trusted Publisher — see Maintainer setup)'
31
+ required: false
32
+ type: boolean
33
+ default: false
34
+
35
+ permissions:
36
+ contents: read
37
+
38
+ concurrency:
39
+ group: publish-${{ github.workflow }}-${{ github.event.release.tag_name || github.sha }}
40
+ cancel-in-progress: false
41
+
42
+ jobs:
43
+ # Runs for every trigger. Builds the exact sdist/wheel that `publish` will
44
+ # upload and validates it with twine. This IS the workflow_dispatch dry
45
+ # run: trigger this workflow manually via the Actions tab and you get this
46
+ # job (and nothing else, since `publish` requires a `release` event).
47
+ build:
48
+ name: Build and check distribution
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - name: Check out repository
52
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
53
+ with:
54
+ ref: ${{ github.event.inputs.ref || github.ref }}
55
+
56
+ - name: Set up Python
57
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
58
+ with:
59
+ python-version: '3.12'
60
+
61
+ - name: Install build tooling
62
+ run: pip install build twine
63
+
64
+ # Tag-driven versioning: the release tag (vX.Y.Z) is the source of truth.
65
+ # Inject it into pyproject.toml before building so the published artifact
66
+ # carries the tag's version. (On a workflow_dispatch dry run there is no
67
+ # release tag; the committed placeholder version is built as-is.)
68
+ - name: Set version from release tag
69
+ if: github.event_name == 'release'
70
+ shell: bash
71
+ env:
72
+ TAG: ${{ github.event.release.tag_name }}
73
+ run: |
74
+ set -euo pipefail
75
+ VERSION="${TAG#v}"
76
+ # Full SemVer 2.0: X.Y.Z, with an optional -prerelease and an
77
+ # optional +build-metadata segment (both may be present together).
78
+ if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
79
+ echo "::error::Release tag '${TAG}' is not a valid version (expected vX.Y.Z)."
80
+ exit 1
81
+ fi
82
+ python3 - "$VERSION" <<'PY'
83
+ import re, sys
84
+ version = sys.argv[1]
85
+ p = "pyproject.toml"
86
+ s = open(p, encoding="utf-8").read()
87
+ s2, n = re.subn(r'(?m)^version = "[^"]+"$', f'version = "{version}"', s, count=1)
88
+ if n != 1:
89
+ raise SystemExit('could not find a top-level version = "..." line in pyproject.toml')
90
+ open(p, "w", encoding="utf-8").write(s2)
91
+ print(f"Set pyproject.toml version = {version}")
92
+ PY
93
+
94
+ - name: Build wheel and sdist
95
+ run: python -m build
96
+
97
+ - name: Verify distribution metadata (twine check)
98
+ run: twine check dist/*
99
+
100
+ - name: Upload built distribution
101
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
102
+ with:
103
+ name: pypi-dist
104
+ path: dist/
105
+ retention-days: 7
106
+
107
+ publish:
108
+ name: Publish to PyPI
109
+ needs: [build]
110
+ if: github.event_name == 'release'
111
+ runs-on: ubuntu-latest
112
+ # Manual approval gate: create this environment in
113
+ # Settings -> Environments with required reviewers, so every publish
114
+ # needs a human click even though the trigger (release published) is
115
+ # automatic. See "Maintainer setup" for the one-time steps.
116
+ environment: pypi
117
+ permissions:
118
+ id-token: write # OIDC for PyPI Trusted Publishing — no API token secret
119
+ contents: read
120
+ steps:
121
+ - name: Download built distribution
122
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
123
+ with:
124
+ name: pypi-dist
125
+ path: dist/
126
+
127
+ - name: Publish to PyPI
128
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
129
+ with:
130
+ packages-dir: dist/
131
+ skip-existing: true # idempotent re-runs: no-op instead of erroring if this version is already up
132
+
133
+ # Nice-to-have: exercise the full OIDC publish path against TestPyPI
134
+ # without touching real PyPI. Opt-in via workflow_dispatch input; needs
135
+ # its own Trusted Publisher + "testpypi" environment (see Maintainer setup).
136
+ publish-testpypi:
137
+ name: Publish to TestPyPI (dry run)
138
+ needs: [build]
139
+ if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish_to_testpypi == 'true'
140
+ runs-on: ubuntu-latest
141
+ environment: testpypi
142
+ permissions:
143
+ id-token: write
144
+ contents: read
145
+ steps:
146
+ - name: Download built distribution
147
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
148
+ with:
149
+ name: pypi-dist
150
+ path: dist/
151
+
152
+ - name: Publish to TestPyPI
153
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
154
+ with:
155
+ packages-dir: dist/
156
+ repository-url: https://test.pypi.org/legacy/
157
+ skip-existing: true
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ *.egg-info/
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: comfy-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for running ComfyUI workflows via the Comfy API v2 (self-hosted, Comfy Cloud, serverless).
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: blake3>=0.4
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: pydantic>=2.6
9
+ Provides-Extra: codegen
10
+ Requires-Dist: datamodel-code-generator~=0.68.1; extra == 'codegen'
11
+ Requires-Dist: pyyaml>=6; extra == 'codegen'
12
+ Provides-Extra: dev
13
+ Requires-Dist: datamodel-code-generator~=0.68.1; extra == 'dev'
14
+ Requires-Dist: mypy~=2.3.0; extra == 'dev'
15
+ Requires-Dist: pillow>=10; extra == 'dev'
16
+ Requires-Dist: pytest-asyncio~=1.4.0; extra == 'dev'
17
+ Requires-Dist: pytest~=9.1.1; extra == 'dev'
18
+ Requires-Dist: pyyaml>=6; extra == 'dev'
19
+ Requires-Dist: ruff~=0.15.22; extra == 'dev'
20
+ Provides-Extra: pil
21
+ Requires-Dist: pillow>=10; extra == 'pil'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # comfy-sdk (Python)
25
+
26
+ Python SDK for running ComfyUI workflows via the **Comfy API v2**. The same
27
+ code runs against a self-hosted ComfyUI, Comfy Cloud, or a serverless
28
+ deployment — only the base URL and an optional API key change.
29
+
30
+ ```python
31
+ from comfy_sdk import Comfy
32
+
33
+ client = Comfy("http://127.0.0.1:8189") # self-hosted, no key
34
+ # client = Comfy("https://api.comfy.org", api_key="ck_...") # Comfy Cloud
35
+
36
+ wf = client.workflows.from_file("workflow_api.json")
37
+
38
+ # Lazy asset handle: hashed locally with blake3, deduped against the server's
39
+ # fast-path (mint over existing bytes), or streamed-uploaded on a miss — then
40
+ # substituted into the graph as a core/ASSET reference.
41
+ asset = client.assets.from_file("photo.png")
42
+ wf.set_input("10", "image", asset)
43
+
44
+ job = client.run(wf) # submit, then poll to a terminal state
45
+ job.get_outputs("13")[0].to_file("out.png")
46
+ ```
47
+
48
+ ## Requirements and install
49
+
50
+ Requires **Python 3.10+**. Dependencies: `httpx`, `blake3`, `pydantic` (v2).
51
+
52
+ ```bash
53
+ pip install comfy-sdk
54
+ ```
55
+
56
+ Releases are published to PyPI from a GitHub Release (tag `vX.Y.Z`) by
57
+ [`.github/workflows/publish.yml`](.github/workflows/publish.yml), using
58
+ PyPI's Trusted Publishing (OIDC) — no API token is stored in this repo.
59
+
60
+ To install from source instead (for local development, or to track an
61
+ unreleased commit):
62
+
63
+ ```bash
64
+ git clone https://github.com/Comfy-Org/ComfyPythonSDK
65
+ cd ComfyPythonSDK
66
+ pip install -e .
67
+ # with everything needed to lint/type-check/test locally:
68
+ pip install -e ".[dev]"
69
+ ```
70
+
71
+ `Preview.to_pil()` (decoding an in-progress SSE preview frame to a `PIL.Image`)
72
+ needs the optional `pil` extra: `pip install -e ".[pil]"`.
73
+
74
+ ## Authentication — one client, per-surface key
75
+
76
+ | Surface | Example base URL | `api_key` |
77
+ |---|---|---|
78
+ | Self-hosted ComfyUI (behind the API proxy) | `http://127.0.0.1:8189` | Omit — no key is sent, even implicitly |
79
+ | Comfy Cloud | `https://api.comfy.org` | Required |
80
+ | Serverless deployment | `https://<deployment>.comfy.org` | Required |
81
+
82
+ ```python
83
+ client = Comfy("http://127.0.0.1:8189") # self-hosted
84
+ client = Comfy("https://api.comfy.org", api_key="ck_...") # Comfy Cloud / serverless
85
+ ```
86
+
87
+ `AsyncComfy` takes the same two arguments. A key is only ever attached to
88
+ requests aimed at the configured `base_url`'s own origin — a server-returned
89
+ follow-up link (`job.urls.self`/`cancel`/`events`, or a redirected asset
90
+ download) pointing anywhere else never receives it.
91
+
92
+ ## Assets and `core/ASSET`
93
+
94
+ `client.assets.from_file(...)` / `from_bytes(...)` / `from_stream(...)` /
95
+ `from_url(...)` return a **lazy** asset handle immediately — no network call
96
+ yet. Embed it directly into the workflow graph:
97
+
98
+ ```python
99
+ asset = client.assets.from_file("photo.png")
100
+ wf.set_input("10", "image", asset)
101
+ ```
102
+
103
+ On first use (submitting the workflow, or an explicit `asset.commit()`), the
104
+ SDK:
105
+
106
+ 1. hashes the bytes locally with blake3;
107
+ 2. probes the server's dedup fast-path — a `HEAD` existence check by hash,
108
+ then a cheap `from-hash` mint if the server already has those bytes;
109
+ 3. only streams a full multipart upload on a miss.
110
+
111
+ At submit time, every asset handle found anywhere in the graph is replaced by
112
+ a `core/ASSET` reference object (`{"__type": "core/ASSET", "info": {"id":
113
+ ..., "hash": ..., "file_path": ...}}`), which the server resolves back to the
114
+ uploaded asset when it runs the workflow.
115
+
116
+ ## Live progress
117
+
118
+ ```python
119
+ job = client.submit(wf)
120
+ for event in job.events(): # SSE; live, auto-reconnecting (no replay)
121
+ match event:
122
+ case Progress() as p: print(f"{p.value:.0%} {p.message}")
123
+ case Preview() as pv: show(pv.to_pil())
124
+ case OutputReady() as o: o.output.to_file(f"partial/{o.output.name}")
125
+ case StatusChange(status="succeeded"): break
126
+ result = job.result() # raises JobFailed with node details on failure
127
+ ```
128
+
129
+ `job.events()` reconnects automatically if the stream drops, but never
130
+ replays a frame you've already seen (the stream carries no cursor). That's
131
+ why polling stays authoritative: `job.wait()` / `job.result()` (and
132
+ `client.run()`, which is `submit()` + `result()`) always fall back to
133
+ `GET /jobs/{id}` to decide when a job is really done — use `events()` for
134
+ live UI feedback, and `wait()`/`result()`/`run()` for the definitive answer.
135
+ `job.status` is the current status string; `job.outputs` is the full list of
136
+ output handles regardless of which node produced them (`job.get_outputs(node_id)`
137
+ filters to one node, as in the quickstart above).
138
+
139
+ ## Sync and async
140
+
141
+ `Comfy` and `AsyncComfy` expose the identical surface — swap the import and
142
+ add `await` / `async for`:
143
+
144
+ ```python
145
+ from comfy_sdk import AsyncComfy
146
+
147
+ async def main() -> None:
148
+ async with AsyncComfy("http://127.0.0.1:8189") as client:
149
+ wf = client.workflows.from_file("workflow_api.json")
150
+ job = await client.run(wf)
151
+ await job.outputs[0].to_file("out.png")
152
+ ```
153
+
154
+ ## Typed errors
155
+
156
+ `comfy_sdk` translates the API's error envelope into a small set of
157
+ exceptions, all importable from the top-level package and all subclasses of
158
+ `ComfyError`:
159
+
160
+ - `Unauthorized`, `Forbidden`, `NotFound` — auth and lookup failures.
161
+ - `InvalidWorkflow`, `WorkflowFormatUi` — the graph itself was rejected;
162
+ `WorkflowFormatUi` specifically means a UI-export (`nodes`/`links`/
163
+ `last_node_id`) was submitted instead of the API-format graph — the SDK
164
+ catches this locally before it ever reaches the server.
165
+ - `MissingAsset` — a `core/ASSET` reference could not be resolved.
166
+ - `HashMismatch`, `BlobNotFound` — asset upload/dedup failures.
167
+ - `IdempotencyKeyReuse` — the `Idempotency-Key` was reused. `submit()` (and
168
+ `run()`) attach a fresh key to every call, so an accidental exact resend never
169
+ runs the workflow twice. Keys are single-use — reject-on-duplicate, there is
170
+ no replay — so if you pass your own `idempotency_key=` and reuse it, the second
171
+ call raises this. After an ambiguous failure (e.g. a timeout where you don't
172
+ know if the job was created), poll or list your jobs rather than resubmitting
173
+ with the same key.
174
+ - `InsufficientCredits` — the account can't afford the job.
175
+ - `QueueFull` — backpressure; carries `.retry_after` seconds. `client.submit`
176
+ already retries this automatically for a bounded budget before giving up
177
+ and raising it.
178
+ - `JobFailed` — a job reached a non-`succeeded` terminal state; `.error`
179
+ carries node-level detail when the platform provided one.
180
+
181
+ ```python
182
+ from comfy_sdk import JobFailed, QueueFull, Unauthorized
183
+
184
+ try:
185
+ result = client.run(wf)
186
+ except JobFailed as e:
187
+ print(e.error)
188
+ except Unauthorized:
189
+ print("check your api_key")
190
+ ```
191
+
192
+ ## Architecture — two layers
193
+
194
+ * **`comfy_low`** — generated protocol bindings. Pydantic v2 models generated
195
+ from `spec/openapi.yaml` (`src/comfy_low/models/_generated.py`, committed;
196
+ regenerate with `scripts/gen_models.sh`, CI fails on drift) plus a thin
197
+ hand-written `httpx` transport (sync + async), one function per `operationId`,
198
+ with the mandatory escape hatches: raw response access, unbuffered/streaming
199
+ bodies, all headers, and per-request timeout/abort. Boring and replaceable.
200
+
201
+ * **`comfy_sdk`** — the idiomatic layer integrators import. This is where the
202
+ value lives: blake3 content-addressed dedup-upload, `core/ASSET`
203
+ substitution, idempotent submit, live SSE with reconnect, poll-authoritative
204
+ `run()`, range-aware downloads, and typed exceptions mapping the error
205
+ envelope.
206
+
207
+ `spec/openapi.yaml` is a one-way vendored copy of the canonical Comfy API v2
208
+ contract — do not hand-edit it (see `spec/README.md`). It's synced
209
+ periodically from that canonical contract, stripped of anything tagged
210
+ `internal`, and pinned by `spec/VERSION`.
211
+
212
+ ## Related projects
213
+
214
+ Part of the same SDK family: a TypeScript client with the equivalent surface
215
+ for JS/Node integrators, and the local API proxy that fronts a self-hosted
216
+ ComfyUI instance with this same v2 contract (`comfy-api-proxy` in the
217
+ `servers` list of `spec/openapi.yaml`).
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ pip install -e ".[dev]"
223
+ ruff check .
224
+ ruff format --check .
225
+ mypy src
226
+ pytest -v
227
+ ```
228
+
229
+ Regenerating and checking the vendored protocol layer (a separate CI job):
230
+
231
+ ```bash
232
+ pip install -e ".[codegen]"
233
+ python scripts/gen_models.sh # regenerate comfy_low models from spec/openapi.yaml
234
+ python scripts/check_drift.py # same check CI runs; fails if committed models drifted
235
+ ```