Skip to content

Soft-delete / Trash support: SDK + CLI#463

Merged
yeldarby merged 17 commits intomainfrom
soft-delete
Apr 29, 2026
Merged

Soft-delete / Trash support: SDK + CLI#463
yeldarby merged 17 commits intomainfrom
soft-delete

Conversation

@yeldarby
Copy link
Copy Markdown
Contributor

@yeldarby yeldarby commented Apr 24, 2026

Summary

Adds project, version, and workflow lifecycle management mirroring the soft-delete and Trash features added to the web app in roboflow/roboflow#11131. Deleting a project, version, or workflow now moves it to Trash (30-day retention) and cancels any in-flight training jobs on it; items can be restored within the retention window.

  • Project-level: Project.delete() / Project.restore()
  • Version-level: Version.delete() / Version.restore()
  • Workspace-level: Workspace.trash(), Workspace.restore_from_trash()
  • CLI: roboflow project delete|restore, roboflow version delete|restore, roboflow workflow delete|restore, roboflow trash list

The SDK doesn't yet have a first-class Workflow handle object, so workflow lifecycle is exposed through rfapi.delete_workflow() + Workspace.restore_from_trash("workflow", id) plus the matching CLI commands.

Details

New rfapi functions (roboflow/adapters/rfapi.py):

  • delete_project(api_key, workspace_url, project_url)DELETE /:workspace/:project
  • delete_version(api_key, workspace_url, project_url, version)DELETE /:workspace/:project/:version
  • delete_workflow(api_key, workspace_url, workflow_url)DELETE /:workspace/workflows/:workflow
  • list_trash(api_key, workspace_url)GET /:workspace/trash
  • restore_trash_item(api_key, workspace_url, item_type, item_id, parent_id=None)POST /:workspace/trash/restore

SDK methodsProject.delete() returns the server response. Project.restore() looks the project up by slug in the workspace Trash and restores it (raises RuntimeError if not found). Same shape for Version. For scripts without a live handle (or for workflows, which don't have an SDK object yet), Workspace.trash() returns the list of items and Workspace.restore_from_trash(type, id, parent_id) restores by id.

CLI commands — destructive commands (project delete, version delete, workflow delete) prompt for confirmation interactively and accept --yes / -y for scripted use. All commands honor --json for structured output and use output_error() with actionable hints on every error path (per the Agent Experience checklist in CONTRIBUTING.md).

Error handlingrfapi extracts the error field from JSON response bodies before raising RoboflowError, so users see a clean message ("Not authorized to view trash") rather than the raw response body.

Permanent deletion is intentionally web-UI-only

Emptying Trash or immediately deleting a single Trash item destroys data irrecoverably, so it's not exposed on the SDK or CLI — those actions live only in the Roboflow app's Trash view, which has an explicit confirmation dialog. Items left in Trash are cleaned up automatically after 30 days.

Guard tests ensure rfapi.trash_delete_immediately, rfapi.empty_trash, Workspace.delete_from_trash, Workspace.empty_trash, roboflow trash empty, and roboflow trash delete stay off the SDK/CLI surface going forward.

Backward compatibility

Purely additive — no existing APIs changed. The new routes require project:update (for projects), version:update (for versions), or workflow:update (for workflows), which most existing keys already have.

Test plan

  • All 493 unit tests pass (python -m unittest) — new tests cover project / version / workflow / trash CLI handlers and guard tests for the removed permanent-delete surface
  • ruff check + ruff format --check clean on all new/modified files
  • mypy roboflow clean
  • End-to-end smoke test against staging API (test_soft_delete_flow.py, in-tree but untracked) drives the CLI through delete project → restore project → delete version → restore version → delete workflow → restore workflow, with real HTTP round-trips to localapi.roboflow.one
  • CLI --help output lists the new commands (roboflow project --help, roboflow version --help, roboflow workflow --help, roboflow trash --help)
  • Audited against the Agent Experience checklist in CONTRIBUTING.md: --json, no interactive prompts when --yes/--json set, output_error(..., hint=..., exit_code=N) on every failure path, exit codes 0/1/2/3

Dependencies

Blocked on the backend soft-delete PR: roboflow/roboflow#11131. That PR adds the server-side endpoints this SDK calls. Do not merge this until that one is in prod.

Companion docs PRs

🤖 Generated with Claude Code

yeldarby and others added 7 commits April 23, 2026 21:41
Adds project and version lifecycle management mirroring the soft-delete
and Trash features that just landed in the web app. Projects and
versions are moved to Trash on delete (30-day retention) and can be
restored within the retention window; any in-flight training jobs are
cancelled automatically on the server.

New rfapi functions:
- delete_project, delete_version
- list_trash, restore_trash_item
- trash_delete_immediately, empty_trash

New SDK methods:
- Project.delete(), Project.restore()
- Version.delete(), Version.restore()
- Workspace.trash(), Workspace.restore_from_trash(),
  Workspace.delete_from_trash(), Workspace.empty_trash()

New CLI commands:
- roboflow project delete / restore
- roboflow version delete / restore
- roboflow trash list / empty / delete

All existing tests pass; new commands have --yes / -y to skip the
confirmation prompt for scripted use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CLI-COMMANDS.md: new "Delete and restore" section covering project,
  version, and trash commands; added `trash` to the command-groups table.
- tests/cli/test_project_handler.py: registration + delete/restore
  unit tests.
- tests/cli/test_version_handler.py: registration + delete/restore
  unit tests (including the parent-project disambiguation lookup).
- tests/cli/test_trash_handler.py: full coverage for list/empty/delete
  commands with mocked rfapi.

SDK-side (`docs/core/*.md`) is auto-generated via mkdocstrings from
docstrings, so no changes needed there — the new methods are already
documented via their docstrings.

479 tests pass (20 new), ruff clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Permanent deletion destroys data irrecoverably. Exposing it on the
public API — and through the SDK/CLI that wraps it — makes it one stray
curl or for-loop away from catastrophe, with no undo. It stays
available only in the web UI's Trash view, which has an explicit
confirmation dialog; items left in Trash are cleaned up automatically
after 30 days either way.

Removed:
- rfapi.trash_delete_immediately(), rfapi.empty_trash()
- Workspace.delete_from_trash(), Workspace.empty_trash()
- roboflow trash empty, roboflow trash delete commands
- Corresponding tests (replaced with guard tests that fail if the
  permanent-delete surface is ever re-added by accident)
- CLI-COMMANDS.md references

Kept (soft-delete + restore + list only):
- Project.delete() / Project.restore()
- Version.delete() / Version.restore()
- Workspace.trash() / Workspace.restore_from_trash()
- roboflow project delete|restore
- roboflow version delete|restore
- roboflow trash list

Paired with the corresponding backend removal in
roboflow/roboflow#11131 — the public REST API no longer exposes
POST /:ws/trash/deleteImmediately or POST /:ws/trash/empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONTRIBUTING.md's Agent Experience checklist asks that every
output_error() include an actionable hint. The delete/restore/list
commands shipped with hints on some paths but not others. Fills in
the gaps so agents get a consistent "what went wrong AND what to do"
pair on every failure:

- resolver ValueError: tell them the valid shorthand formats
- API RoboflowError: point at the specific scope that's likely missing
  ('project:update' / 'version:update' / 'project:read')
- 'not in trash' on version restore: additionally note that a parent
  project in trash must be restored first

No behavior change — same error paths, same exit codes, same JSON
schema. Just more useful text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The delete_project() docstring still mentioned trash_delete_immediately
as a follow-on action, but that helper was removed. Replace with a note
about the 30-day cleanup cron so the lifecycle is documented without
pointing at a symbol that doesn't exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yeldarby yeldarby requested review from tonylampada April 24, 2026 02:18
@yeldarby yeldarby marked this pull request as ready for review April 24, 2026 02:18
yeldarby and others added 7 commits April 23, 2026 23:30
Mirrors delete_project() / delete_version() — wraps DELETE
/:workspace/workflows/:workflowUrl on the public API (added in the
paired backend PR). Workflow restore was already available via
restore_trash_item(..., \"workflow\", ...) through the generic trash
endpoint, so this closes the symmetry gap.

Intentionally not adding Workflow.delete() / .restore() SDK methods or
a `roboflow workflow delete|restore` CLI subcommand in this PR — there
is no Workflow handle object in the SDK yet, and adding that is
significant scope. Agents/scripts that want workflow lifecycle control
can call rfapi.delete_workflow() and Workspace.restore_from_trash()
directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the CLI surface for soft-delete — workflows now have the same
delete/restore subcommands as projects and versions. Both commands:

- call rfapi.delete_workflow / rfapi.restore_trash_item under the hood
  (the public API endpoints added in the paired backend PR)
- honor --json for structured output
- honor --yes / -y on delete to skip the confirmation prompt for
  scripted use
- emit output_error(..., hint=..., exit_code=N) with actionable hints
  per the Agent Experience checklist: which scope to check, what to
  run to see what's in Trash, etc.
- accept either the workflow URL slug (e.g. slow-webhooks) or its
  Firestore id (e.g. wf_abc123) on restore

Restore looks the workflow up in the workspace Trash by URL (falling
back to id), just like project/version restore. CLI-COMMANDS.md gets
a one-line update in the "Delete and restore" section.

485 tests pass (6 new: 2 registration + 4 handler).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend trash endpoints (DELETE /:ws/:project, DELETE /:ws/:project/:version,
DELETE /:ws/workflows/:url, GET /:ws/trash, POST /:ws/trash/restore) now
return proper 4xx/5xx HTTP status codes with `{"error": "..."}` JSON
bodies instead of 200 with an error tucked into the body. Update the
Python SDK to pull the `error` string out before raising RoboflowError
so users (and the CLI's error formatter) see a clean message instead
of the raw response body.

Falls back to response.text if the body isn't JSON or doesn't have an
`error` field, so non-trash endpoints (which still return raw text on
some failure paths) keep their existing behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ional

The previous version raised inside the try block if the JSON body had an
`error` field, then fell through to a second raise outside the try if it
didn't. Worked, but if anyone widened the except clause from `ValueError`
to something broader (e.g. `Exception`) the success-path raise would
silently get swallowed and the helper would return None, breaking every
caller's `if status != 200: _raise_for_trash_response(response)` flow.

Compute the message inside the try, then raise once unconditionally
outside it. Same observable behavior, no swallow risk.
…sion.restore

- workflow restore hint was 'project:update' → should be 'workflow:update'
- version restore hint was 'project:update' → should be 'version:update'
  (the public route picks the per-type scope, so the old hints sent
  users to add a scope they already had)

Also dropped the parentId fallback in Version.restore's matcher.
self.project is always the project URL slug — the SDK never holds the
Firestore doc id that parentId would equal — so the `or v.get("parentId")
== self.project` branch was dead. Comment now explicitly says we match
on parentUrl because that's the only available key.
digaobarbosa
digaobarbosa previously approved these changes Apr 28, 2026
Copy link
Copy Markdown
Contributor

@digaobarbosa digaobarbosa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Aligns the SDK with the rest of the public API. Diego flagged on the
docs PR that DELETE responses use `type: "project"` but the trash
list/restore payloads were using `type: "dataset"` — same item, two
names. Fixing on `"project"` to match the rest of the public surface
(`:project` URL params, DELETE responses, the `project:*` scope
namespace).

Backend ships the same change on the `soft-delete` branch; the docs
PR covers the user-visible REST API and SDK pages.

- `Project.restore()`: looks up `trash["sections"]["projects"]` and
  passes `"project"` to `rfapi.restore_trash_item`. Docstring updated
  to show `type: "project"` in the example response.
- `Workspace.restore_from_trash()`: docstring lists the valid item
  types as `"project"`, `"version"`, `"workflow"` (was
  `"dataset"`/.../...).
- `roboflow project restore` CLI: same lookup + restore call.
- `rfapi.restore_trash_item` docstring: matching update.
- Tests under `tests/cli/test_project_handler.py` and
  `tests/cli/test_trash_handler.py` now exercise the project
  vocabulary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
yeldarby and others added 2 commits April 28, 2026 22:34
Brings in the soft-delete / Trash work from #463 plus the master merge
that lands the 1.3.6 image-upload fix from #464.

CHANGELOG updated with the 1.3.7 release notes (SDK + CLI + low-level
rfapi additions, the upload-bytes change inherited from 1.3.6, and the
intentional non-exposure of permanent-delete actions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yeldarby yeldarby merged commit 6639e72 into main Apr 29, 2026
13 checks passed
@yeldarby yeldarby deleted the soft-delete branch April 29, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants