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.
- label_studio_sdk/__init__.py +4 -1
- label_studio_sdk/client.py +94 -78
- label_studio_sdk/data_manager.py +32 -23
- label_studio_sdk/exceptions.py +10 -0
- label_studio_sdk/label_interface/__init__.py +1 -0
- label_studio_sdk/label_interface/base.py +77 -0
- label_studio_sdk/label_interface/control_tags.py +756 -0
- label_studio_sdk/label_interface/interface.py +922 -0
- label_studio_sdk/label_interface/label_tags.py +72 -0
- label_studio_sdk/label_interface/object_tags.py +292 -0
- label_studio_sdk/label_interface/region.py +43 -0
- label_studio_sdk/objects.py +35 -0
- label_studio_sdk/project.py +711 -258
- label_studio_sdk/schema/label_config_schema.json +226 -0
- label_studio_sdk/users.py +15 -13
- label_studio_sdk/utils.py +31 -30
- label_studio_sdk/workspaces.py +13 -11
- {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/METADATA +3 -1
- label_studio_sdk-0.0.34.dist-info/RECORD +37 -0
- {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/WHEEL +1 -1
- {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/top_level.txt +0 -1
- tests/test_client.py +21 -10
- tests/test_export.py +105 -0
- tests/test_interface/__init__.py +1 -0
- tests/test_interface/configs.py +137 -0
- tests/test_interface/mockups.py +22 -0
- tests/test_interface/test_compat.py +64 -0
- tests/test_interface/test_control_tags.py +55 -0
- tests/test_interface/test_data_generation.py +45 -0
- tests/test_interface/test_lpi.py +15 -0
- tests/test_interface/test_main.py +196 -0
- tests/test_interface/test_object_tags.py +36 -0
- tests/test_interface/test_region.py +36 -0
- tests/test_interface/test_validate_summary.py +35 -0
- tests/test_interface/test_validation.py +59 -0
- docs/__init__.py +0 -3
- label_studio_sdk-0.0.32.dist-info/RECORD +0 -15
- {label_studio_sdk-0.0.32.dist-info → label_studio_sdk-0.0.34.dist-info}/LICENSE +0 -0
label_studio_sdk/project.py
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
""" .. include::../docs/project.md
|
|
2
2
|
"""
|
|
3
|
-
|
|
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 =
|
|
36
|
+
RANDOM = "Uniform sampling"
|
|
36
37
|
""" Uniform random sampling of tasks """
|
|
37
|
-
SEQUENCE =
|
|
38
|
+
SEQUENCE = "Sequential sampling"
|
|
38
39
|
""" Sequential sampling of tasks using task IDs """
|
|
39
|
-
UNCERTAINTY =
|
|
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 =
|
|
47
|
+
GOOGLE = "gcs"
|
|
47
48
|
""" Google Cloud Storage """
|
|
48
|
-
S3 =
|
|
49
|
+
S3 = "s3"
|
|
49
50
|
""" Amazon S3 Storage """
|
|
50
|
-
AZURE =
|
|
51
|
+
AZURE = "azure_blob"
|
|
51
52
|
""" Microsoft Azure Blob Storage """
|
|
52
|
-
LOCAL =
|
|
53
|
+
LOCAL = "localfiles"
|
|
53
54
|
""" Label Studio Local File Storage """
|
|
54
|
-
REDIS =
|
|
55
|
+
REDIS = "redis"
|
|
55
56
|
""" Redis Storage """
|
|
56
|
-
S3_SECURED =
|
|
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 =
|
|
66
|
+
CREATED = "created"
|
|
66
67
|
""" Export snapshot is created """
|
|
67
|
-
IN_PROGRESS =
|
|
68
|
+
IN_PROGRESS = "in_progress"
|
|
68
69
|
""" Export snapshot is in progress """
|
|
69
|
-
FAILED =
|
|
70
|
+
FAILED = "failed"
|
|
70
71
|
""" Export snapshot failed with errors """
|
|
71
|
-
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
|
-
|
|
81
|
+
"status" in self.response
|
|
81
82
|
), '"status" field not found in export snapshot status response'
|
|
82
|
-
return self.response[
|
|
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
|
-
|
|
88
|
+
"status" in self.response
|
|
88
89
|
), '"status" field not found in export_snapshot_status response'
|
|
89
|
-
return self.response[
|
|
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
|
-
|
|
95
|
+
"status" in self.response
|
|
95
96
|
), '"status" field not found in export_snapshot_status response'
|
|
96
|
-
return self.response[
|
|
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
|
-
|
|
102
|
+
"status" in self.response
|
|
102
103
|
), '"status" field not found in export_snapshot_status response'
|
|
103
|
-
return self.response[
|
|
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(
|
|
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[
|
|
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 = {
|
|
185
|
+
payload = {"user": user.id}
|
|
185
186
|
response = self.make_request(
|
|
186
|
-
|
|
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 = {
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
"users": users_ids,
|
|
212
|
+
"selectedItems": {"all": False, "included": c},
|
|
213
|
+
"type": "AN",
|
|
213
214
|
}
|
|
214
215
|
response = self.make_request(
|
|
215
|
-
|
|
216
|
+
"POST", f"/api/projects/{self.id}/tasks/assignees", json=payload
|
|
216
217
|
)
|
|
217
|
-
final_response[
|
|
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 = {
|
|
234
|
+
payload = {"selectedItems": {"all": False, "included": tasks_ids}}
|
|
234
235
|
response = self.make_request(
|
|
235
|
-
|
|
236
|
-
f
|
|
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 = {
|
|
255
|
+
payload = {"selectedItems": {"all": False, "included": tasks_ids}}
|
|
255
256
|
response = self.make_request(
|
|
256
|
-
|
|
257
|
-
f
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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[
|
|
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 = {
|
|
498
|
+
params = {"return_task_ids": "1"}
|
|
498
499
|
if preannotated_from_fields:
|
|
499
|
-
params[
|
|
500
|
+
params["preannotated_from_fields"] = ",".join(preannotated_from_fields)
|
|
500
501
|
if isinstance(tasks, (list, dict)):
|
|
501
502
|
response = self.make_request(
|
|
502
|
-
method=
|
|
503
|
-
url=f
|
|
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
|
|
512
|
-
with open(tasks, mode=
|
|
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=
|
|
515
|
-
url=f
|
|
516
|
-
files={
|
|
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
|
|
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=
|
|
536
|
+
method="GET",
|
|
536
537
|
url=f'/api/projects/{self.id}/imports/{response["import"]}',
|
|
537
538
|
).json()
|
|
538
539
|
|
|
539
|
-
if import_status[
|
|
540
|
-
return import_status[
|
|
540
|
+
if import_status["status"] == "completed":
|
|
541
|
+
return import_status["task_ids"]
|
|
541
542
|
|
|
542
|
-
if import_status[
|
|
543
|
-
raise LabelStudioException(import_status[
|
|
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(
|
|
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[
|
|
555
|
+
return response["task_ids"]
|
|
555
556
|
|
|
556
557
|
def export_tasks(
|
|
557
558
|
self,
|
|
558
|
-
export_type: str =
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
596
|
+
"exportType": export_type,
|
|
597
|
+
"download_all_tasks": download_all_tasks,
|
|
598
|
+
"download_resources": download_resources,
|
|
598
599
|
}
|
|
599
600
|
if ids:
|
|
600
|
-
params[
|
|
601
|
+
params["ids"] = ids
|
|
601
602
|
response = self.make_request(
|
|
602
|
-
method=
|
|
603
|
+
method="GET", url=f"/api/projects/{self.id}/export", params=params
|
|
603
604
|
)
|
|
604
605
|
if export_location is None:
|
|
605
|
-
if
|
|
606
|
+
if "JSON" not in export_type.upper():
|
|
606
607
|
raise ValueError(
|
|
607
|
-
f
|
|
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(
|
|
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(
|
|
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[
|
|
720
|
+
result += data["tasks"]
|
|
720
721
|
page += 1
|
|
721
722
|
except LabelStudioException as e:
|
|
722
|
-
logger.debug(f
|
|
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 =
|
|
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
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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[
|
|
820
|
+
params["include"] = "id"
|
|
818
821
|
|
|
819
822
|
response = self.make_request(
|
|
820
|
-
|
|
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 {
|
|
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
|
|
833
|
+
raise LabelStudioException(f"Error loading tasks: {e}")
|
|
831
834
|
|
|
832
835
|
data = response.json()
|
|
833
|
-
tasks = data[
|
|
836
|
+
tasks = data["tasks"]
|
|
834
837
|
if only_ids:
|
|
835
|
-
data[
|
|
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[
|
|
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[
|
|
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(
|
|
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=
|
|
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
|
-
|
|
894
|
-
|
|
896
|
+
"project": self.id,
|
|
897
|
+
"data": {"title": title, "ordering": ordering, "filters": filters},
|
|
895
898
|
}
|
|
896
|
-
response = self.make_request(
|
|
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
|
-
|
|
926
|
-
|
|
945
|
+
"conjunction": "and",
|
|
946
|
+
"items": [
|
|
927
947
|
{
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
967
|
-
|
|
986
|
+
"conjunction": "and",
|
|
987
|
+
"items": [
|
|
968
988
|
{
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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(
|
|
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(
|
|
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 = {
|
|
1125
|
+
data = {"task": task_id, "result": result, "score": score}
|
|
1106
1126
|
if model_version is not None:
|
|
1107
|
-
data[
|
|
1108
|
-
response = self.make_request(
|
|
1109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
params={
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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(
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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(
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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(
|
|
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
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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(
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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(
|
|
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
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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(
|
|
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
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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(
|
|
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
|
|
1843
|
+
if "LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT" not in os.environ:
|
|
1682
1844
|
raise ValueError(
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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[
|
|
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
|
|
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
|
|
1694
|
-
f
|
|
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
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
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
|
-
|
|
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,
|
|
1743
|
-
assert len(users) >= overlap,
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
1969
|
-
f
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2026
|
-
f
|
|
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(
|
|
2036
|
-
with open(os.path.join(path, filename),
|
|
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
|
-
|
|
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[
|
|
2531
|
+
for key in task["data"]:
|
|
2079
2532
|
try:
|
|
2080
2533
|
filename = get_local_path(
|
|
2081
|
-
task[
|
|
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),
|
|
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),
|
|
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
|
-
),
|
|
2583
|
+
), "excluded_ids should be list of int or None"
|
|
2131
2584
|
if excluded_ids is None:
|
|
2132
2585
|
excluded_ids = []
|
|
2133
2586
|
payload = {
|