mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #151] feat(errors): add a typed AssetInUse so a delete conflict is catchable #149
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
effort:medium
effort:small
effort:small
effort:trivial
priority:nice-to-have
priority:nice-to-have
priority:recommended
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-python-sdk#149
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-python-sdk/pull/151
Author: @mattmillerai
Created: 9/14/2026
Status: 🔄 Open
Base:
main← Head:matt/be-14168-typed-asset-in-use📝 Commits (4)
7841863feat(errors): add a typed AssetInUse so a delete conflict is catchablec59e375Merge origin/main into matt/be-14168-typed-asset-in-usedf7c0efMerge origin/main into matt/be-14168-typed-asset-in-use870ffb4test(router): reconcile the credits_used header-lift tripwire📊 Changes
11 files changed (+180 additions, -10 deletions)
View changed files
📝
CHANGELOG.md(+9 -0)📝
README.md(+10 -0)📝
src/comfy_low/__init__.py(+2 -0)📝
src/comfy_low/errors.py(+14 -0)📝
src/comfy_sdk/__init__.py(+2 -0)📝
src/comfy_sdk/exceptions.py(+14 -0)📝
tests/conftest.py(+5 -0)📝
tests/test_assets.py(+44 -1)📝
tests/test_error_mapping.py(+47 -0)📝
tests/test_exception_modules.py(+18 -0)📝
tests/test_router_spec_contract.py(+15 -9)📄 Description
ELI-5
The API says "no" in two different ways when it returns a
409. Uploading bytes that don't match their declared hash is one (hash_mismatch); deleting an asset something still depends on is the other (asset_in_use). Only the first had a name you couldexcepton, so a caller who wanted to handle just the delete case had to writeif exc.code == "asset_in_use"— reaching past the typed errors into the wire protocol. This gives the second one a name too.What changed
comfy_low.errors.AssetInUse(ApiError)withcode = "asset_in_use", registered in that module's_BY_CODEbesideHashMismatch.comfy_sdk.exceptions.AssetInUse(ComfyError)with the matching_BY_CODEentry, exported fromcomfy_sdk(andAssetInUsefromcomfy_low) the way every other typed error is.DELETE /api/v2/assets/{id}route (sync + async), and a regression assert thathash_mismatchstill maps toHashMismatchand the two classes are siblings rather than one subclassing the other.CHANGELOG.mdunder[Unreleased] → Added— the release flow is tag-driven andpyproject.toml's version is a placeholder, so an already-tagged heading would have been wrong.Judgment calls
Registering
asset_in_useincomfy_sdk's_BY_CODEis the one line with blast radius, because that table is also a filter._router_only_class()returnsNonefor any code already in_BY_CODE, so adding a key removes that code from Router-bucket retyping. I checked before adding it:asset_in_useappears nowhere inrouter_exceptions.pyorspec/router-openapi.yaml— it is a v2-envelope code only — so nothing was shadowed. The router-spec contract check (scripts/check_drift.py) still reports all 15 buckets covered.The two 409s are siblings, not a hierarchy.
WorkflowFormatUisubclassesInvalidWorkflowbecause it is a narrower case of the same failure; a delete conflict and an upload hash conflict are unrelated failures that only share a status, soAssetInUseextends the base directly. A test pins that both ways, since making one catch the other later would be a silent behaviour change for existingexcept HashMismatchhandlers.I added a
reject_delete_in_useflag to the stub server rather than mocking httpx, per the repo's rule that the suite drivesserver.stateand has no network dependency.No capability is being denied by this diff, so the falsification discipline does not apply: it adds a class and a
_BY_CODErow, introduces no deny/dead-end path, no "not supported"/"unavailable" string, and flips no test to assert something is impossible. The only "you can't" prose is the README's note that an immediate retry does not clear a platform hold, which restates the spec's own description of the status rather than asserting a missing feature — and it is written so the retry-later path stays visible.Verification against what the ticket names
Every artifact the ticket points at, exercised read-only:
spec/openapi.yaml— read both 409s:hash_mismatchonPOST /api/v2/assets(line 117) andasset_in_useonDELETE /api/v2/assets/{id}(line 278), whose description is the source for "the body deliberately never says which hold applies".src/comfy_low/errors.py_BY_CODEandsrc/comfy_sdk/exceptions.py_BY_CODE— both confirmed to have lackedasset_in_use, both now carry it.409 asset_in_useon both layers — the stated acceptance criterion, covered bytest_an_enveloped_delete_conflict_is_typed_at_the_protocol_layer/..._at_the_sdk_layerplus three end-to-end tests through the realassets.delete()/Asset.delete()/ async paths.mainmerge. The two changes stayed independent as expected (#132 touches_CODE_BY_STATUS, this touches_BY_CODE), and together they close both halves of theasset_in_usestory — see Residual. Based onmain, not stacked.pyproject.toml— confirmed the version is the placeholder the publish workflow overwrites, which is why the changelog entry is under[Unreleased].Provenance
mainmerged in atc59e375):ruff check .: all checks passed;ruff format --check .: 57 files already formatted;mypy src: no issues in 21 source files;pytest: 982 passed, 9 skipped (skips are the network-gated integration tests, skipped onmaintoo);scripts/check_drift.py: models in sync, all 18 router error types covered, model-run URL as declared;scripts/check_public_repo_hygiene.py: no internal-only references.AssetInUsedocstring and thetest_error_mapping.pycomment now spell the routeDELETE /api/v2/assets/{id}(panel nit — the tree reserves bare/v2/...for the Router surface), andAssetInUsewas added tocomfy_sdk.exceptions.__all__, which the first pass missed while updating both packages'__all__; a guard test now asserts every class in_BY_CODEis in__all__.Residual
AClosed — #132 has merged and is in this branch. When this was written,409that carries no envelope code is still mistyped asHashMismatch, on delete as well as upload._CODE_BY_STATUSstill mapped409 → "hash_mismatch", so a bare409fromDELETE /api/v2/assets/{id}— one from an intermediary, or a surface answering the status without the envelope — reached the caller asHashMismatch. That row was left untouched here because removing it was the whole subject of the then-open #132, and changing it in two places at once would have conflicted.#132 has since merged and arrives in this branch with the
mainmerge: it dropped409from_CODE_BY_STATUSoutright, so a code-less409now decodes tohttp_409and surfaces as a plainComfyErrorcarrying the realhttp_status, the response's message and anyRetry-After. The two changes compose as intended —except AssetInUseis reliable for enveloped delete conflicts, and a code-less409no longer claims to be an upload failure instead. The cursor-review panel raised this same gap against this PR (Medium); it is resolved by the merge rather than by a change on this branch.Three of the twelve error codes the v2 spec names on a 4xx/5xx response still have no typed class at either layer, which is the same defect this ticket describes, one artifact over. I ran the same sweep over the remainder rather than only over the code I fixed: of the 12 codes
spec/openapi.yamlnames on an error response, 9 are now typed at both layers, and these 3 are not:too_many_streams(GET /jobs/{id}/events, line 579) — the closest sibling of this ticket. It reaches a caller as a bareComfyErrorand the correct handling (close a stream, or fall back to polling) is exactly the kind of decision a typed class should carry.deployment_not_ready(429 on deployment-scoped surfaces, line 447) — less acute, becauseclient.py's submit path already retries429 + Retry-Aftergenerically, so it is usually absorbed before a caller sees it. It is still indistinguishable fromqueue_fullby class.not_implemented(501 from the events endpoint, line 588) — mostly absorbed too:jobs.pycatcheshttp_status == 501and falls back to polling. A caller reaching a raw transport call still gets a bare error.None of these were in scope here and none are caused by this change; they are named because the sweep that sized this fix could see them.
Nothing was exercised against a live deployment. Verification is against the repo's stdlib stub server, which is the repo's own mandated approach (
AGENTS.md: the suite has no network dependency, driveserver.staterather than mocking httpx).tests/integration/test_gateway_e2e.pyis gated onCOMFY_BASE_URL+COMFY_API_KEYand skipped in this run, so no real409 asset_in_usefrom a real surface was observed — the wire shape is taken fromspec/openapi.yamlas the contract of record. If a deployment's delete conflict turns out not to carryerror.codein the envelope, the code-less gap above is what it would hit.Summary by CodeRabbit
AssetInUseerror for asset deletion requests refused because the platform still depends on the asset.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.