Skip to content

Commit e9a912e

Browse files
authored
Merge pull request #479 from roboflow/sergii/add-project-to-folder
Add/remove projects to/from a folder
2 parents ed0c83f + 9a48870 commit e9a912e

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

roboflow/adapters/rfapi.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,28 @@ def delete_folder(api_key, workspace_url, group_id):
763763
return response.json()
764764

765765

766+
def add_projects_to_folder(api_key, workspace_url, group_id, project_ids):
767+
"""PATCH /{ws}/groups/{id}/projects — add projects to a folder."""
768+
response = requests.patch(
769+
f"{API_URL}/{workspace_url}/groups/{group_id}/projects",
770+
params={"api_key": api_key},
771+
json={"projects": project_ids},
772+
)
773+
if response.status_code not in (200, 204):
774+
raise RoboflowError(response.text)
775+
776+
777+
def remove_projects_from_folder(api_key, workspace_url, group_id, project_ids):
778+
"""DELETE /{ws}/groups/{id}/projects — remove projects from a folder."""
779+
response = requests.delete(
780+
f"{API_URL}/{workspace_url}/groups/{group_id}/projects",
781+
params={"api_key": api_key},
782+
json={"projects": project_ids},
783+
)
784+
if response.status_code not in (200, 204):
785+
raise RoboflowError(response.text)
786+
787+
766788
# ---------------------------------------------------------------------------
767789
# Phase 2: Workflow endpoints
768790
# ---------------------------------------------------------------------------

roboflow/cli/handlers/folder.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@ def delete_folder(
6161
_delete_folder(args)
6262

6363

64+
@folder_app.command("add-projects")
65+
def add_projects(
66+
ctx: typer.Context,
67+
folder_id: Annotated[str, typer.Argument(help="Folder ID")],
68+
projects: Annotated[str, typer.Argument(help="Comma-separated project IDs")],
69+
) -> None:
70+
"""Add projects to a folder."""
71+
args = ctx_to_args(ctx, folder_id=folder_id, projects=projects)
72+
_add_projects(args)
73+
74+
75+
@folder_app.command("remove-projects")
76+
def remove_projects(
77+
ctx: typer.Context,
78+
folder_id: Annotated[str, typer.Argument(help="Folder ID")],
79+
projects: Annotated[str, typer.Argument(help="Comma-separated project IDs")],
80+
) -> None:
81+
"""Remove projects from a folder."""
82+
args = ctx_to_args(ctx, folder_id=folder_id, projects=projects)
83+
_remove_projects(args)
84+
85+
6486
# ---------------------------------------------------------------------------
6587
# Business logic (unchanged from argparse version)
6688
# ---------------------------------------------------------------------------
@@ -200,3 +222,51 @@ def _delete_folder(args) -> None: # noqa: ANN001
200222

201223
data = {"status": "deleted"}
202224
output(args, data, text=f"Deleted folder '{args.folder_id}'")
225+
226+
227+
def _add_projects(args) -> None: # noqa: ANN001
228+
import roboflow
229+
from roboflow.cli._output import output, output_error, suppress_sdk_output
230+
231+
with suppress_sdk_output(args):
232+
try:
233+
rf = roboflow.Roboflow(api_key=args.api_key)
234+
workspace = rf.workspace(args.workspace)
235+
except Exception as exc:
236+
output_error(args, str(exc))
237+
return
238+
239+
project_ids = [p.strip() for p in args.projects.split(",")]
240+
241+
try:
242+
workspace.add_projects_to_folder(args.folder_id, project_ids)
243+
except Exception as exc:
244+
output_error(args, str(exc), exit_code=1)
245+
return
246+
247+
data = {"status": "added", "folder_id": args.folder_id, "projects": project_ids}
248+
output(args, data, text=f"Added {len(project_ids)} project(s) to folder '{args.folder_id}'")
249+
250+
251+
def _remove_projects(args) -> None: # noqa: ANN001
252+
import roboflow
253+
from roboflow.cli._output import output, output_error, suppress_sdk_output
254+
255+
with suppress_sdk_output(args):
256+
try:
257+
rf = roboflow.Roboflow(api_key=args.api_key)
258+
workspace = rf.workspace(args.workspace)
259+
except Exception as exc:
260+
output_error(args, str(exc))
261+
return
262+
263+
project_ids = [p.strip() for p in args.projects.split(",")]
264+
265+
try:
266+
workspace.remove_projects_from_folder(args.folder_id, project_ids)
267+
except Exception as exc:
268+
output_error(args, str(exc), exit_code=1)
269+
return
270+
271+
data = {"status": "removed", "folder_id": args.folder_id, "projects": project_ids}
272+
output(args, data, text=f"Removed {len(project_ids)} project(s) from folder '{args.folder_id}'")

roboflow/core/workspace.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,18 @@ def create_folder(self, name, parent_id=None, project_ids=None):
10811081

10821082
return rfapi.create_folder(self.__api_key, self.url, name, parent_id=parent_id, project_ids=project_ids)
10831083

1084+
def add_projects_to_folder(self, group_id, project_ids):
1085+
"""Add projects to an existing folder."""
1086+
from roboflow.adapters import rfapi
1087+
1088+
return rfapi.add_projects_to_folder(self.__api_key, self.url, group_id, project_ids)
1089+
1090+
def remove_projects_from_folder(self, group_id, project_ids):
1091+
"""Remove projects from a folder."""
1092+
from roboflow.adapters import rfapi
1093+
1094+
return rfapi.remove_projects_from_folder(self.__api_key, self.url, group_id, project_ids)
1095+
10841096
# -----------------------------------------------------------------
10851097
# Phase 2: Workflow management
10861098
# -----------------------------------------------------------------

0 commit comments

Comments
 (0)