From bbda11a34944f92a73abb2018853c066622b0cb2 Mon Sep 17 00:00:00 2001 From: Shantanu Bala Date: Mon, 10 Mar 2025 13:40:25 -0400 Subject: [PATCH 1/2] Add a project method for creating annotation jobs --- roboflow/core/project.py | 63 ++++++++++++++++++++++++++++++ tests/test_project.py | 84 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/roboflow/core/project.py b/roboflow/core/project.py index 374c864f..8485ef5b 100644 --- a/roboflow/core/project.py +++ b/roboflow/core/project.py @@ -812,3 +812,66 @@ def image(self, image_id: str) -> Dict: image_details = data["image"] return image_details + + def create_annotation_job( + self, + name: str, + batch_id: str, + num_images: int, + labeler_email: str, + reviewer_email: str + ) -> Dict: + """ + Create a new annotation job in the project. + + Args: + name (str): The name of the annotation job + batch_id (str): The ID of the batch that contains the images to annotate + num_images (int): The number of images to include in the job + labeler_email (str): The email of the user who will label the images + reviewer_email (str): The email of the user who will review the annotations + + Returns: + Dict: A dictionary containing the created job details + + Example: + >>> import roboflow + + >>> rf = roboflow.Roboflow(api_key="YOUR_API_KEY") + + >>> project = rf.workspace().project("PROJECT_ID") + + >>> job = project.create_annotation_job( + ... name="Job created by API", + ... batch_id="batch123", + ... num_images=10, + ... labeler_email="user@example.com", + ... reviewer_email="reviewer@example.com" + ... ) + """ + url = f"{API_URL}/{self.__workspace}/{self.__project_name}/jobs?api_key={self.__api_key}" + + payload = { + "name": name, + "batch": batch_id, + "num_images": num_images, + "labelerEmail": labeler_email, + "reviewerEmail": reviewer_email + } + + response = requests.post( + url, + headers={"Content-Type": "application/json"}, + json=payload + ) + + if response.status_code != 200: + try: + error_data = response.json() + if "error" in error_data: + raise RuntimeError(error_data["error"]) + raise RuntimeError(response.text) + except ValueError: + raise RuntimeError(f"Failed to create annotation job: {response.text}") + + return response.json() diff --git a/tests/test_project.py b/tests/test_project.py index a7cd55de..6bb89e1a 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,5 +1,6 @@ import requests import responses +from responses.matchers import json_params_matcher from roboflow import API_URL from roboflow.adapters.rfapi import AnnotationSaveError, ImageUploadError @@ -144,3 +145,86 @@ def test_image_invalid_json_response(self): self.project.image(image_id) self.assertIn("Expecting value", str(context.exception)) + + def test_create_annotation_job_success(self): + job_name = "Test Job" + batch_id = "test-batch-123" + num_images = 10 + labeler_email = "labeler@example.com" + reviewer_email = "reviewer@example.com" + + expected_response = { + "success": True, + "job": { + "id": "job-123", + "name": job_name, + "batch": batch_id, + "num_images": num_images, + "labeler": labeler_email, + "reviewer": reviewer_email, + "status": "created", + "created": 1616161616 + } + } + + expected_url = f"{API_URL}/{WORKSPACE_NAME}/{PROJECT_NAME}/jobs?api_key={ROBOFLOW_API_KEY}" + + responses.add( + responses.POST, + expected_url, + json=expected_response, + status=200, + match=[ + json_params_matcher({ + "name": job_name, + "batch": batch_id, + "num_images": num_images, + "labelerEmail": labeler_email, + "reviewerEmail": reviewer_email + }) + ] + ) + + result = self.project.create_annotation_job( + name=job_name, + batch_id=batch_id, + num_images=num_images, + labeler_email=labeler_email, + reviewer_email=reviewer_email + ) + + self.assertEqual(result, expected_response) + self.assertTrue(result["success"]) + self.assertEqual(result["job"]["id"], "job-123") + self.assertEqual(result["job"]["name"], job_name) + + def test_create_annotation_job_error(self): + job_name = "Test Job" + batch_id = "invalid-batch" + num_images = 10 + labeler_email = "labeler@example.com" + reviewer_email = "reviewer@example.com" + + error_response = { + "error": "Batch not found" + } + + expected_url = f"{API_URL}/{WORKSPACE_NAME}/{PROJECT_NAME}/jobs?api_key={ROBOFLOW_API_KEY}" + + responses.add( + responses.POST, + expected_url, + json=error_response, + status=404 + ) + + with self.assertRaises(RuntimeError) as context: + self.project.create_annotation_job( + name=job_name, + batch_id=batch_id, + num_images=num_images, + labeler_email=labeler_email, + reviewer_email=reviewer_email + ) + + self.assertEqual(str(context.exception), "Batch not found") From f73f029ade71d1c17adc3df9bc7e561dd52d49e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:44:37 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roboflow/core/project.py | 25 ++++++---------- tests/test_project.py | 61 ++++++++++++++++++---------------------- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/roboflow/core/project.py b/roboflow/core/project.py index 8485ef5b..e9da7f6a 100644 --- a/roboflow/core/project.py +++ b/roboflow/core/project.py @@ -812,14 +812,9 @@ def image(self, image_id: str) -> Dict: image_details = data["image"] return image_details - + def create_annotation_job( - self, - name: str, - batch_id: str, - num_images: int, - labeler_email: str, - reviewer_email: str + self, name: str, batch_id: str, num_images: int, labeler_email: str, reviewer_email: str ) -> Dict: """ Create a new annotation job in the project. @@ -850,21 +845,17 @@ def create_annotation_job( ... ) """ url = f"{API_URL}/{self.__workspace}/{self.__project_name}/jobs?api_key={self.__api_key}" - + payload = { "name": name, "batch": batch_id, "num_images": num_images, "labelerEmail": labeler_email, - "reviewerEmail": reviewer_email + "reviewerEmail": reviewer_email, } - - response = requests.post( - url, - headers={"Content-Type": "application/json"}, - json=payload - ) - + + response = requests.post(url, headers={"Content-Type": "application/json"}, json=payload) + if response.status_code != 200: try: error_data = response.json() @@ -873,5 +864,5 @@ def create_annotation_job( raise RuntimeError(response.text) except ValueError: raise RuntimeError(f"Failed to create annotation job: {response.text}") - + return response.json() diff --git a/tests/test_project.py b/tests/test_project.py index 6bb89e1a..afa69437 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -145,14 +145,14 @@ def test_image_invalid_json_response(self): self.project.image(image_id) self.assertIn("Expecting value", str(context.exception)) - + def test_create_annotation_job_success(self): job_name = "Test Job" batch_id = "test-batch-123" num_images = 10 labeler_email = "labeler@example.com" reviewer_email = "reviewer@example.com" - + expected_response = { "success": True, "job": { @@ -163,68 +163,63 @@ def test_create_annotation_job_success(self): "labeler": labeler_email, "reviewer": reviewer_email, "status": "created", - "created": 1616161616 - } + "created": 1616161616, + }, } - + expected_url = f"{API_URL}/{WORKSPACE_NAME}/{PROJECT_NAME}/jobs?api_key={ROBOFLOW_API_KEY}" - + responses.add( responses.POST, expected_url, json=expected_response, status=200, match=[ - json_params_matcher({ - "name": job_name, - "batch": batch_id, - "num_images": num_images, - "labelerEmail": labeler_email, - "reviewerEmail": reviewer_email - }) - ] + json_params_matcher( + { + "name": job_name, + "batch": batch_id, + "num_images": num_images, + "labelerEmail": labeler_email, + "reviewerEmail": reviewer_email, + } + ) + ], ) - + result = self.project.create_annotation_job( name=job_name, batch_id=batch_id, num_images=num_images, labeler_email=labeler_email, - reviewer_email=reviewer_email + reviewer_email=reviewer_email, ) - + self.assertEqual(result, expected_response) self.assertTrue(result["success"]) self.assertEqual(result["job"]["id"], "job-123") self.assertEqual(result["job"]["name"], job_name) - + def test_create_annotation_job_error(self): job_name = "Test Job" batch_id = "invalid-batch" num_images = 10 labeler_email = "labeler@example.com" reviewer_email = "reviewer@example.com" - - error_response = { - "error": "Batch not found" - } - + + error_response = {"error": "Batch not found"} + expected_url = f"{API_URL}/{WORKSPACE_NAME}/{PROJECT_NAME}/jobs?api_key={ROBOFLOW_API_KEY}" - - responses.add( - responses.POST, - expected_url, - json=error_response, - status=404 - ) - + + responses.add(responses.POST, expected_url, json=error_response, status=404) + with self.assertRaises(RuntimeError) as context: self.project.create_annotation_job( name=job_name, batch_id=batch_id, num_images=num_images, labeler_email=labeler_email, - reviewer_email=reviewer_email + reviewer_email=reviewer_email, ) - + self.assertEqual(str(context.exception), "Batch not found")