label-studio-sdk 0.0.32__py3-none-any.whl → 0.0.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of label-studio-sdk might be problematic. Click here for more details.

Files changed (38) hide show
  1. label_studio_sdk/__init__.py +4 -1
  2. label_studio_sdk/client.py +94 -78
  3. label_studio_sdk/data_manager.py +32 -23
  4. label_studio_sdk/exceptions.py +10 -0
  5. label_studio_sdk/label_interface/__init__.py +1 -0
  6. label_studio_sdk/label_interface/base.py +77 -0
  7. label_studio_sdk/label_interface/control_tags.py +756 -0
  8. label_studio_sdk/label_interface/interface.py +922 -0
  9. label_studio_sdk/label_interface/label_tags.py +72 -0
  10. label_studio_sdk/label_interface/object_tags.py +292 -0
  11. label_studio_sdk/label_interface/region.py +43 -0
  12. label_studio_sdk/objects.py +35 -0
  13. label_studio_sdk/project.py +711 -258
  14. label_studio_sdk/schema/label_config_schema.json +226 -0
  15. label_studio_sdk/users.py +15 -13
  16. label_studio_sdk/utils.py +31 -30
  17. label_studio_sdk/workspaces.py +13 -11
  18. {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/METADATA +3 -1
  19. label_studio_sdk-0.0.34.dist-info/RECORD +37 -0
  20. {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/WHEEL +1 -1
  21. {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/top_level.txt +0 -1
  22. tests/test_client.py +21 -10
  23. tests/test_export.py +105 -0
  24. tests/test_interface/__init__.py +1 -0
  25. tests/test_interface/configs.py +137 -0
  26. tests/test_interface/mockups.py +22 -0
  27. tests/test_interface/test_compat.py +64 -0
  28. tests/test_interface/test_control_tags.py +55 -0
  29. tests/test_interface/test_data_generation.py +45 -0
  30. tests/test_interface/test_lpi.py +15 -0
  31. tests/test_interface/test_main.py +196 -0
  32. tests/test_interface/test_object_tags.py +36 -0
  33. tests/test_interface/test_region.py +36 -0
  34. tests/test_interface/test_validate_summary.py +35 -0
  35. tests/test_interface/test_validation.py +59 -0
  36. docs/__init__.py +0 -3
  37. label_studio_sdk-0.0.32.dist-info/RECORD +0 -15
  38. {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/LICENSE +0 -0
@@ -1,22 +1,23 @@
1
1
  """ .. include::../docs/project.md
2
2
  """
3
- import os
3
+
4
4
  import json
5
5
  import logging
6
+ import os
6
7
  import pathlib
7
8
  import time
8
-
9
9
  from enum import Enum, auto
10
- from random import sample, shuffle
11
- from requests.exceptions import HTTPError, InvalidSchema, MissingSchema
12
- from requests import Response
13
10
  from pathlib import Path
11
+ from random import sample, shuffle
14
12
  from typing import Optional, Union, List, Dict, Callable
15
- from .client import Client
16
- from .utils import parse_config, chunk
17
13
 
18
- from label_studio_tools.core.utils.io import get_local_path
19
14
  from label_studio_tools.core.label_config import parse_config
15
+ from label_studio_tools.core.utils.io import get_local_path
16
+ from requests import Response
17
+ from requests.exceptions import HTTPError, InvalidSchema, MissingSchema
18
+
19
+ from .client import Client
20
+ from .utils import parse_config, chunk
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
@@ -32,28 +33,28 @@ class LabelStudioAttributeError(LabelStudioException):
32
33
  class ProjectSampling(Enum):
33
34
  """Enumerate the available task sampling modes for labeling."""
34
35
 
35
- RANDOM = 'Uniform sampling'
36
+ RANDOM = "Uniform sampling"
36
37
  """ Uniform random sampling of tasks """
37
- SEQUENCE = 'Sequential sampling'
38
+ SEQUENCE = "Sequential sampling"
38
39
  """ Sequential sampling of tasks using task IDs """
39
- UNCERTAINTY = 'Uncertainty sampling'
40
+ UNCERTAINTY = "Uncertainty sampling"
40
41
  """ Sample tasks based on prediction scores, such as for active learning (Enterprise only)"""
41
42
 
42
43
 
43
44
  class ProjectStorage(Enum):
44
45
  """Enumerate the available types of external source and target storage for labeling projects."""
45
46
 
46
- GOOGLE = 'gcs'
47
+ GOOGLE = "gcs"
47
48
  """ Google Cloud Storage """
48
- S3 = 's3'
49
+ S3 = "s3"
49
50
  """ Amazon S3 Storage """
50
- AZURE = 'azure_blob'
51
+ AZURE = "azure_blob"
51
52
  """ Microsoft Azure Blob Storage """
52
- LOCAL = 'localfiles'
53
+ LOCAL = "localfiles"
53
54
  """ Label Studio Local File Storage """
54
- REDIS = 'redis'
55
+ REDIS = "redis"
55
56
  """ Redis Storage """
56
- S3_SECURED = 's3s'
57
+ S3_SECURED = "s3s"
57
58
  """ Amazon S3 Storage secured by IAM roles (Enterprise only) """
58
59
 
59
60
 
@@ -62,13 +63,13 @@ class AssignmentSamplingMethod(Enum):
62
63
 
63
64
 
64
65
  class ExportSnapshotStatus:
65
- CREATED = 'created'
66
+ CREATED = "created"
66
67
  """ Export snapshot is created """
67
- IN_PROGRESS = 'in_progress'
68
+ IN_PROGRESS = "in_progress"
68
69
  """ Export snapshot is in progress """
69
- FAILED = 'failed'
70
+ FAILED = "failed"
70
71
  """ Export snapshot failed with errors """
71
- COMPLETED = 'completed'
72
+ COMPLETED = "completed"
72
73
  """ Export snapshot was created and can be downloaded """
73
74
 
74
75
  def __init__(self, response):
@@ -77,30 +78,30 @@ class ExportSnapshotStatus:
77
78
  def is_created(self):
78
79
  """Export snapshot is created"""
79
80
  assert (
80
- 'status' in self.response
81
+ "status" in self.response
81
82
  ), '"status" field not found in export snapshot status response'
82
- return self.response['status'] == self.CREATED
83
+ return self.response["status"] == self.CREATED
83
84
 
84
85
  def is_in_progress(self):
85
86
  """Export snapshot is in progress"""
86
87
  assert (
87
- 'status' in self.response
88
+ "status" in self.response
88
89
  ), '"status" field not found in export_snapshot_status response'
89
- return self.response['status'] == self.IN_PROGRESS
90
+ return self.response["status"] == self.IN_PROGRESS
90
91
 
91
92
  def is_failed(self):
92
93
  """Export snapshot failed with errors"""
93
94
  assert (
94
- 'status' in self.response
95
+ "status" in self.response
95
96
  ), '"status" field not found in export_snapshot_status response'
96
- return self.response['status'] == self.FAILED
97
+ return self.response["status"] == self.FAILED
97
98
 
98
99
  def is_completed(self):
99
100
  """Export snapshot was created and can be downloaded"""
100
101
  assert (
101
- 'status' in self.response
102
+ "status" in self.response
102
103
  ), '"status" field not found in export_snapshot_status response'
103
- return self.response['status'] == self.COMPLETED
104
+ return self.response["status"] == self.COMPLETED
104
105
 
105
106
 
106
107
  class Project(Client):
@@ -161,10 +162,10 @@ class Project(Client):
161
162
  "Use get_users() instead."
162
163
  )
163
164
 
164
- response = self.make_request('GET', f'/api/projects/{self.id}/members')
165
+ response = self.make_request("GET", f"/api/projects/{self.id}/members")
165
166
  users = []
166
167
  for user_data in response.json():
167
- user_data['client'] = self
168
+ user_data["client"] = self
168
169
  users.append(User(**user_data))
169
170
  return users
170
171
 
@@ -181,9 +182,9 @@ class Project(Client):
181
182
  Dict with created member
182
183
 
183
184
  """
184
- payload = {'user': user.id}
185
+ payload = {"user": user.id}
185
186
  response = self.make_request(
186
- 'POST', f'/api/projects/{self.id}/members', json=payload
187
+ "POST", f"/api/projects/{self.id}/members", json=payload
187
188
  )
188
189
  return response.json()
189
190
 
@@ -201,20 +202,20 @@ class Project(Client):
201
202
  Dict with counter of created assignments
202
203
 
203
204
  """
204
- final_response = {'assignments': 0}
205
+ final_response = {"assignments": 0}
205
206
  users_ids = [user.id for user in users]
206
207
  # Assign tasks to users with batches
207
208
  for c in chunk(tasks_ids, 1000):
208
209
  logger.debug(f"Starting assignment for: {users_ids}")
209
210
  payload = {
210
- 'users': users_ids,
211
- 'selectedItems': {'all': False, 'included': c},
212
- 'type': 'AN',
211
+ "users": users_ids,
212
+ "selectedItems": {"all": False, "included": c},
213
+ "type": "AN",
213
214
  }
214
215
  response = self.make_request(
215
- 'POST', f'/api/projects/{self.id}/tasks/assignees', json=payload
216
+ "POST", f"/api/projects/{self.id}/tasks/assignees", json=payload
216
217
  )
217
- final_response['assignments'] += response.json()['assignments']
218
+ final_response["assignments"] += response.json()["assignments"]
218
219
  return final_response
219
220
 
220
221
  def delete_annotators_assignment(self, tasks_ids):
@@ -230,10 +231,10 @@ class Project(Client):
230
231
  Dict with counter of deleted annotator assignments
231
232
 
232
233
  """
233
- payload = {'selectedItems': {'all': False, 'included': tasks_ids}}
234
+ payload = {"selectedItems": {"all": False, "included": tasks_ids}}
234
235
  response = self.make_request(
235
- 'POST',
236
- f'/api/dm/actions?id=delete_annotators&project={self.id}',
236
+ "POST",
237
+ f"/api/dm/actions?id=delete_annotators&project={self.id}",
237
238
  json=payload,
238
239
  )
239
240
  return response.json()
@@ -251,10 +252,10 @@ class Project(Client):
251
252
  Dict with counter of deleted reviewer assignments
252
253
 
253
254
  """
254
- payload = {'selectedItems': {'all': False, 'included': tasks_ids}}
255
+ payload = {"selectedItems": {"all": False, "included": tasks_ids}}
255
256
  response = self.make_request(
256
- 'POST',
257
- f'/api/dm/actions?id=delete_reviewers&project={self.id}',
257
+ "POST",
258
+ f"/api/dm/actions?id=delete_reviewers&project={self.id}",
258
259
  json=payload,
259
260
  )
260
261
  return response.json()
@@ -274,12 +275,12 @@ class Project(Client):
274
275
 
275
276
  """
276
277
  payload = {
277
- 'users': [user.id for user in users],
278
- 'selectedItems': {'all': False, 'included': tasks_ids},
279
- 'type': 'RE',
278
+ "users": [user.id for user in users],
279
+ "selectedItems": {"all": False, "included": tasks_ids},
280
+ "type": "RE",
280
281
  }
281
282
  response = self.make_request(
282
- 'POST', f'/api/projects/{self.id}/tasks/assignees', json=payload
283
+ "POST", f"/api/projects/{self.id}/tasks/assignees", json=payload
283
284
  )
284
285
  return response.json()
285
286
 
@@ -354,7 +355,7 @@ class Project(Client):
354
355
  Retrieve and display predictions when loading a task
355
356
 
356
357
  """
357
- response = self.make_request('GET', f'/api/projects/{self.id}')
358
+ response = self.make_request("GET", f"/api/projects/{self.id}")
358
359
  return response.json()
359
360
 
360
361
  def get_model_versions(self):
@@ -366,7 +367,7 @@ class Project(Client):
366
367
  Model versions
367
368
 
368
369
  """
369
- response = self.make_request('GET', f'/api/projects/{self.id}/model-versions')
370
+ response = self.make_request("GET", f"/api/projects/{self.id}/model-versions")
370
371
  return response.json()
371
372
 
372
373
  def update_params(self):
@@ -434,11 +435,11 @@ class Project(Client):
434
435
  Raises LabelStudioException in case of errors.
435
436
 
436
437
  """
437
- response = self.make_request('POST', '/api/projects', json=kwargs)
438
+ response = self.make_request("POST", "/api/projects", json=kwargs)
438
439
  if response.status_code == 201:
439
440
  self.params = response.json()
440
441
  else:
441
- raise LabelStudioException('Project not created')
442
+ raise LabelStudioException("Project not created")
442
443
 
443
444
  @classmethod
444
445
  def _create_from_id(cls, client, project_id, params=None):
@@ -453,7 +454,7 @@ class Project(Client):
453
454
  if params and isinstance(params, dict):
454
455
  # TODO: validate project parameters
455
456
  project.params = params
456
- project.params['id'] = project_id
457
+ project.params["id"] = project_id
457
458
  return project
458
459
 
459
460
  @classmethod
@@ -494,13 +495,13 @@ class Project(Client):
494
495
  Imported task IDs
495
496
 
496
497
  """
497
- params = {'return_task_ids': '1'}
498
+ params = {"return_task_ids": "1"}
498
499
  if preannotated_from_fields:
499
- params['preannotated_from_fields'] = ','.join(preannotated_from_fields)
500
+ params["preannotated_from_fields"] = ",".join(preannotated_from_fields)
500
501
  if isinstance(tasks, (list, dict)):
501
502
  response = self.make_request(
502
- method='POST',
503
- url=f'/api/projects/{self.id}/import',
503
+ method="POST",
504
+ url=f"/api/projects/{self.id}/import",
504
505
  json=tasks,
505
506
  params=params,
506
507
  timeout=(10, 600),
@@ -508,12 +509,12 @@ class Project(Client):
508
509
  elif isinstance(tasks, (str, Path)):
509
510
  # try import from file
510
511
  if not os.path.isfile(tasks):
511
- raise LabelStudioException(f'Not found import tasks file {tasks}')
512
- with open(tasks, mode='rb') as f:
512
+ raise LabelStudioException(f"Not found import tasks file {tasks}")
513
+ with open(tasks, mode="rb") as f:
513
514
  response = self.make_request(
514
- method='POST',
515
- url=f'/api/projects/{self.id}/import',
516
- files={'file': f},
515
+ method="POST",
516
+ url=f"/api/projects/{self.id}/import",
517
+ files={"file": f},
517
518
  params=params,
518
519
  timeout=(10, 600),
519
520
  )
@@ -523,7 +524,7 @@ class Project(Client):
523
524
  )
524
525
  response = response.json()
525
526
 
526
- if 'import' in response:
527
+ if "import" in response:
527
528
  # check import status
528
529
  timeout = 300
529
530
  fibonacci_backoff = [1, 1]
@@ -532,18 +533,18 @@ class Project(Client):
532
533
 
533
534
  while True:
534
535
  import_status = self.make_request(
535
- method='GET',
536
+ method="GET",
536
537
  url=f'/api/projects/{self.id}/imports/{response["import"]}',
537
538
  ).json()
538
539
 
539
- if import_status['status'] == 'completed':
540
- return import_status['task_ids']
540
+ if import_status["status"] == "completed":
541
+ return import_status["task_ids"]
541
542
 
542
- if import_status['status'] == 'failed':
543
- raise LabelStudioException(import_status['error'])
543
+ if import_status["status"] == "failed":
544
+ raise LabelStudioException(import_status["error"])
544
545
 
545
546
  if time.time() - start_time >= timeout:
546
- raise LabelStudioException('Import timeout')
547
+ raise LabelStudioException("Import timeout")
547
548
 
548
549
  time.sleep(fibonacci_backoff[0])
549
550
  fibonacci_backoff = [
@@ -551,11 +552,11 @@ class Project(Client):
551
552
  fibonacci_backoff[0] + fibonacci_backoff[1],
552
553
  ]
553
554
 
554
- return response['task_ids']
555
+ return response["task_ids"]
555
556
 
556
557
  def export_tasks(
557
558
  self,
558
- export_type: str = 'JSON',
559
+ export_type: str = "JSON",
559
560
  download_all_tasks: bool = False,
560
561
  download_resources: bool = False,
561
562
  ids: Optional[List[int]] = None,
@@ -592,19 +593,19 @@ class Project(Client):
592
593
 
593
594
  """
594
595
  params = {
595
- 'exportType': export_type,
596
- 'download_all_tasks': download_all_tasks,
597
- 'download_resources': download_resources,
596
+ "exportType": export_type,
597
+ "download_all_tasks": download_all_tasks,
598
+ "download_resources": download_resources,
598
599
  }
599
600
  if ids:
600
- params['ids'] = ids
601
+ params["ids"] = ids
601
602
  response = self.make_request(
602
- method='GET', url=f'/api/projects/{self.id}/export', params=params
603
+ method="GET", url=f"/api/projects/{self.id}/export", params=params
603
604
  )
604
605
  if export_location is None:
605
- if 'JSON' not in export_type.upper():
606
+ if "JSON" not in export_type.upper():
606
607
  raise ValueError(
607
- f'{export_type} export type requires an export location to be specified'
608
+ f"{export_type} export type requires an export location to be specified"
608
609
  )
609
610
  return response.json()
610
611
 
@@ -623,7 +624,7 @@ class Project(Client):
623
624
 
624
625
  def set_params(self, **kwargs):
625
626
  """Low level function to set project parameters."""
626
- response = self.make_request('PATCH', f'/api/projects/{self.id}', json=kwargs)
627
+ response = self.make_request("PATCH", f"/api/projects/{self.id}", json=kwargs)
627
628
  assert response.status_code == 200
628
629
 
629
630
  def set_sampling(self, sampling: ProjectSampling):
@@ -705,7 +706,7 @@ class Project(Client):
705
706
  page = 1
706
707
  result = []
707
708
  data = {}
708
- while not data.get('end_pagination'):
709
+ while not data.get("end_pagination"):
709
710
  try:
710
711
  data = self.get_paginated_tasks(
711
712
  filters=filters,
@@ -716,10 +717,10 @@ class Project(Client):
716
717
  page=page,
717
718
  page_size=100,
718
719
  )
719
- result += data['tasks']
720
+ result += data["tasks"]
720
721
  page += 1
721
722
  except LabelStudioException as e:
722
- logger.debug(f'Error during pagination: {e}')
723
+ logger.debug(f"Error during pagination: {e}")
723
724
  break
724
725
  return result
725
726
 
@@ -730,7 +731,7 @@ class Project(Client):
730
731
  view_id=None,
731
732
  selected_ids=None,
732
733
  page: int = 1,
733
- page_size: int = -1,
734
+ page_size: int = 100,
734
735
  only_ids: bool = False,
735
736
  resolve_uri: bool = True,
736
737
  ):
@@ -768,7 +769,7 @@ class Project(Client):
768
769
  page: int
769
770
  Page. Default is 1.
770
771
  page_size: int
771
- Page size. Default is -1, to retrieve all tasks in the project.
772
+ Page size. Default is 100, to retrieve all tasks in the project you can use get_tasks().
772
773
  only_ids: bool
773
774
  If true, return only task IDs
774
775
  resolve_uri: bool
@@ -798,54 +799,56 @@ class Project(Client):
798
799
 
799
800
  """
800
801
  query = {
801
- 'filters': filters,
802
- 'ordering': ordering or [],
803
- 'selectedItems': {'all': False, 'included': selected_ids}
804
- if selected_ids
805
- else {'all': True, "excluded": []},
802
+ "filters": filters,
803
+ "ordering": ordering or [],
804
+ "selectedItems": (
805
+ {"all": False, "included": selected_ids}
806
+ if selected_ids
807
+ else {"all": True, "excluded": []}
808
+ ),
806
809
  }
807
810
  params = {
808
- 'project': self.id,
809
- 'page': page,
810
- 'page_size': page_size,
811
- 'view': view_id,
812
- 'query': json.dumps(query),
813
- 'fields': 'all',
814
- 'resolve_uri': resolve_uri,
811
+ "project": self.id,
812
+ "page": page,
813
+ "page_size": page_size,
814
+ "view": view_id,
815
+ "query": json.dumps(query),
816
+ "fields": "all",
817
+ "resolve_uri": resolve_uri,
815
818
  }
816
819
  if only_ids:
817
- params['include'] = 'id'
820
+ params["include"] = "id"
818
821
 
819
822
  response = self.make_request(
820
- 'GET', '/api/tasks', params, raise_exceptions=False
823
+ "GET", "/api/tasks", params, raise_exceptions=False
821
824
  )
822
825
  # we'll get 404 from API on empty page
823
826
  if response.status_code == 404:
824
- return {'tasks': [], 'end_pagination': True}
827
+ return {"tasks": [], "end_pagination": True}
825
828
  elif response.status_code != 200:
826
829
  self.log_response_error(response)
827
830
  try:
828
831
  response.raise_for_status()
829
832
  except HTTPError as e:
830
- raise LabelStudioException(f'Error loading tasks: {e}')
833
+ raise LabelStudioException(f"Error loading tasks: {e}")
831
834
 
832
835
  data = response.json()
833
- tasks = data['tasks']
836
+ tasks = data["tasks"]
834
837
  if only_ids:
835
- data['tasks'] = [task['id'] for task in tasks]
838
+ data["tasks"] = [task["id"] for task in tasks]
836
839
 
837
840
  return data
838
841
 
839
842
  def get_tasks_ids(self, *args, **kwargs):
840
843
  """Same as `label_studio_sdk.project.Project.get_tasks()` but returns only task IDs."""
841
- kwargs['only_ids'] = True
844
+ kwargs["only_ids"] = True
842
845
  return self.get_tasks(*args, **kwargs)
843
846
 
844
847
  def get_paginated_tasks_ids(self, *args, **kwargs):
845
848
  """Same as `label_studio_sdk.project.Project.get_paginated_tasks()` but returns
846
849
  only task IDs.
847
850
  """
848
- kwargs['only_ids'] = True
851
+ kwargs["only_ids"] = True
849
852
  return self.get_paginated_tasks(*args, **kwargs)
850
853
 
851
854
  def get_views(self):
@@ -866,10 +869,10 @@ class Project(Client):
866
869
  data: dict
867
870
  Filters, orderings and other visual settings
868
871
  """
869
- response = self.make_request('GET', f'/api/dm/views?project={self.id}')
872
+ response = self.make_request("GET", f"/api/dm/views?project={self.id}")
870
873
  return response.json()
871
874
 
872
- def create_view(self, filters, ordering=None, title='Tasks'):
875
+ def create_view(self, filters, ordering=None, title="Tasks"):
873
876
  """Create view
874
877
 
875
878
  Parameters
@@ -890,12 +893,29 @@ class Project(Client):
890
893
 
891
894
  """
892
895
  data = {
893
- 'project': self.id,
894
- 'data': {'title': title, 'ordering': ordering, 'filters': filters},
896
+ "project": self.id,
897
+ "data": {"title": title, "ordering": ordering, "filters": filters},
895
898
  }
896
- response = self.make_request('POST', '/api/dm/views', json=data)
899
+ response = self.make_request("POST", "/api/dm/views", json=data)
897
900
  return response.json()
898
901
 
902
+ def delete_view(self, view_id):
903
+ """Delete view
904
+
905
+ Parameters
906
+ ----------
907
+ view_id: int
908
+ View ID
909
+
910
+ Returns
911
+ -------
912
+ dict:
913
+ dict with deleted view
914
+
915
+ """
916
+ response = self.make_request("DELETE", f"/api/dm/views/{view_id}")
917
+ return
918
+
899
919
  @property
900
920
  def tasks(self):
901
921
  """Retrieve all tasks from the project. This call can be very slow if the project has a lot of tasks."""
@@ -922,13 +942,13 @@ class Project(Client):
922
942
  """
923
943
  return self.get_tasks(
924
944
  filters={
925
- 'conjunction': 'and',
926
- 'items': [
945
+ "conjunction": "and",
946
+ "items": [
927
947
  {
928
- 'filter': 'filter:tasks:completed_at',
929
- 'operator': 'empty',
930
- 'value': False,
931
- 'type': 'Datetime',
948
+ "filter": "filter:tasks:completed_at",
949
+ "operator": "empty",
950
+ "value": False,
951
+ "type": "Datetime",
932
952
  }
933
953
  ],
934
954
  },
@@ -963,13 +983,13 @@ class Project(Client):
963
983
  """
964
984
  return self.get_tasks(
965
985
  filters={
966
- 'conjunction': 'and',
967
- 'items': [
986
+ "conjunction": "and",
987
+ "items": [
968
988
  {
969
- 'filter': 'filter:tasks:completed_at',
970
- 'operator': 'empty',
971
- 'value': True,
972
- 'type': 'Datetime',
989
+ "filter": "filter:tasks:completed_at",
990
+ "operator": "empty",
991
+ "value": True,
992
+ "type": "Datetime",
973
993
  }
974
994
  ],
975
995
  },
@@ -1029,7 +1049,7 @@ class Project(Client):
1029
1049
  Uploaded file used as data source for this task
1030
1050
  ```
1031
1051
  """
1032
- response = self.make_request('GET', f'/api/tasks/{task_id}')
1052
+ response = self.make_request("GET", f"/api/tasks/{task_id}")
1033
1053
  return response.json()
1034
1054
 
1035
1055
  def update_task(self, task_id, **kwargs):
@@ -1048,7 +1068,7 @@ class Project(Client):
1048
1068
  Dict with updated task
1049
1069
 
1050
1070
  """
1051
- response = self.make_request('PATCH', f'/api/tasks/{task_id}', json=kwargs)
1071
+ response = self.make_request("PATCH", f"/api/tasks/{task_id}", json=kwargs)
1052
1072
  response.raise_for_status()
1053
1073
  return response.json()
1054
1074
 
@@ -1102,11 +1122,13 @@ class Project(Client):
1102
1122
  model_version: str
1103
1123
  Any string identifying your model
1104
1124
  """
1105
- data = {'task': task_id, 'result': result, 'score': score}
1125
+ data = {"task": task_id, "result": result, "score": score}
1106
1126
  if model_version is not None:
1107
- data['model_version'] = model_version
1108
- response = self.make_request('POST', '/api/predictions', json=data)
1109
- return response.json()
1127
+ data["model_version"] = model_version
1128
+ response = self.make_request("POST", "/api/predictions", json=data)
1129
+ json = response.json()
1130
+ logger.debug(f"Response: {json}")
1131
+ return json
1110
1132
 
1111
1133
  def create_predictions(self, predictions):
1112
1134
  """Bulk create predictions for tasks. See <a href="https://labelstud.io/guide/predictions.html">more
@@ -1119,7 +1141,7 @@ class Project(Client):
1119
1141
  Label Studio JSON format as for annotations</a>.
1120
1142
  """
1121
1143
  response = self.make_request(
1122
- 'POST', f'/api/projects/{self.id}/import/predictions', json=predictions
1144
+ "POST", f"/api/projects/{self.id}/import/predictions", json=predictions
1123
1145
  )
1124
1146
  return response.json()
1125
1147
 
@@ -1138,20 +1160,37 @@ class Project(Client):
1138
1160
 
1139
1161
  """
1140
1162
  payload = {
1141
- 'filters': {'conjunction': 'and', 'items': []},
1142
- 'model_version': model_versions,
1143
- 'ordering': [],
1144
- 'project': self.id,
1145
- 'selectedItems': {'all': True, 'excluded': []},
1163
+ "filters": {"conjunction": "and", "items": []},
1164
+ "model_version": model_versions,
1165
+ "ordering": [],
1166
+ "project": self.id,
1167
+ "selectedItems": {"all": True, "excluded": []},
1146
1168
  }
1147
1169
  response = self.make_request(
1148
- 'POST',
1149
- '/api/dm/actions',
1150
- params={'id': 'predictions_to_annotations', 'project': self.id},
1170
+ "POST",
1171
+ "/api/dm/actions",
1172
+ params={"id": "predictions_to_annotations", "project": self.id},
1151
1173
  json=payload,
1152
1174
  )
1153
1175
  return response.json()
1154
1176
 
1177
+ def list_annotations(self, task_id: int) -> List:
1178
+ """List all annotations for a task.
1179
+
1180
+ Parameters
1181
+ ----------
1182
+ task_id: int
1183
+ Task ID
1184
+
1185
+ Returns
1186
+ -------
1187
+ list of dict:
1188
+ List of annotations objects
1189
+ """
1190
+ response = self.make_request("GET", f"/api/tasks/{task_id}/annotations")
1191
+ response.raise_for_status()
1192
+ return response.json()
1193
+
1155
1194
  def create_annotation(self, task_id: int, **kwargs) -> Dict:
1156
1195
  """Add annotations to a task like an annotator does.
1157
1196
 
@@ -1170,11 +1209,28 @@ class Project(Client):
1170
1209
 
1171
1210
  """
1172
1211
  response = self.make_request(
1173
- 'POST', f'/api/tasks/{task_id}/annotations/', json=kwargs
1212
+ "POST", f"/api/tasks/{task_id}/annotations/", json=kwargs
1174
1213
  )
1175
1214
  response.raise_for_status()
1176
1215
  return response.json()
1177
1216
 
1217
+ def get_annotation(self, annotation_id: int) -> dict:
1218
+ """Retrieve a specific annotation for a task using the annotation ID.
1219
+
1220
+ Parameters
1221
+ ----------
1222
+ annotation_id: int
1223
+ A unique integer value identifying this annotation.
1224
+
1225
+ Returns
1226
+ ----------
1227
+ dict
1228
+ Retreived annotation object
1229
+ """
1230
+ response = self.make_request("GET", f"/api/annotations/{annotation_id}")
1231
+ response.raise_for_status()
1232
+ return response.json()
1233
+
1178
1234
  def update_annotation(self, annotation_id, **kwargs):
1179
1235
  """Update specific annotation with new annotation parameters, e.g.
1180
1236
  ```
@@ -1195,11 +1251,29 @@ class Project(Client):
1195
1251
 
1196
1252
  """
1197
1253
  response = self.make_request(
1198
- 'PATCH', f'/api/annotations/{annotation_id}', json=kwargs
1254
+ "PATCH", f"/api/annotations/{annotation_id}", json=kwargs
1199
1255
  )
1200
1256
  response.raise_for_status()
1201
1257
  return response.json()
1202
1258
 
1259
+ def delete_annotation(self, annotation_id: int) -> int:
1260
+ """Delete an annotation using the annotation ID. This action can't be undone!
1261
+
1262
+ Parameters
1263
+ ----------
1264
+ annotation_id: int
1265
+ A unique integer value identifying this annotation.
1266
+
1267
+ Returns
1268
+ ----------
1269
+ int
1270
+ Status code for operation
1271
+
1272
+ """
1273
+ response = self.make_request("DELETE", f"/api/annotations/{annotation_id}")
1274
+ response.raise_for_status()
1275
+ return response.status_code
1276
+
1203
1277
  def get_predictions_coverage(self):
1204
1278
  """Prediction coverage stats for all model versions for the project.
1205
1279
 
@@ -1218,7 +1292,7 @@ class Project(Client):
1218
1292
  """
1219
1293
  model_versions = self.get_model_versions()
1220
1294
  params = self.get_params()
1221
- tasks_number = params['task_number']
1295
+ tasks_number = params["task_number"]
1222
1296
  coverage = {
1223
1297
  model_version: count / tasks_number
1224
1298
  for model_version, count in model_versions.items()
@@ -1240,8 +1314,8 @@ class Project(Client):
1240
1314
  google_application_credentials: Optional[str] = None,
1241
1315
  presign: Optional[bool] = True,
1242
1316
  presign_ttl: Optional[int] = 1,
1243
- title: Optional[str] = '',
1244
- description: Optional[str] = '',
1317
+ title: Optional[str] = "",
1318
+ description: Optional[str] = "",
1245
1319
  ):
1246
1320
  """Connect a Google Cloud Storage (GCS) bucket to Label Studio to use as source storage and import tasks.
1247
1321
 
@@ -1283,23 +1357,25 @@ class Project(Client):
1283
1357
  Number of tasks synced in the last sync
1284
1358
 
1285
1359
  """
1286
- if os.path.isfile(google_application_credentials):
1360
+ if google_application_credentials and os.path.isfile(
1361
+ google_application_credentials
1362
+ ):
1287
1363
  with open(google_application_credentials) as f:
1288
1364
  google_application_credentials = f.read()
1289
1365
 
1290
1366
  payload = {
1291
- 'bucket': bucket,
1292
- 'project': self.id,
1293
- 'prefix': prefix,
1294
- 'regex_filter': regex_filter,
1295
- 'use_blob_urls': use_blob_urls,
1296
- 'google_application_credentials': google_application_credentials,
1297
- 'presign': presign,
1298
- 'presign_ttl': presign_ttl,
1299
- 'title': title,
1300
- 'description': description,
1367
+ "bucket": bucket,
1368
+ "project": self.id,
1369
+ "prefix": prefix,
1370
+ "regex_filter": regex_filter,
1371
+ "use_blob_urls": use_blob_urls,
1372
+ "google_application_credentials": google_application_credentials,
1373
+ "presign": presign,
1374
+ "presign_ttl": presign_ttl,
1375
+ "title": title,
1376
+ "description": description,
1301
1377
  }
1302
- response = self.make_request('POST', '/api/storages/gcs', json=payload)
1378
+ response = self.make_request("POST", "/api/storages/gcs", json=payload)
1303
1379
  return response.json()
1304
1380
 
1305
1381
  def connect_google_export_storage(
@@ -1307,8 +1383,8 @@ class Project(Client):
1307
1383
  bucket: str,
1308
1384
  prefix: Optional[str] = None,
1309
1385
  google_application_credentials: Optional[str] = None,
1310
- title: Optional[str] = '',
1311
- description: Optional[str] = '',
1386
+ title: Optional[str] = "",
1387
+ description: Optional[str] = "",
1312
1388
  can_delete_objects: bool = False,
1313
1389
  ):
1314
1390
  """Connect a Google Cloud Storage (GCS) bucket to Label Studio to use as target storage and export tasks.
@@ -1350,15 +1426,15 @@ class Project(Client):
1350
1426
  google_application_credentials = f.read()
1351
1427
 
1352
1428
  payload = {
1353
- 'bucket': bucket,
1354
- 'prefix': prefix,
1355
- 'google_application_credentials': google_application_credentials,
1356
- 'title': title,
1357
- 'description': description,
1358
- 'can_delete_objects': can_delete_objects,
1359
- 'project': self.id,
1429
+ "bucket": bucket,
1430
+ "prefix": prefix,
1431
+ "google_application_credentials": google_application_credentials,
1432
+ "title": title,
1433
+ "description": description,
1434
+ "can_delete_objects": can_delete_objects,
1435
+ "project": self.id,
1360
1436
  }
1361
- response = self.make_request('POST', '/api/storages/export/gcs', json=payload)
1437
+ response = self.make_request("POST", "/api/storages/export/gcs", json=payload)
1362
1438
  return response.json()
1363
1439
 
1364
1440
  def connect_s3_import_storage(
@@ -1369,13 +1445,14 @@ class Project(Client):
1369
1445
  use_blob_urls: Optional[bool] = True,
1370
1446
  presign: Optional[bool] = True,
1371
1447
  presign_ttl: Optional[int] = 1,
1372
- title: Optional[str] = '',
1373
- description: Optional[str] = '',
1448
+ title: Optional[str] = "",
1449
+ description: Optional[str] = "",
1374
1450
  aws_access_key_id: Optional[str] = None,
1375
1451
  aws_secret_access_key: Optional[str] = None,
1376
1452
  aws_session_token: Optional[str] = None,
1377
1453
  region_name: Optional[str] = None,
1378
1454
  s3_endpoint: Optional[str] = None,
1455
+ recursive_scan: Optional[bool] = False,
1379
1456
  ):
1380
1457
  """Connect an Amazon S3 bucket to Label Studio to use as source storage and import tasks.
1381
1458
 
@@ -1407,6 +1484,8 @@ class Project(Client):
1407
1484
  Optional, specify the AWS region of your S3 bucket.
1408
1485
  s3_endpoint: string
1409
1486
  Optional, specify an S3 endpoint URL to use to access your bucket instead of the standard access method.
1487
+ recursive_scan: bool
1488
+ Optional, specify whether to perform recursive scan over the bucket content.
1410
1489
 
1411
1490
  Returns
1412
1491
  -------
@@ -1425,30 +1504,113 @@ class Project(Client):
1425
1504
  Number of tasks synced in the last sync
1426
1505
  """
1427
1506
  payload = {
1428
- 'bucket': bucket,
1429
- 'prefix': prefix,
1430
- 'regex_filter': regex_filter,
1431
- 'use_blob_urls': use_blob_urls,
1432
- 'aws_access_key_id': aws_access_key_id,
1433
- 'aws_secret_access_key': aws_secret_access_key,
1434
- 'aws_session_token': aws_session_token,
1435
- 'region_name': region_name,
1436
- 's3_endpoint': s3_endpoint,
1437
- 'presign': presign,
1438
- 'presign_ttl': presign_ttl,
1439
- 'title': title,
1440
- 'description': description,
1441
- 'project': self.id,
1507
+ "bucket": bucket,
1508
+ "prefix": prefix,
1509
+ "regex_filter": regex_filter,
1510
+ "use_blob_urls": use_blob_urls,
1511
+ "aws_access_key_id": aws_access_key_id,
1512
+ "aws_secret_access_key": aws_secret_access_key,
1513
+ "aws_session_token": aws_session_token,
1514
+ "region_name": region_name,
1515
+ "s3_endpoint": s3_endpoint,
1516
+ "presign": presign,
1517
+ "presign_ttl": presign_ttl,
1518
+ "title": title,
1519
+ "description": description,
1520
+ "project": self.id,
1521
+ "recursive_scan": recursive_scan,
1522
+ }
1523
+ response = self.make_request("POST", "/api/storages/s3", json=payload)
1524
+ return response.json()
1525
+
1526
+ def connect_s3s_iam_import_storage(
1527
+ self,
1528
+ role_arn: str,
1529
+ external_id: Optional[str] = None,
1530
+ bucket: Optional[str] = None,
1531
+ prefix: Optional[str] = None,
1532
+ regex_filter: Optional[str] = None,
1533
+ use_blob_urls: Optional[bool] = True,
1534
+ presign: Optional[bool] = True,
1535
+ presign_ttl: Optional[int] = 1,
1536
+ title: Optional[str] = "",
1537
+ description: Optional[str] = "",
1538
+ region_name: Optional[str] = None,
1539
+ s3_endpoint: Optional[str] = None,
1540
+ recursive_scan: Optional[bool] = False,
1541
+ aws_sse_kms_key_id: Optional[str] = None,
1542
+ ):
1543
+ """Create S3 secured import storage with IAM role access. Enterprise only.
1544
+
1545
+ Parameters
1546
+ ----------
1547
+ role_arn: string
1548
+ Required, specify the AWS Role ARN to assume.
1549
+ external_id: string or None
1550
+ Optional, specify the external ID to use to assume the role. If None, SDK will call api/organizations/<id>
1551
+ and use external_id from the response. You can find this ID on the organization page in the Label Studio UI.
1552
+ bucket: string
1553
+ Specify the name of the S3 bucket.
1554
+ prefix: string
1555
+ Optional, specify the prefix within the S3 bucket to import your data from.
1556
+ regex_filter: string
1557
+ Optional, specify a regex filter to use to match the file types of your data.
1558
+ use_blob_urls: bool
1559
+ Optional, true by default. Specify whether your data is raw image or video data, or JSON tasks.
1560
+ presign: bool
1561
+ Optional, true by default. Specify whether or not to create presigned URLs.
1562
+ presign_ttl: int
1563
+ Optional, 1 by default. Specify how long to keep presigned URLs active.
1564
+ title: string
1565
+ Optional, specify a title for your S3 import storage that appears in Label Studio.
1566
+ description: string
1567
+ Optional, specify a description for your S3 import storage.
1568
+ region_name: string
1569
+ Optional, specify the AWS region of your S3 bucket.
1570
+ s3_endpoint: string
1571
+ Optional, specify an S3 endpoint URL to use to access your bucket instead of the standard access method.
1572
+ recursive_scan: bool
1573
+ Optional, specify whether to perform recursive scan over the bucket content.
1574
+ aws_sse_kms_key_id: string
1575
+ Optional, specify an AWS SSE KMS Key ID for server-side encryption.
1576
+ synchronizable, last_sync, last_sync_count, last_sync_job, status, traceback, meta:
1577
+ Parameters for synchronization details and storage status.
1578
+
1579
+ Returns
1580
+ -------
1581
+ dict:
1582
+ containing the response from the API including storage ID and type, among other details.
1583
+ """
1584
+ if external_id is None:
1585
+ organization = self.get_organization()
1586
+ external_id = organization["external_id"]
1587
+
1588
+ payload = {
1589
+ "bucket": bucket,
1590
+ "prefix": prefix,
1591
+ "regex_filter": regex_filter,
1592
+ "use_blob_urls": use_blob_urls,
1593
+ "presign": presign,
1594
+ "presign_ttl": presign_ttl,
1595
+ "title": title,
1596
+ "description": description,
1597
+ "recursive_scan": recursive_scan,
1598
+ "role_arn": role_arn,
1599
+ "region_name": region_name,
1600
+ "s3_endpoint": s3_endpoint,
1601
+ "aws_sse_kms_key_id": aws_sse_kms_key_id,
1602
+ "project": self.id,
1603
+ "external_id": external_id,
1442
1604
  }
1443
- response = self.make_request('POST', '/api/storages/s3', json=payload)
1605
+ response = self.make_request("POST", "/api/storages/s3s/", json=payload)
1444
1606
  return response.json()
1445
1607
 
1446
1608
  def connect_s3_export_storage(
1447
1609
  self,
1448
1610
  bucket: str,
1449
1611
  prefix: Optional[str] = None,
1450
- title: Optional[str] = '',
1451
- description: Optional[str] = '',
1612
+ title: Optional[str] = "",
1613
+ description: Optional[str] = "",
1452
1614
  aws_access_key_id: Optional[str] = None,
1453
1615
  aws_secret_access_key: Optional[str] = None,
1454
1616
  aws_session_token: Optional[str] = None,
@@ -1499,19 +1661,19 @@ class Project(Client):
1499
1661
  """
1500
1662
 
1501
1663
  payload = {
1502
- 'bucket': bucket,
1503
- 'prefix': prefix,
1504
- 'aws_access_key_id': aws_access_key_id,
1505
- 'aws_secret_access_key': aws_secret_access_key,
1506
- 'aws_session_token': aws_session_token,
1507
- 'region_name': region_name,
1508
- 's3_endpoint': s3_endpoint,
1509
- 'title': title,
1510
- 'description': description,
1511
- 'can_delete_objects': can_delete_objects,
1512
- 'project': self.id,
1664
+ "bucket": bucket,
1665
+ "prefix": prefix,
1666
+ "aws_access_key_id": aws_access_key_id,
1667
+ "aws_secret_access_key": aws_secret_access_key,
1668
+ "aws_session_token": aws_session_token,
1669
+ "region_name": region_name,
1670
+ "s3_endpoint": s3_endpoint,
1671
+ "title": title,
1672
+ "description": description,
1673
+ "can_delete_objects": can_delete_objects,
1674
+ "project": self.id,
1513
1675
  }
1514
- response = self.make_request('POST', '/api/storages/export/s3', json=payload)
1676
+ response = self.make_request("POST", "/api/storages/export/s3", json=payload)
1515
1677
  return response.json()
1516
1678
 
1517
1679
  def connect_azure_import_storage(
@@ -1522,8 +1684,8 @@ class Project(Client):
1522
1684
  use_blob_urls: Optional[bool] = True,
1523
1685
  presign: Optional[bool] = True,
1524
1686
  presign_ttl: Optional[int] = 1,
1525
- title: Optional[str] = '',
1526
- description: Optional[str] = '',
1687
+ title: Optional[str] = "",
1688
+ description: Optional[str] = "",
1527
1689
  account_name: Optional[str] = None,
1528
1690
  account_key: Optional[str] = None,
1529
1691
  ):
@@ -1569,27 +1731,27 @@ class Project(Client):
1569
1731
  Number of tasks synced in the last sync
1570
1732
  """
1571
1733
  payload = {
1572
- 'container': container,
1573
- 'prefix': prefix,
1574
- 'regex_filter': regex_filter,
1575
- 'use_blob_urls': use_blob_urls,
1576
- 'account_name': account_name,
1577
- 'account_key': account_key,
1578
- 'presign': presign,
1579
- 'presign_ttl': presign_ttl,
1580
- 'title': title,
1581
- 'description': description,
1582
- 'project': self.id,
1734
+ "container": container,
1735
+ "prefix": prefix,
1736
+ "regex_filter": regex_filter,
1737
+ "use_blob_urls": use_blob_urls,
1738
+ "account_name": account_name,
1739
+ "account_key": account_key,
1740
+ "presign": presign,
1741
+ "presign_ttl": presign_ttl,
1742
+ "title": title,
1743
+ "description": description,
1744
+ "project": self.id,
1583
1745
  }
1584
- response = self.make_request('POST', '/api/storages/azure', json=payload)
1746
+ response = self.make_request("POST", "/api/storages/azure", json=payload)
1585
1747
  return response.json()
1586
1748
 
1587
1749
  def connect_azure_export_storage(
1588
1750
  self,
1589
1751
  container: str,
1590
1752
  prefix: Optional[str] = None,
1591
- title: Optional[str] = '',
1592
- description: Optional[str] = '',
1753
+ title: Optional[str] = "",
1754
+ description: Optional[str] = "",
1593
1755
  account_name: Optional[str] = None,
1594
1756
  account_key: Optional[str] = None,
1595
1757
  can_delete_objects: bool = False,
@@ -1630,16 +1792,16 @@ class Project(Client):
1630
1792
  Number of tasks synced in the last sync
1631
1793
  """
1632
1794
  payload = {
1633
- 'container': container,
1634
- 'prefix': prefix,
1635
- 'account_name': account_name,
1636
- 'account_key': account_key,
1637
- 'title': title,
1638
- 'description': description,
1639
- 'can_delete_objects': can_delete_objects,
1640
- 'project': self.id,
1795
+ "container": container,
1796
+ "prefix": prefix,
1797
+ "account_name": account_name,
1798
+ "account_key": account_key,
1799
+ "title": title,
1800
+ "description": description,
1801
+ "can_delete_objects": can_delete_objects,
1802
+ "project": self.id,
1641
1803
  }
1642
- response = self.make_request('POST', '/api/storages/export/azure', json=payload)
1804
+ response = self.make_request("POST", "/api/storages/export/azure", json=payload)
1643
1805
  return response.json()
1644
1806
 
1645
1807
  def connect_local_import_storage(
@@ -1647,8 +1809,8 @@ class Project(Client):
1647
1809
  local_store_path: [str],
1648
1810
  regex_filter: Optional[str] = None,
1649
1811
  use_blob_urls: Optional[bool] = True,
1650
- title: Optional[str] = '',
1651
- description: Optional[str] = '',
1812
+ title: Optional[str] = "",
1813
+ description: Optional[str] = "",
1652
1814
  ):
1653
1815
  """Connect a Local storage to Label Studio to use as source storage and import tasks.
1654
1816
  Parameters
@@ -1678,37 +1840,233 @@ class Project(Client):
1678
1840
  last_sync_count: int
1679
1841
  Number of tasks synced in the last sync
1680
1842
  """
1681
- if 'LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT' not in os.environ:
1843
+ if "LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT" not in os.environ:
1682
1844
  raise ValueError(
1683
- 'To use connect_local_import_storage() you should set '
1684
- 'LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT environment variable, '
1685
- 'read more: https://labelstud.io/guide/storage.html#Prerequisites-2'
1845
+ "To use connect_local_import_storage() you should set "
1846
+ "LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT environment variable, "
1847
+ "read more: https://labelstud.io/guide/storage.html#Prerequisites-2"
1686
1848
  )
1687
- root = os.environ['LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT']
1849
+ root = os.environ["LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT"]
1688
1850
 
1689
1851
  if not os.path.isdir(local_store_path):
1690
- raise ValueError(f'{local_store_path} is not a directory')
1852
+ raise ValueError(f"{local_store_path} is not a directory")
1691
1853
  if (Path(root) in Path(local_store_path).parents) is False:
1692
1854
  raise ValueError(
1693
- f'{str(Path(root))} is not presented in local_store_path parents: '
1694
- f'{str(Path(local_store_path).parents)}'
1855
+ f"{str(Path(root))} is not presented in local_store_path parents: "
1856
+ f"{str(Path(local_store_path).parents)}"
1695
1857
  )
1696
1858
 
1697
1859
  payload = {
1698
- 'regex_filter': regex_filter,
1699
- 'use_blob_urls': use_blob_urls,
1700
- 'path': local_store_path,
1701
- 'presign': False,
1702
- 'presign_ttl': 1,
1703
- 'title': title,
1704
- 'description': description,
1705
- 'project': self.id,
1860
+ "regex_filter": regex_filter,
1861
+ "use_blob_urls": use_blob_urls,
1862
+ "path": local_store_path,
1863
+ "presign": False,
1864
+ "presign_ttl": 1,
1865
+ "title": title,
1866
+ "description": description,
1867
+ "project": self.id,
1706
1868
  }
1707
1869
  response = self.make_request(
1708
- 'POST', f'/api/storages/localfiles?project={self.id}', json=payload
1870
+ "POST", f"/api/storages/localfiles?project={self.id}", json=payload
1871
+ )
1872
+ return response.json()
1873
+
1874
+ def sync_import_storage(self, storage_type, storage_id):
1875
+ """Synchronize Import (Source) Cloud Storage.
1876
+
1877
+ Parameters
1878
+ ----------
1879
+ storage_type: string
1880
+ Specify the type of the storage container. See ProjectStorage for available types.
1881
+ storage_id: int
1882
+ Specify the storage ID of the storage container. See get_import_storages() to get ids.
1883
+
1884
+ Returns
1885
+ -------
1886
+ dict:
1887
+ containing the same fields as in the original storage request and:
1888
+
1889
+ id: int
1890
+ Storage ID
1891
+ type: str
1892
+ Type of storage
1893
+ created_at: str
1894
+ Creation time
1895
+ last_sync: str
1896
+ Time last sync finished, can be empty.
1897
+ last_sync_count: int
1898
+ Number of tasks synced in the last sync
1899
+ """
1900
+ # originally syn was implemented in Client class, keep it for compatibility
1901
+ response = self.make_request(
1902
+ "POST", f"/api/storages/{storage_type}/{str(storage_id)}/sync"
1709
1903
  )
1710
1904
  return response.json()
1711
1905
 
1906
+ # write func for syn export storage
1907
+ def sync_export_storage(self, storage_type, storage_id):
1908
+ """Synchronize Export (Target) Cloud Storage.
1909
+
1910
+ Parameters
1911
+ ----------
1912
+ storage_type: string
1913
+ Specify the type of the storage container. See ProjectStorage for available types.
1914
+ storage_id: int
1915
+ Specify the storage ID of the storage container. See get_export_storages() to get ids.
1916
+
1917
+ Returns
1918
+ -------
1919
+ dict:
1920
+ containing the same fields as in the original storage request and:
1921
+
1922
+ id: int
1923
+ Storage ID
1924
+ type: str
1925
+ Type of storage
1926
+ created_at: str
1927
+ Creation time
1928
+ other fields:
1929
+ See more https://api.labelstud.io/#tag/Storage:S3/operation/api_storages_export_s3_sync_create
1930
+ """
1931
+ response = self.make_request(
1932
+ "POST", f"/api/storages/export/{storage_type}/{str(storage_id)}/sync"
1933
+ )
1934
+ return response.json()
1935
+
1936
+ # write code for get_import_storages()
1937
+ def get_import_storages(self):
1938
+ """Get Import (Source) Cloud Storage.
1939
+
1940
+ Returns
1941
+ -------
1942
+ list of dicts:
1943
+ List of dicts with source storages, each dict consists of these fields:
1944
+
1945
+ -------
1946
+ Each dict consists of these fields:
1947
+
1948
+ id : int
1949
+ A unique integer value identifying this storage.
1950
+ type : str
1951
+ The type of the storage. Default is "s3".
1952
+ synchronizable : bool
1953
+ Indicates if the storage is synchronizable. Default is True.
1954
+ presign : bool
1955
+ Indicates if the storage is presign. Default is True.
1956
+ last_sync : str or None
1957
+ The last sync finished time. Can be None.
1958
+ last_sync_count : int or None
1959
+ The count of tasks synced last time. Can be None.
1960
+ last_sync_job : str or None
1961
+ The last sync job ID. Can be None.
1962
+ status : str
1963
+ The status of the storage. Can be one of "initialized", "queued", "in_progress", "failed", "completed".
1964
+ traceback : str or None
1965
+ The traceback report for the last failed sync. Can be None.
1966
+ meta : dict or None
1967
+ Meta and debug information about storage processes. Can be None.
1968
+ title : str or None
1969
+ The title of the cloud storage. Can be None.
1970
+ description : str or None
1971
+ The description of the cloud storage. Can be None.
1972
+ created_at : str
1973
+ The creation time of the storage.
1974
+ bucket : str or None
1975
+ The S3 bucket name. Can be None.
1976
+ prefix : str or None
1977
+ The S3 bucket prefix. Can be None.
1978
+ regex_filter : str or None
1979
+ The cloud storage regex for filtering objects. Can be None.
1980
+ use_blob_urls : bool
1981
+ Indicates if objects are interpreted as BLOBs and generate URLs.
1982
+ aws_access_key_id : str or None
1983
+ The AWS_ACCESS_KEY_ID. Can be None.
1984
+ aws_secret_access_key : str or None
1985
+ The AWS_SECRET_ACCESS_KEY. Can be None.
1986
+ aws_session_token : str or None
1987
+ The AWS_SESSION_TOKEN. Can be None.
1988
+ aws_sse_kms_key_id : str or None
1989
+ The AWS SSE KMS Key ID. Can be None.
1990
+ region_name : str or None
1991
+ The AWS Region. Can be None.
1992
+ s3_endpoint : str or None
1993
+ The S3 Endpoint. Can be None.
1994
+ presign_ttl : int
1995
+ The presigned URLs TTL (in minutes).
1996
+ recursive_scan : bool
1997
+ Indicates if a recursive scan over the bucket content is performed.
1998
+ glob_pattern : str or None
1999
+ The glob pattern for syncing from bucket. Can be None.
2000
+ synced : bool
2001
+ Flag indicating if the dataset has been previously synced or not.
2002
+
2003
+ """
2004
+ response = self.make_request("GET", f"/api/storages/?project={self.id}")
2005
+ return response.json()
2006
+
2007
+ def get_export_storages(self):
2008
+ """Get Export (Target) Cloud Storage.
2009
+
2010
+ Returns
2011
+ -------
2012
+ list of dicts:
2013
+ List of dicts with target storages
2014
+
2015
+ -------
2016
+ Each dict consists of these fields:
2017
+
2018
+ id : int
2019
+ A unique integer value identifying this storage.
2020
+ type : str
2021
+ The type of the storage. Default is "s3".
2022
+ synchronizable : bool
2023
+ Indicates if the storage is synchronizable. Default is True.
2024
+ last_sync : str or None
2025
+ The last sync finished time. Can be None.
2026
+ last_sync_count : int or None
2027
+ The count of tasks synced last time. Can be None.
2028
+ last_sync_job : str or None
2029
+ The last sync job ID. Can be None.
2030
+ status : str
2031
+ The status of the storage. Can be one of "initialized", "queued", "in_progress", "failed", "completed".
2032
+ traceback : str or None
2033
+ The traceback report for the last failed sync. Can be None.
2034
+ meta : dict or None
2035
+ Meta and debug information about storage processes. Can be None.
2036
+ title : str or None
2037
+ The title of the cloud storage. Can be None.
2038
+ description : str or None
2039
+ The description of the cloud storage. Can be None.
2040
+ created_at : str
2041
+ The creation time of the storage.
2042
+ can_delete_objects : bool or None
2043
+ Deletion from storage enabled. Can be None.
2044
+ bucket : str or None
2045
+ The S3 bucket name. Can be None.
2046
+ prefix : str or None
2047
+ The S3 bucket prefix. Can be None.
2048
+ regex_filter : str or None
2049
+ The cloud storage regex for filtering objects. Can be None.
2050
+ use_blob_urls : bool
2051
+ Indicates if objects are interpreted as BLOBs and generate URLs.
2052
+ aws_access_key_id : str or None
2053
+ The AWS_ACCESS_KEY_ID. Can be None.
2054
+ aws_secret_access_key : str or None
2055
+ The AWS_SECRET_ACCESS_KEY. Can be None.
2056
+ aws_session_token : str or None
2057
+ The AWS_SESSION_TOKEN. Can be None.
2058
+ aws_sse_kms_key_id : str or None
2059
+ The AWS SSE KMS Key ID. Can be None.
2060
+ region_name : str or None
2061
+ The AWS Region. Can be None.
2062
+ s3_endpoint : str or None
2063
+ The S3 Endpoint. Can be None.
2064
+ project : int
2065
+ A unique integer value identifying this project.
2066
+ """
2067
+ response = self.make_request("GET", f"/api/storages/export?project={self.id}")
2068
+ return response.json()
2069
+
1712
2070
  def _assign_by_sampling(
1713
2071
  self,
1714
2072
  users: List[int],
@@ -1739,8 +2097,8 @@ class Project(Client):
1739
2097
  list[dict]
1740
2098
  List of dicts with counter of created assignments
1741
2099
  """
1742
- assert len(users) > 0, 'Users list is empty.'
1743
- assert len(users) >= overlap, 'Overlap is more than number of users.'
2100
+ assert len(users) > 0, "Users list is empty."
2101
+ assert len(users) >= overlap, "Overlap is more than number of users."
1744
2102
  # check if users are int and not User objects
1745
2103
  if isinstance(users[0], int):
1746
2104
  # get users from project
@@ -1750,7 +2108,7 @@ class Project(Client):
1750
2108
  final_results = []
1751
2109
  # Get tasks to assign
1752
2110
  tasks = self.get_tasks(view_id=view_id, only_ids=True)
1753
- assert len(tasks) > 0, 'Tasks list is empty.'
2111
+ assert len(tasks) > 0, "Tasks list is empty."
1754
2112
  # Choice fraction of tasks
1755
2113
  if fraction != 1.0:
1756
2114
  k = int(len(tasks) * fraction)
@@ -1839,7 +2197,7 @@ class Project(Client):
1839
2197
  overlap: int = 1,
1840
2198
  ):
1841
2199
  """
1842
- Behaves similarly like `assign_annotators()` but instead of specify tasks_ids explicitely,
2200
+ Behaves similarly like `assign_annotators()` but instead of specify tasks_ids explicitly,
1843
2201
  it gets users' IDs list and optional view ID and splits all tasks across annotators.
1844
2202
  Fraction expresses the size of dataset to be assigned.
1845
2203
  Parameters
@@ -1888,7 +2246,7 @@ class Project(Client):
1888
2246
  finished_at: str
1889
2247
  Finished time
1890
2248
  """
1891
- response = self.make_request('GET', f'/api/projects/{self.id}/exports')
2249
+ response = self.make_request("GET", f"/api/projects/{self.id}/exports")
1892
2250
  return response.json()
1893
2251
 
1894
2252
  def export_snapshot_create(
@@ -1965,12 +2323,107 @@ class Project(Client):
1965
2323
  },
1966
2324
  }
1967
2325
  response = self.make_request(
1968
- 'POST',
1969
- f'/api/projects/{self.id}/exports?interpolate_key_frames={interpolate_key_frames}',
2326
+ "POST",
2327
+ f"/api/projects/{self.id}/exports?interpolate_key_frames={interpolate_key_frames}",
1970
2328
  json=payload,
1971
2329
  )
1972
2330
  return response.json()
1973
2331
 
2332
+ def export(
2333
+ self,
2334
+ filters=None,
2335
+ title="SDK Export",
2336
+ export_type="JSON",
2337
+ output_dir=".",
2338
+ **kwargs,
2339
+ ):
2340
+ """
2341
+ Export tasks from the project with optional filters,
2342
+ and save the exported data to a specified directory.
2343
+
2344
+ This method:
2345
+ (1) creates a temporary view with the specified filters if they are not None,
2346
+ (2) creates a new export snapshot using the view ID,
2347
+ (3) checks the status of the snapshot creation while it's in progress,
2348
+ (4) and downloads the snapshot file in the specified export format.
2349
+ (5) After the export, it cleans up and remove the temporary view.
2350
+
2351
+ Parameters
2352
+ ----------
2353
+ filters : data_manager.Filters, dict, optional
2354
+ Filters to apply when exporting tasks.
2355
+ If provided, a temporary view is created with these filters.
2356
+ The format of the filters should match the Label Studio filter options.
2357
+ Default is None, which means all tasks are exported.
2358
+ Use label_studio_sdk.data_manager.Filters.create() to create filters,
2359
+ Example of the filters JSON format:
2360
+ ```json
2361
+ {
2362
+ "conjunction": "and",
2363
+ "items": [
2364
+ {
2365
+ "filter": "filter:tasks:id",
2366
+ "operator": "equal",
2367
+ "type": "Number",
2368
+ "value": 1
2369
+ }
2370
+ ]
2371
+ }
2372
+ ```
2373
+ titile : str, optional
2374
+ The title of the export snapshot. Default is 'SDK Export'.
2375
+ export_type : str, optional
2376
+ The format of the exported data. It should be one of the formats supported by Label Studio ('JSON', 'CSV', etc.). Default is 'JSON'.
2377
+ output_dir : str, optional
2378
+ The directory where the exported file will be saved. Default is the current directory.
2379
+ kwargs : kwargs, optional
2380
+ The same parameters as in the export_snapshot_create method.
2381
+
2382
+ Returns
2383
+ -------
2384
+ dict
2385
+ containing the status of the export, the filename of the exported file, and the export ID.
2386
+
2387
+ filename : str
2388
+ Path to the downloaded export file
2389
+ status : int
2390
+ 200 is ok
2391
+ export_id : int
2392
+ Export ID, you can retrieve more details about this export using this ID
2393
+ """
2394
+
2395
+ # Create a temporary view with the specified filters
2396
+ if filters:
2397
+ view = self.create_view(title="Temp SDK export", filters=filters)
2398
+ task_filter_options = {"view": view["id"]}
2399
+ else:
2400
+ task_filter_options = None
2401
+ view = None
2402
+
2403
+ # Create a new export snapshot using the view ID
2404
+ export_result = self.export_snapshot_create(
2405
+ title=title,
2406
+ task_filter_options=task_filter_options,
2407
+ **kwargs,
2408
+ )
2409
+
2410
+ # Check the status of the snapshot creation
2411
+ export_id = export_result["id"]
2412
+ while self.export_snapshot_status(export_id).is_in_progress():
2413
+ time.sleep(1.0) # Wait until the snapshot is ready
2414
+
2415
+ os.makedirs(output_dir, exist_ok=True)
2416
+
2417
+ # Download the snapshot file once it's ready
2418
+ status, filename = self.export_snapshot_download(
2419
+ export_id, export_type=export_type, path=output_dir
2420
+ )
2421
+
2422
+ # Clean up the view
2423
+ if view:
2424
+ self.delete_view(view["id"])
2425
+ return {"status": status, "filename": filename, "export_id": export_id}
2426
+
1974
2427
  def export_snapshot_status(self, export_id: int) -> ExportSnapshotStatus:
1975
2428
  """
1976
2429
  Get export snapshot status by Export ID
@@ -1997,12 +2450,12 @@ class Project(Client):
1997
2450
  Finished time
1998
2451
  """
1999
2452
  response = self.make_request(
2000
- 'GET', f'/api/projects/{self.id}/exports/{export_id}'
2453
+ "GET", f"/api/projects/{self.id}/exports/{export_id}"
2001
2454
  )
2002
2455
  return ExportSnapshotStatus(response.json())
2003
2456
 
2004
2457
  def export_snapshot_download(
2005
- self, export_id: int, export_type: str = 'JSON', path: str = "."
2458
+ self, export_id: int, export_type: str = "JSON", path: str = "."
2006
2459
  ) -> (int, str):
2007
2460
  """
2008
2461
  Download file with export snapshot in provided format
@@ -2022,8 +2475,8 @@ class Project(Client):
2022
2475
  Status code for operation and downloaded filename
2023
2476
  """
2024
2477
  response = self.make_request(
2025
- 'GET',
2026
- f'/api/projects/{self.id}/exports/{export_id}/download?exportType={export_type}',
2478
+ "GET",
2479
+ f"/api/projects/{self.id}/exports/{export_id}/download?exportType={export_type}",
2027
2480
  )
2028
2481
  filename = None
2029
2482
  if response.status_code == 200:
@@ -2032,8 +2485,8 @@ class Project(Client):
2032
2485
  filename = content_disposition.split("filename=")[-1].strip("\"'")
2033
2486
  filename = os.path.basename(filename)
2034
2487
  else:
2035
- raise LabelStudioException('No filename in response')
2036
- with open(os.path.join(path, filename), 'wb') as f:
2488
+ raise LabelStudioException("No filename in response")
2489
+ with open(os.path.join(path, filename), "wb") as f:
2037
2490
  for chk in response:
2038
2491
  f.write(chk)
2039
2492
  return response.status_code, filename
@@ -2051,7 +2504,7 @@ class Project(Client):
2051
2504
  Status code for operation
2052
2505
  """
2053
2506
  response = self.make_request(
2054
- 'DELETE', f'/api/projects/{self.id}/exports/{export_id}'
2507
+ "DELETE", f"/api/projects/{self.id}/exports/{export_id}"
2055
2508
  )
2056
2509
  return response.status_code
2057
2510
 
@@ -2075,10 +2528,10 @@ class Project(Client):
2075
2528
  filenames = []
2076
2529
  if tasks:
2077
2530
  for task in tasks:
2078
- for key in task['data']:
2531
+ for key in task["data"]:
2079
2532
  try:
2080
2533
  filename = get_local_path(
2081
- task['data'][key],
2534
+ task["data"][key],
2082
2535
  access_token=self.api_key,
2083
2536
  hostname=self.url,
2084
2537
  )
@@ -2095,7 +2548,7 @@ class Project(Client):
2095
2548
  task_id: int
2096
2549
  Task id.
2097
2550
  """
2098
- assert isinstance(task_id, int), 'task_id should be int'
2551
+ assert isinstance(task_id, int), "task_id should be int"
2099
2552
  return self.make_request("DELETE", f"/api/tasks/{task_id}")
2100
2553
 
2101
2554
  def delete_tasks(self, task_ids: list) -> Response:
@@ -2106,7 +2559,7 @@ class Project(Client):
2106
2559
  task_ids: list of int
2107
2560
  Task ids.
2108
2561
  """
2109
- assert isinstance(task_ids, list), 'task_ids should be list of int'
2562
+ assert isinstance(task_ids, list), "task_ids should be list of int"
2110
2563
  if not task_ids: # avoid deletion of all tasks when task_ids = []
2111
2564
  return Response()
2112
2565
  payload = {
@@ -2127,7 +2580,7 @@ class Project(Client):
2127
2580
  """
2128
2581
  assert (
2129
2582
  isinstance(excluded_ids, list) or excluded_ids is None
2130
- ), 'excluded_ids should be list of int or None'
2583
+ ), "excluded_ids should be list of int or None"
2131
2584
  if excluded_ids is None:
2132
2585
  excluded_ids = []
2133
2586
  payload = {