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.
- comfy_sdk-0.1.0/.coderabbit.yaml +62 -0
- comfy_sdk-0.1.0/.coverage +0 -0
- comfy_sdk-0.1.0/.github/workflows/ci.yml +112 -0
- comfy_sdk-0.1.0/.github/workflows/publish.yml +157 -0
- comfy_sdk-0.1.0/.gitignore +5 -0
- comfy_sdk-0.1.0/PKG-INFO +235 -0
- comfy_sdk-0.1.0/README.md +212 -0
- comfy_sdk-0.1.0/pyproject.toml +61 -0
- comfy_sdk-0.1.0/scripts/check_drift.py +68 -0
- comfy_sdk-0.1.0/scripts/check_public_repo_hygiene.py +158 -0
- comfy_sdk-0.1.0/scripts/gen_models.sh +25 -0
- comfy_sdk-0.1.0/spec/README.md +1 -0
- comfy_sdk-0.1.0/spec/VERSION +1 -0
- comfy_sdk-0.1.0/spec/openapi.yaml +968 -0
- comfy_sdk-0.1.0/src/comfy_low/__init__.py +86 -0
- comfy_sdk-0.1.0/src/comfy_low/_multipart.py +99 -0
- comfy_sdk-0.1.0/src/comfy_low/errors.py +141 -0
- comfy_sdk-0.1.0/src/comfy_low/models/__init__.py +47 -0
- comfy_sdk-0.1.0/src/comfy_low/models/_generated.py +257 -0
- comfy_sdk-0.1.0/src/comfy_low/sse.py +70 -0
- comfy_sdk-0.1.0/src/comfy_low/transport.py +572 -0
- comfy_sdk-0.1.0/src/comfy_sdk/__init__.py +95 -0
- comfy_sdk-0.1.0/src/comfy_sdk/_core.py +93 -0
- comfy_sdk-0.1.0/src/comfy_sdk/_hashing.py +42 -0
- comfy_sdk-0.1.0/src/comfy_sdk/assets.py +303 -0
- comfy_sdk-0.1.0/src/comfy_sdk/client.py +177 -0
- comfy_sdk-0.1.0/src/comfy_sdk/events.py +126 -0
- comfy_sdk-0.1.0/src/comfy_sdk/exceptions.py +146 -0
- comfy_sdk-0.1.0/src/comfy_sdk/jobs.py +232 -0
- comfy_sdk-0.1.0/src/comfy_sdk/outputs.py +127 -0
- comfy_sdk-0.1.0/src/comfy_sdk/workflows.py +50 -0
- comfy_sdk-0.1.0/tests/conftest.py +360 -0
- comfy_sdk-0.1.0/tests/test_assets.py +104 -0
- comfy_sdk-0.1.0/tests/test_async.py +132 -0
- comfy_sdk-0.1.0/tests/test_auth_headers.py +34 -0
- comfy_sdk-0.1.0/tests/test_content_redirect_security.py +61 -0
- comfy_sdk-0.1.0/tests/test_download_and_workflows.py +66 -0
- comfy_sdk-0.1.0/tests/test_event_types.py +41 -0
- comfy_sdk-0.1.0/tests/test_events.py +68 -0
- comfy_sdk-0.1.0/tests/test_jobs.py +152 -0
- comfy_sdk-0.1.0/tests/test_low_decoding.py +66 -0
- comfy_sdk-0.1.0/tests/test_spec_coverage.py +45 -0
- comfy_sdk-0.1.0/tests/test_transport_security.py +45 -0
- 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
|
comfy_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|