Repository: voxel51/fiftyone

Issue:
[FR] Allow task names to be provided when annotating with CVAT
Currently, when annotating with the CVAT integration, the name for the tasks that get generated are hardcoded as `FiftyOne_{dataset_name}` which is not ideal when launching multiple annotation runs on the same dataset. There should be an argument allowing the user to provide one or more task names to use for the generated tasks.

Also, changing the default to `FiftyOne_{dataset_name}_{annotation_key}` would be helpful as well.


Discussion:

Resolving patch:
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -6,6 +6,7 @@
 | `voxel51.com <https://voxel51.com/>`_
 |
 """
+import math
 from collections import defaultdict
 from copy import copy, deepcopy
 from datetime import datetime
@@ -3060,6 +3061,7 @@ class CVATBackendConfig(foua.AnnotationBackendConfig):
             default, no project is used
         project_id (None): an optional ID of an existing CVAT project to which
             to upload the annotation tasks. By default, no project is used
+        task_name (None): an optional task name to use for the created CVAT task
         occluded_attr (None): an optional attribute name containing existing
             occluded values and/or in which to store downloaded occluded values
             for all objects in the annotation run
@@ -3091,6 +3093,7 @@ def __init__(
         job_reviewers=None,
         project_name=None,
         project_id=None,
+        task_name=None,
         occluded_attr=None,
         group_id_attr=None,
         issue_tracker=None,
@@ -3109,6 +3112,7 @@ def __init__(
         self.job_reviewers = job_reviewers
         self.project_name = project_name
         self.project_id = project_id
+        self.task_name = task_name
         self.occluded_attr = occluded_attr
         self.group_id_attr = group_id_attr
         self.issue_tracker = issue_tracker
@@ -4226,6 +4230,7 @@ def upload_samples(self, samples, backend):
 
         num_samples = len(samples)
         batch_size = self._get_batch_size(samples, task_size)
+        num_batches = math.ceil(num_samples / batch_size)
 
         samples.compute_metadata()
 
@@ -4290,8 +4295,16 @@ def upload_samples(self, samples, backend):
                     project_id = self.create_project(project_name, cvat_schema)
                     project_ids.append(project_id)
 
-                _dataset_name = samples_batch._dataset.name.replace(" ", "_")
-                task_name = "FiftyOne_%s" % _dataset_name
+                if config.task_name is None:
+                    _dataset_name = samples_batch._dataset.name.replace(
+                        " ", "_"
+                    )
+                    task_name = f"FiftyOne_{_dataset_name}"
+                else:
+                    task_name = config.task_name
+                # append task number when multiple tasks are created
+                if num_batches > 1:
+                    task_name += f"_{idx + 1}"
 
                 (
                     task_id,


Test patch:
diff --git a/tests/intensive/cvat_tests.py b/tests/intensive/cvat_tests.py
--- a/tests/intensive/cvat_tests.py
+++ b/tests/intensive/cvat_tests.py
@@ -417,6 +417,7 @@ def test_task_creation_arguments(self):
 
         anno_key = "anno_key"
         bug_tracker = "test_tracker"
+        task_name = "test_task"
         results = dataset.annotate(
             anno_key,
             backend="cvat",
@@ -427,15 +428,17 @@ def test_task_creation_arguments(self):
             job_assignees=users,
             job_reviewers=users,
             issue_tracker=bug_tracker,
+            task_name=task_name,
         )
         task_ids = results.task_ids
         with results:
             api = results.connect_to_api()
             self.assertEqual(len(task_ids), 2)
-            for task_id in task_ids:
+            for idx, task_id in enumerate(task_ids):
                 task_json = api.get(api.task_url(task_id)).json()
                 self.assertEqual(task_json["bug_tracker"], bug_tracker)
                 self.assertEqual(task_json["segment_size"], 1)
+                self.assertEqual(task_json["name"], f"{task_name}_{idx + 1}")
                 if user is not None:
                     self.assertEqual(task_json["assignee"]["username"], user)
                 for job in api.get(api.jobs_url(task_id)).json():