diff --git a/roboflow/adapters/rfapi.py b/roboflow/adapters/rfapi.py index 328332f1..5562aa22 100644 --- a/roboflow/adapters/rfapi.py +++ b/roboflow/adapters/rfapi.py @@ -763,6 +763,28 @@ def delete_folder(api_key, workspace_url, group_id): return response.json() +def add_projects_to_folder(api_key, workspace_url, group_id, project_ids): + """PATCH /{ws}/groups/{id}/projects — add projects to a folder.""" + response = requests.patch( + f"{API_URL}/{workspace_url}/groups/{group_id}/projects", + params={"api_key": api_key}, + json={"projects": project_ids}, + ) + if response.status_code not in (200, 204): + raise RoboflowError(response.text) + + +def remove_projects_from_folder(api_key, workspace_url, group_id, project_ids): + """DELETE /{ws}/groups/{id}/projects — remove projects from a folder.""" + response = requests.delete( + f"{API_URL}/{workspace_url}/groups/{group_id}/projects", + params={"api_key": api_key}, + json={"projects": project_ids}, + ) + if response.status_code not in (200, 204): + raise RoboflowError(response.text) + + # --------------------------------------------------------------------------- # Phase 2: Workflow endpoints # --------------------------------------------------------------------------- diff --git a/roboflow/cli/handlers/folder.py b/roboflow/cli/handlers/folder.py index 05028814..822ae294 100644 --- a/roboflow/cli/handlers/folder.py +++ b/roboflow/cli/handlers/folder.py @@ -61,6 +61,28 @@ def delete_folder( _delete_folder(args) +@folder_app.command("add-projects") +def add_projects( + ctx: typer.Context, + folder_id: Annotated[str, typer.Argument(help="Folder ID")], + projects: Annotated[str, typer.Argument(help="Comma-separated project IDs")], +) -> None: + """Add projects to a folder.""" + args = ctx_to_args(ctx, folder_id=folder_id, projects=projects) + _add_projects(args) + + +@folder_app.command("remove-projects") +def remove_projects( + ctx: typer.Context, + folder_id: Annotated[str, typer.Argument(help="Folder ID")], + projects: Annotated[str, typer.Argument(help="Comma-separated project IDs")], +) -> None: + """Remove projects from a folder.""" + args = ctx_to_args(ctx, folder_id=folder_id, projects=projects) + _remove_projects(args) + + # --------------------------------------------------------------------------- # Business logic (unchanged from argparse version) # --------------------------------------------------------------------------- @@ -200,3 +222,51 @@ def _delete_folder(args) -> None: # noqa: ANN001 data = {"status": "deleted"} output(args, data, text=f"Deleted folder '{args.folder_id}'") + + +def _add_projects(args) -> None: # noqa: ANN001 + import roboflow + from roboflow.cli._output import output, output_error, suppress_sdk_output + + with suppress_sdk_output(args): + try: + rf = roboflow.Roboflow(api_key=args.api_key) + workspace = rf.workspace(args.workspace) + except Exception as exc: + output_error(args, str(exc)) + return + + project_ids = [p.strip() for p in args.projects.split(",")] + + try: + workspace.add_projects_to_folder(args.folder_id, project_ids) + except Exception as exc: + output_error(args, str(exc), exit_code=1) + return + + data = {"status": "added", "folder_id": args.folder_id, "projects": project_ids} + output(args, data, text=f"Added {len(project_ids)} project(s) to folder '{args.folder_id}'") + + +def _remove_projects(args) -> None: # noqa: ANN001 + import roboflow + from roboflow.cli._output import output, output_error, suppress_sdk_output + + with suppress_sdk_output(args): + try: + rf = roboflow.Roboflow(api_key=args.api_key) + workspace = rf.workspace(args.workspace) + except Exception as exc: + output_error(args, str(exc)) + return + + project_ids = [p.strip() for p in args.projects.split(",")] + + try: + workspace.remove_projects_from_folder(args.folder_id, project_ids) + except Exception as exc: + output_error(args, str(exc), exit_code=1) + return + + data = {"status": "removed", "folder_id": args.folder_id, "projects": project_ids} + output(args, data, text=f"Removed {len(project_ids)} project(s) from folder '{args.folder_id}'") diff --git a/roboflow/core/workspace.py b/roboflow/core/workspace.py index f25292e3..0206ad5f 100644 --- a/roboflow/core/workspace.py +++ b/roboflow/core/workspace.py @@ -1081,6 +1081,18 @@ def create_folder(self, name, parent_id=None, project_ids=None): return rfapi.create_folder(self.__api_key, self.url, name, parent_id=parent_id, project_ids=project_ids) + def add_projects_to_folder(self, group_id, project_ids): + """Add projects to an existing folder.""" + from roboflow.adapters import rfapi + + return rfapi.add_projects_to_folder(self.__api_key, self.url, group_id, project_ids) + + def remove_projects_from_folder(self, group_id, project_ids): + """Remove projects from a folder.""" + from roboflow.adapters import rfapi + + return rfapi.remove_projects_from_folder(self.__api_key, self.url, group_id, project_ids) + # ----------------------------------------------------------------- # Phase 2: Workflow management # -----------------------------------------------------------------