Repository: voxel51/fiftyone

Issue:
[FR] Add support for CVAT organizations
CVAT 2.0 came with the ability to [create organizations](https://openvinotoolkit.github.io/cvat/docs/manual/advanced/organization/). By default, annotations are only uploaded to the user's "Personal Workspace", but there should be an argument to define the organization to which to upload the data. 

## Goal

```python
dataset.annotate(
    anno_key,
    label_field=label_field,
    organization="my-org",
)
```

## Current workaround

Set the `X-Organization` header to the organization you want to use.

```python
headers = {"X-Organization": "my-org"}

dataset.annotate(
    anno_key,
    label_field=label_field,
    headers=headers,
)
```


Discussion:
An additional ask, if a project or project id is passed into the annotate function can the organization of the project be automatically used?

Project Id 5 is in organization "my-org"
```
dataset.annotate(
    anno_key,
    label_field=label_field,
    organization="my-org",
    project_id=5
)
```

Can it just be?
```
dataset.annotate(
    anno_key,
    label_field=label_field,
    project_id=5
)
```


Resolving patch:
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -3071,6 +3071,8 @@ class CVATBackendConfig(foua.AnnotationBackendConfig):
         issue_tracker (None): URL(s) of an issue tracker to link to the created
             task(s). This argument can be a list of URLs when annotating videos
             or when using ``task_size`` and generating multiple tasks
+        organization (None): the name of the organization to use when sending
+            requests to CVAT
     """
 
     def __init__(
@@ -3097,6 +3099,7 @@ def __init__(
         occluded_attr=None,
         group_id_attr=None,
         issue_tracker=None,
+        organization=None,
         **kwargs,
     ):
         super().__init__(name, label_schema, media_field=media_field, **kwargs)
@@ -3116,6 +3119,7 @@ def __init__(
         self.occluded_attr = occluded_attr
         self.group_id_attr = group_id_attr
         self.issue_tracker = issue_tracker
+        self.organization = organization
 
         # store privately so these aren't serialized
         self._username = username
@@ -3228,6 +3232,7 @@ def _connect_to_api(self):
             username=self.config.username,
             password=self.config.password,
             headers=self.config.headers,
+            organization=self.config.organization,
         )
 
     def upload_annotations(self, samples, launch_editor=False):
@@ -3489,14 +3494,25 @@ class CVATAnnotationAPI(foua.AnnotationAPI):
         username (None): the CVAT username
         password (None): the CVAT password
         headers (None): an optional dict of headers to add to all requests
+        organization (None): the name of the organization to use when sending
+            requests to CVAT
     """
 
-    def __init__(self, name, url, username=None, password=None, headers=None):
+    def __init__(
+        self,
+        name,
+        url,
+        username=None,
+        password=None,
+        headers=None,
+        organization=None,
+    ):
         self._name = name
         self._url = url.rstrip("/")
         self._username = username
         self._password = password
         self._headers = headers
+        self._organization = organization
 
         self._server_version = None
         self._session = None
@@ -3657,11 +3673,19 @@ def _setup(self):
             self._login(username, password)
 
         self._add_referer()
+        self._add_organization()
 
     def _add_referer(self):
         if "Referer" not in self._session.headers:
             self._session.headers["Referer"] = self.login_url
 
+    def _add_organization(self):
+        if (
+            "X-Organization" not in self._session.headers
+            and self._organization
+        ):
+            self._session.headers["X-Organization"] = self._organization
+
     def close(self):
         self._session.close()
 


Test patch: