Skip to content

Commit 303b69e

Browse files
Merge branch 'main' into fix/include-keypoints-metadata-json-on-model-upload
2 parents e095c5a + 6509b18 commit 303b69e

8 files changed

Lines changed: 660 additions & 61 deletions

File tree

roboflow/adapters/rfapi.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import mimetypes
33
import os
44
import urllib
5-
from typing import Dict, List, Optional, Union
5+
from typing import Any, Dict, List, Optional, Union
66
from urllib.parse import quote
77

88
import requests
@@ -378,6 +378,76 @@ def workspace_delete_images(
378378
return response.json()
379379

380380

381+
def update_image_metadata(
382+
api_key: str,
383+
workspace_url: str,
384+
image_id: str,
385+
*,
386+
metadata: Optional[Dict] = None,
387+
remove_metadata: Optional[List[str]] = None,
388+
add_tags: Optional[List[str]] = None,
389+
remove_tags: Optional[List[str]] = None,
390+
) -> dict:
391+
"""Update metadata and tags on a single image (synchronous).
392+
393+
Args:
394+
api_key: Roboflow API key.
395+
workspace_url: Workspace slug/url.
396+
image_id: Image/source ID.
397+
metadata: Key-value pairs to set on the image.
398+
remove_metadata: Metadata keys to delete.
399+
add_tags: Tags to append.
400+
remove_tags: Tags to remove.
401+
402+
Returns:
403+
Parsed JSON response (``{"success": true}``).
404+
405+
Raises:
406+
RoboflowError: On non-200 response.
407+
"""
408+
url = f"{API_URL}/{workspace_url}/images/{quote(image_id, safe='')}/metadata"
409+
body: Dict[str, Any] = {}
410+
if metadata is not None:
411+
body["metadata"] = metadata
412+
if remove_metadata is not None:
413+
body["removeMetadata"] = remove_metadata
414+
if add_tags is not None:
415+
body["addTags"] = add_tags
416+
if remove_tags is not None:
417+
body["removeTags"] = remove_tags
418+
419+
response = requests.post(url, params={"api_key": api_key}, json=body)
420+
if response.status_code != 200:
421+
raise RoboflowError(response.text)
422+
return response.json()
423+
424+
425+
def batch_update_image_metadata(
426+
api_key: str,
427+
workspace_url: str,
428+
updates: List[Dict],
429+
) -> dict:
430+
"""Batch-update metadata and tags on multiple images (asynchronous).
431+
432+
Args:
433+
api_key: Roboflow API key.
434+
workspace_url: Workspace slug/url.
435+
updates: List of update dicts, each containing ``imageId`` and optionally
436+
``metadata``, ``removeMetadata``, ``addTags``, ``removeTags``.
437+
438+
Returns:
439+
Parsed JSON with ``taskId`` and ``url`` for polling.
440+
441+
Raises:
442+
RoboflowError: On non-202 response.
443+
"""
444+
url = f"{API_URL}/{workspace_url}/images/metadata"
445+
response = requests.post(url, params={"api_key": api_key}, json={"updates": updates})
446+
if response.status_code != 202:
447+
raise RoboflowError(response.text)
448+
return response.json()
449+
450+
381451
def upload_image(
382452
api_key,
383453
project_url,
@@ -763,6 +833,28 @@ def delete_folder(api_key, workspace_url, group_id):
763833
return response.json()
764834

765835

836+
def add_projects_to_folder(api_key, workspace_url, group_id, project_ids):
837+
"""PATCH /{ws}/groups/{id}/projects — add projects to a folder."""
838+
response = requests.patch(
839+
f"{API_URL}/{workspace_url}/groups/{group_id}/projects",
840+
params={"api_key": api_key},
841+
json={"projects": project_ids},
842+
)
843+
if response.status_code not in (200, 204):
844+
raise RoboflowError(response.text)
845+
846+
847+
def remove_projects_from_folder(api_key, workspace_url, group_id, project_ids):
848+
"""DELETE /{ws}/groups/{id}/projects — remove projects from a folder."""
849+
response = requests.delete(
850+
f"{API_URL}/{workspace_url}/groups/{group_id}/projects",
851+
params={"api_key": api_key},
852+
json={"projects": project_ids},
853+
)
854+
if response.status_code not in (200, 204):
855+
raise RoboflowError(response.text)
856+
857+
766858
# ---------------------------------------------------------------------------
767859
# Phase 2: Workflow endpoints
768860
# ---------------------------------------------------------------------------

roboflow/cli/handlers/_aliases.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ def upload_alias(
6363
project: Annotated[str, typer.Option("-p", "--project", help="Project ID")],
6464
annotation: Annotated[Optional[str], typer.Option("-a", "--annotation", help="Annotation file")] = None,
6565
labelmap: Annotated[Optional[str], typer.Option("-m", "--labelmap", help="Labelmap file")] = None,
66-
split: Annotated[str, typer.Option("-s", "--split", help="Split (train/valid/test)")] = "train",
66+
split: Annotated[
67+
Optional[str],
68+
typer.Option("-s", "--split", help="Override split for all uploaded images (default: infer from folder)"),
69+
] = None,
6770
num_retries: Annotated[int, typer.Option("-r", "--retries", help="Retry count")] = 0,
6871
batch: Annotated[Optional[str], typer.Option("-b", "--batch", help="Batch name")] = None,
6972
tag_names: Annotated[Optional[str], typer.Option("-t", "--tag", help="Tag names")] = None,

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}'")

0 commit comments

Comments
 (0)