label-studio-sdk 1.0.0__py3-none-any.whl → 1.0.2__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.
- label_studio_sdk/__init__.py +46 -4
- label_studio_sdk/_extensions/pager_ext.py +49 -0
- label_studio_sdk/_legacy/schema/label_config_schema.json +14 -14
- label_studio_sdk/actions/__init__.py +27 -0
- label_studio_sdk/actions/client.py +129 -8
- label_studio_sdk/actions/types/__init__.py +27 -0
- label_studio_sdk/actions/types/actions_create_request_filters.py +43 -0
- label_studio_sdk/actions/types/actions_create_request_filters_conjunction.py +5 -0
- label_studio_sdk/actions/types/actions_create_request_filters_items_item.py +50 -0
- label_studio_sdk/actions/types/actions_create_request_filters_items_item_filter.py +31 -0
- label_studio_sdk/actions/types/actions_create_request_filters_items_item_operator.py +23 -0
- label_studio_sdk/actions/types/actions_create_request_filters_items_item_value.py +5 -0
- label_studio_sdk/actions/types/actions_create_request_id.py +19 -0
- label_studio_sdk/actions/types/actions_create_request_ordering_item.py +31 -0
- label_studio_sdk/actions/types/actions_create_request_selected_items.py +10 -0
- label_studio_sdk/actions/types/actions_create_request_selected_items_excluded.py +39 -0
- label_studio_sdk/actions/types/actions_create_request_selected_items_included.py +39 -0
- label_studio_sdk/base_client.py +183 -0
- label_studio_sdk/client.py +17 -175
- label_studio_sdk/core/client_wrapper.py +1 -1
- label_studio_sdk/core/http_client.py +5 -1
- label_studio_sdk/errors/bad_request_error.py +3 -1
- label_studio_sdk/export_storage/azure/client.py +176 -10
- label_studio_sdk/export_storage/azure/types/azure_create_response.py +15 -0
- label_studio_sdk/export_storage/azure/types/azure_update_response.py +15 -0
- label_studio_sdk/export_storage/gcs/client.py +180 -14
- label_studio_sdk/export_storage/gcs/types/gcs_create_response.py +16 -1
- label_studio_sdk/export_storage/gcs/types/gcs_update_response.py +16 -1
- label_studio_sdk/export_storage/local/client.py +168 -22
- label_studio_sdk/export_storage/local/types/local_create_response.py +12 -2
- label_studio_sdk/export_storage/local/types/local_update_response.py +12 -2
- label_studio_sdk/export_storage/redis/client.py +234 -30
- label_studio_sdk/export_storage/redis/types/redis_create_response.py +20 -5
- label_studio_sdk/export_storage/redis/types/redis_update_response.py +20 -5
- label_studio_sdk/export_storage/s3/client.py +214 -26
- label_studio_sdk/export_storage/s3/types/s3create_response.py +15 -0
- label_studio_sdk/export_storage/s3/types/s3update_response.py +15 -0
- label_studio_sdk/import_storage/azure/client.py +266 -90
- label_studio_sdk/import_storage/azure/types/azure_create_response.py +28 -18
- label_studio_sdk/import_storage/azure/types/azure_update_response.py +28 -18
- label_studio_sdk/import_storage/gcs/client.py +270 -94
- label_studio_sdk/import_storage/gcs/types/gcs_create_response.py +28 -18
- label_studio_sdk/import_storage/gcs/types/gcs_update_response.py +28 -18
- label_studio_sdk/import_storage/local/client.py +168 -22
- label_studio_sdk/import_storage/local/types/local_create_response.py +12 -2
- label_studio_sdk/import_storage/local/types/local_update_response.py +12 -2
- label_studio_sdk/import_storage/redis/client.py +206 -50
- label_studio_sdk/import_storage/redis/types/redis_create_response.py +20 -10
- label_studio_sdk/import_storage/redis/types/redis_update_response.py +20 -10
- label_studio_sdk/import_storage/s3/client.py +336 -110
- label_studio_sdk/import_storage/s3/types/s3create_response.py +35 -25
- label_studio_sdk/import_storage/s3/types/s3update_response.py +35 -25
- label_studio_sdk/{_legacy/label_interface → label_interface}/base.py +10 -0
- label_studio_sdk/{_legacy/label_interface → label_interface}/control_tags.py +109 -71
- label_studio_sdk/{_legacy/label_interface → label_interface}/interface.py +97 -51
- label_studio_sdk/{_legacy/label_interface → label_interface}/object_tags.py +8 -13
- label_studio_sdk/label_interface/objects.py +60 -0
- label_studio_sdk/label_interface/region.py +75 -0
- label_studio_sdk/projects/client.py +6 -4
- label_studio_sdk/projects/client_ext.py +19 -0
- label_studio_sdk/tasks/client.py +35 -8
- label_studio_sdk/tasks/client_ext.py +18 -0
- label_studio_sdk/types/__init__.py +10 -0
- label_studio_sdk/types/annotation.py +5 -5
- label_studio_sdk/types/annotations_dm_field.py +120 -0
- label_studio_sdk/types/annotations_dm_field_last_action.py +19 -0
- label_studio_sdk/types/data_manager_task_serializer.py +123 -0
- label_studio_sdk/types/data_manager_task_serializer_drafts_item.py +31 -0
- label_studio_sdk/types/data_manager_task_serializer_predictions_item.py +37 -0
- label_studio_sdk/types/task.py +1 -1
- label_studio_sdk/views/__init__.py +12 -4
- label_studio_sdk/views/types/__init__.py +12 -4
- label_studio_sdk/views/types/views_create_request_data.py +2 -2
- label_studio_sdk/views/types/views_create_request_data_filters.py +5 -5
- label_studio_sdk/views/types/views_create_request_data_filters_conjunction.py +1 -1
- label_studio_sdk/views/types/views_create_request_data_filters_items_item.py +11 -8
- label_studio_sdk/views/types/views_create_request_data_filters_items_item_filter.py +31 -0
- label_studio_sdk/views/types/views_create_request_data_filters_items_item_operator.py +23 -0
- label_studio_sdk/views/types/views_create_request_data_filters_items_item_value.py +5 -0
- label_studio_sdk/views/types/views_create_request_data_ordering_item.py +27 -34
- label_studio_sdk/views/types/views_update_request_data.py +2 -2
- label_studio_sdk/views/types/views_update_request_data_filters.py +5 -5
- label_studio_sdk/views/types/views_update_request_data_filters_conjunction.py +1 -1
- label_studio_sdk/views/types/views_update_request_data_filters_items_item.py +11 -8
- label_studio_sdk/views/types/views_update_request_data_filters_items_item_filter.py +31 -0
- label_studio_sdk/views/types/views_update_request_data_filters_items_item_operator.py +23 -0
- label_studio_sdk/views/types/views_update_request_data_filters_items_item_value.py +5 -0
- label_studio_sdk/views/types/views_update_request_data_ordering_item.py +27 -34
- label_studio_sdk-1.0.2.dist-info/METADATA +195 -0
- {label_studio_sdk-1.0.0.dist-info → label_studio_sdk-1.0.2.dist-info}/RECORD +94 -69
- label_studio_sdk/_legacy/label_interface/region.py +0 -43
- label_studio_sdk/_legacy/objects.py +0 -35
- label_studio_sdk/views/types/views_create_request_data_ordering_item_direction.py +0 -5
- label_studio_sdk/views/types/views_update_request_data_ordering_item_direction.py +0 -5
- label_studio_sdk-1.0.0.dist-info/METADATA +0 -307
- /label_studio_sdk/{_legacy/label_interface → label_interface}/__init__.py +0 -0
- /label_studio_sdk/{_legacy/label_interface → label_interface}/data_examples.json +0 -0
- /label_studio_sdk/{_legacy/label_interface → label_interface}/label_tags.py +0 -0
- {label_studio_sdk-1.0.0.dist-info → label_studio_sdk-1.0.2.dist-info}/WHEEL +0 -0
|
@@ -12,7 +12,7 @@ from typing import Dict, Optional, List, Tuple, Any, Callable, Union
|
|
|
12
12
|
from pydantic import BaseModel
|
|
13
13
|
|
|
14
14
|
# from typing import Dict, Optional, List, Tuple, Any
|
|
15
|
-
from collections import defaultdict
|
|
15
|
+
from collections import defaultdict, OrderedDict
|
|
16
16
|
from lxml import etree
|
|
17
17
|
import xmljson
|
|
18
18
|
|
|
@@ -22,18 +22,18 @@ from label_studio_sdk._legacy.exceptions import (
|
|
|
22
22
|
LabelStudioValidationErrorSentryIgnored,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
from
|
|
25
|
+
from .control_tags import (
|
|
26
26
|
ControlTag,
|
|
27
27
|
ChoicesTag,
|
|
28
28
|
LabelsTag,
|
|
29
29
|
)
|
|
30
|
-
from
|
|
31
|
-
from
|
|
32
|
-
from
|
|
30
|
+
from .object_tags import ObjectTag
|
|
31
|
+
from .label_tags import LabelTag
|
|
32
|
+
from .objects import AnnotationValue, TaskValue, PredictionValue
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
dir_path = os.path.dirname(os.path.realpath(__file__))
|
|
36
|
-
file_path = os.path.join(dir_path, "..", "schema", "label_config_schema.json")
|
|
36
|
+
file_path = os.path.join(dir_path, "..", "_legacy", "schema", "label_config_schema.json")
|
|
37
37
|
|
|
38
38
|
with open(file_path) as f:
|
|
39
39
|
_LABEL_CONFIG_SCHEMA_DATA = json.load(f)
|
|
@@ -189,23 +189,34 @@ class LabelInterface:
|
|
|
189
189
|
```
|
|
190
190
|
"""
|
|
191
191
|
|
|
192
|
-
def __init__(self, config: str, *args, **kwargs):
|
|
192
|
+
def __init__(self, config: str, tags_mapping=None, *args, **kwargs):
|
|
193
193
|
"""
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
Initialize a LabelInterface instance using a config string.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
config (str): Configuration string.
|
|
198
|
+
tags_mapping: Provide your own implementation of any tag, this is helpful in cases where you want to override one of the control tags, and have your custom .label() method implemented.
|
|
199
|
+
|
|
200
|
+
The configuration string should be formatted as follows:
|
|
201
|
+
|
|
196
202
|
```
|
|
197
|
-
label_config = LabelInterface('''
|
|
198
203
|
<View>
|
|
199
|
-
|
|
204
|
+
<Choices name="sentiment" toName="txt">
|
|
200
205
|
<Choice value="Positive" />
|
|
201
206
|
<Choice value="Negative" />
|
|
202
207
|
<Choice value="Neutral" />
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
</Choices>
|
|
209
|
+
<Text name="txt" value="$text" />
|
|
210
|
+
</View>
|
|
211
|
+
```
|
|
212
|
+
This method will extract the predefined task from the configuration and
|
|
213
|
+
parse the controls, objects, and labels used in it.
|
|
206
214
|
"""
|
|
215
|
+
self.task_loaded = False
|
|
216
|
+
|
|
207
217
|
self._config = config
|
|
208
|
-
|
|
218
|
+
self._tags_mapping = tags_mapping
|
|
219
|
+
|
|
209
220
|
# extract predefined task from the config
|
|
210
221
|
_task_data, _ann, _pred = LabelInterface.get_task_from_labeling_config(config)
|
|
211
222
|
self._sample_config_task = _task_data
|
|
@@ -225,6 +236,8 @@ class LabelInterface:
|
|
|
225
236
|
self._labels = labels
|
|
226
237
|
self._tree = tree
|
|
227
238
|
|
|
239
|
+
|
|
240
|
+
|
|
228
241
|
##### NEW API
|
|
229
242
|
|
|
230
243
|
@property
|
|
@@ -391,7 +404,38 @@ class LabelInterface:
|
|
|
391
404
|
lst = list(filter(match_fn, lst))
|
|
392
405
|
|
|
393
406
|
return lst
|
|
407
|
+
|
|
408
|
+
def load_task(self, task):
|
|
409
|
+
"""Loads a task and substitutes the value in each object tag
|
|
410
|
+
with actual data from the task, returning a copy of the
|
|
411
|
+
LabelConfig object.
|
|
412
|
+
|
|
413
|
+
If the `value` field in an object tag is designed to take
|
|
414
|
+
variable input (i.e., `value_is_variable` is True), the
|
|
415
|
+
function replaces this value with the corresponding value from
|
|
416
|
+
the task dictionary.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
task (dict): Dictionary representing the task, where
|
|
420
|
+
each key-value pair denotes an attribute-value of the
|
|
421
|
+
task.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
LabelInterface: A deep copy of the current LabelIntreface
|
|
425
|
+
instance with the object tags' value fields populated with
|
|
426
|
+
data from the task.
|
|
427
|
+
|
|
428
|
+
"""
|
|
429
|
+
tree = copy.deepcopy(self)
|
|
430
|
+
tree.task_loaded = True
|
|
431
|
+
|
|
432
|
+
for obj in tree.objects:
|
|
433
|
+
print(obj.value_is_variable, obj.value_name)
|
|
434
|
+
if obj.value_is_variable and obj.value_name in task:
|
|
435
|
+
obj.value = task.get(obj.value_name)
|
|
394
436
|
|
|
437
|
+
return tree
|
|
438
|
+
|
|
395
439
|
def parse(self, config_string: str) -> Tuple[Dict, Dict, Dict, etree._Element]:
|
|
396
440
|
"""Parses the received configuration string into dictionaries
|
|
397
441
|
of ControlTags, ObjectTags, and Labels, along with an XML tree
|
|
@@ -422,10 +466,10 @@ class LabelInterface:
|
|
|
422
466
|
variables.append(tag.attrib["indexFlag"])
|
|
423
467
|
|
|
424
468
|
if ControlTag.validate_node(tag):
|
|
425
|
-
controls[tag.attrib["name"]] = ControlTag.parse_node(tag)
|
|
469
|
+
controls[tag.attrib["name"]] = ControlTag.parse_node(tag, tags_mapping=self._tags_mapping)
|
|
426
470
|
|
|
427
471
|
elif ObjectTag.validate_node(tag):
|
|
428
|
-
objects[tag.attrib["name"]] = ObjectTag.parse_node(tag)
|
|
472
|
+
objects[tag.attrib["name"]] = ObjectTag.parse_node(tag, tags_mapping=self._tags_mapping)
|
|
429
473
|
|
|
430
474
|
elif LabelTag.validate_node(tag):
|
|
431
475
|
lb = LabelTag.parse_node(tag, controls)
|
|
@@ -488,34 +532,6 @@ class LabelInterface:
|
|
|
488
532
|
"Label config contains non-unique names"
|
|
489
533
|
)
|
|
490
534
|
|
|
491
|
-
def load_task(self, task):
|
|
492
|
-
"""Loads a task and substitutes the value in each object tag
|
|
493
|
-
with actual data from the task, returning a copy of the
|
|
494
|
-
LabelConfig object.
|
|
495
|
-
|
|
496
|
-
If the `value` field in an object tag is designed to take
|
|
497
|
-
variable input (i.e., `value_is_variable` is True), the
|
|
498
|
-
function replaces this value with the corresponding value from
|
|
499
|
-
the task dictionary.
|
|
500
|
-
|
|
501
|
-
Args:
|
|
502
|
-
task (dict): Dictionary representing the task, where
|
|
503
|
-
each key-value pair denotes an attribute-value of the
|
|
504
|
-
task.
|
|
505
|
-
|
|
506
|
-
Returns:
|
|
507
|
-
LabelInterface: A deep copy of the current LabelIntreface
|
|
508
|
-
instance with the object tags' value fields populated with
|
|
509
|
-
data from the task.
|
|
510
|
-
|
|
511
|
-
"""
|
|
512
|
-
tree = copy.deepcopy(self)
|
|
513
|
-
for obj in tree.objects:
|
|
514
|
-
if obj.value_is_variable and obj.value_name in task:
|
|
515
|
-
obj.value = task.get(obj.value_name)
|
|
516
|
-
|
|
517
|
-
return tree
|
|
518
|
-
|
|
519
535
|
@property
|
|
520
536
|
def is_valid(self):
|
|
521
537
|
""" """
|
|
@@ -568,6 +584,23 @@ class LabelInterface:
|
|
|
568
584
|
|
|
569
585
|
return True
|
|
570
586
|
|
|
587
|
+
def _validate_object(self, obj):
|
|
588
|
+
""" """
|
|
589
|
+
regions = []
|
|
590
|
+
for r in obj.get(RESULT_KEY):
|
|
591
|
+
if r.get('type') != "relation":
|
|
592
|
+
if not self.validate_region(r):
|
|
593
|
+
return False
|
|
594
|
+
|
|
595
|
+
regions.append(r)
|
|
596
|
+
|
|
597
|
+
for r in obj.get(RESULT_KEY):
|
|
598
|
+
if r.get('type') == "relation" and \
|
|
599
|
+
not self.validate_relation(r, regions):
|
|
600
|
+
return False
|
|
601
|
+
|
|
602
|
+
return True
|
|
603
|
+
|
|
571
604
|
def validate_annotation(self, annotation):
|
|
572
605
|
"""Validates the given annotation against the current configuration.
|
|
573
606
|
|
|
@@ -586,11 +619,11 @@ class LabelInterface:
|
|
|
586
619
|
validation, False otherwise.
|
|
587
620
|
|
|
588
621
|
"""
|
|
589
|
-
return
|
|
622
|
+
return self._validate_object(annotation)
|
|
590
623
|
|
|
591
624
|
def validate_prediction(self, prediction):
|
|
592
625
|
"""Same as validate_annotation right now"""
|
|
593
|
-
return
|
|
626
|
+
return self._validate_object(prediction)
|
|
594
627
|
|
|
595
628
|
def validate_region(self, region) -> bool:
|
|
596
629
|
"""Validates a region from the annotation against the current
|
|
@@ -620,22 +653,35 @@ class LabelInterface:
|
|
|
620
653
|
if not control or not obj:
|
|
621
654
|
return False
|
|
622
655
|
|
|
623
|
-
# type of the region should match the tag name
|
|
656
|
+
# type of the region should match the tag name
|
|
624
657
|
if control.tag.lower() != region["type"]:
|
|
625
658
|
return False
|
|
626
|
-
|
|
659
|
+
|
|
627
660
|
# make sure that in config it connects to the same tag as
|
|
628
661
|
# immplied by the region data
|
|
629
662
|
if region["to_name"] not in control.to_name:
|
|
630
663
|
return False
|
|
631
|
-
|
|
664
|
+
|
|
632
665
|
# validate the actual value, for example that <Labels /> tag
|
|
633
666
|
# is producing start, end, and labels
|
|
634
667
|
if not control.validate_value(region["value"]):
|
|
635
668
|
return False
|
|
636
|
-
|
|
669
|
+
|
|
637
670
|
return True
|
|
638
671
|
|
|
672
|
+
def validate_relation(self, relation, regions, _mapping=None) -> bool:
|
|
673
|
+
"""Validates that the relation is correct and all the associated objects are provided"""
|
|
674
|
+
if _mapping is None:
|
|
675
|
+
_mapping = { r['id']: r for r in regions }
|
|
676
|
+
|
|
677
|
+
if relation.get("type") != "relation" or \
|
|
678
|
+
relation.get("direction") not in ("left", "right", "bi") or \
|
|
679
|
+
relation.get("from_id") not in _mapping or \
|
|
680
|
+
relation.get("to_id") not in _mapping:
|
|
681
|
+
return False
|
|
682
|
+
|
|
683
|
+
return True
|
|
684
|
+
|
|
639
685
|
### Generation
|
|
640
686
|
|
|
641
687
|
def _sample_task(self, secure_mode=False):
|
|
@@ -8,7 +8,7 @@ import xml.etree.ElementTree
|
|
|
8
8
|
from urllib.parse import urlencode
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
-
from .base import LabelStudioTag
|
|
11
|
+
from .base import LabelStudioTag, get_tag_class
|
|
12
12
|
|
|
13
13
|
_TAG_TO_CLASS = {
|
|
14
14
|
"audio": "AudioTag",
|
|
@@ -77,12 +77,6 @@ def data_examples(
|
|
|
77
77
|
return _DATA_EXAMPLES[mode]
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def get_tag_class(name):
|
|
81
|
-
""" """
|
|
82
|
-
class_name = _TAG_TO_CLASS.get(name.lower())
|
|
83
|
-
return globals().get(class_name, None)
|
|
84
|
-
|
|
85
|
-
|
|
86
80
|
class ObjectTag(LabelStudioTag):
|
|
87
81
|
"""
|
|
88
82
|
Class that represents a ObjectTag in Label Studio
|
|
@@ -103,7 +97,7 @@ class ObjectTag(LabelStudioTag):
|
|
|
103
97
|
# self._value_type = value_type
|
|
104
98
|
|
|
105
99
|
@classmethod
|
|
106
|
-
def parse_node(cls, tag: xml.etree.ElementTree.Element) -> "ObjectTag":
|
|
100
|
+
def parse_node(cls, tag: xml.etree.ElementTree.Element, tags_mapping=None) -> "ObjectTag":
|
|
107
101
|
"""
|
|
108
102
|
This class method parses a node and returns a ObjectTag object if the node has a name and a value.
|
|
109
103
|
|
|
@@ -117,8 +111,10 @@ class ObjectTag(LabelStudioTag):
|
|
|
117
111
|
ObjectTag
|
|
118
112
|
A new ObjectTag object with the tag name, attributes, name, and value.
|
|
119
113
|
"""
|
|
120
|
-
tag_class = get_tag_class(tag.tag) or cls
|
|
121
|
-
|
|
114
|
+
tag_class = get_tag_class(tag.tag, _TAG_TO_CLASS, re_mapping=tags_mapping) or cls
|
|
115
|
+
if isinstance(tag_class, str):
|
|
116
|
+
tag_class = globals().get(tag_class, None)
|
|
117
|
+
|
|
122
118
|
return tag_class(
|
|
123
119
|
tag=tag.tag,
|
|
124
120
|
attr=dict(tag.attrib),
|
|
@@ -146,10 +142,9 @@ class ObjectTag(LabelStudioTag):
|
|
|
146
142
|
@property
|
|
147
143
|
def value_is_variable(self) -> bool:
|
|
148
144
|
"""Check if value has variable"""
|
|
149
|
-
pattern = re.compile(r"^\$[
|
|
145
|
+
pattern = re.compile(r"^\$[^, ]+$")
|
|
150
146
|
return bool(pattern.fullmatch(self.value))
|
|
151
|
-
|
|
152
|
-
# TODO this should not be here as soon as we cover all the tags
|
|
147
|
+
|
|
153
148
|
|
|
154
149
|
# and have generate_example in each
|
|
155
150
|
def generate_example_value(self, mode="upload", secure_mode=False):
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Type, Dict, Optional, List, Tuple, Any, Union
|
|
2
|
+
from pydantic import BaseModel, Field, confloat, field_serializer
|
|
3
|
+
|
|
4
|
+
from .region import Region
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def serialize_regions(result):
|
|
8
|
+
res = []
|
|
9
|
+
relations = []
|
|
10
|
+
for r in result:
|
|
11
|
+
if isinstance(r, Region):
|
|
12
|
+
res.append(r._dict())
|
|
13
|
+
if r.has_relations:
|
|
14
|
+
relations.append(r._dict_relations())
|
|
15
|
+
else:
|
|
16
|
+
res.append(r)
|
|
17
|
+
|
|
18
|
+
return res + relations
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PredictionValue(BaseModel):
|
|
22
|
+
""" """
|
|
23
|
+
|
|
24
|
+
model_version: Optional[Any] = None
|
|
25
|
+
score: Optional[float] = 0.00
|
|
26
|
+
result: Optional[List[Union[Dict[str, Any], Region]]]
|
|
27
|
+
|
|
28
|
+
class Config:
|
|
29
|
+
allow_population_by_field_name = True
|
|
30
|
+
|
|
31
|
+
@field_serializer('result')
|
|
32
|
+
def serialize_result(self, result):
|
|
33
|
+
return serialize_regions(result)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AnnotationValue(BaseModel):
|
|
37
|
+
""" """
|
|
38
|
+
|
|
39
|
+
was_cancelled: Optional[bool] = False
|
|
40
|
+
ground_truth: Optional[bool] = False
|
|
41
|
+
lead_time: Optional[float] = 0.0
|
|
42
|
+
result_count: Optional[int] = 0
|
|
43
|
+
completed_by: int
|
|
44
|
+
|
|
45
|
+
result: Optional[List[Union[Dict[str, Any], Region]]]
|
|
46
|
+
|
|
47
|
+
class Config:
|
|
48
|
+
allow_population_by_field_name = True
|
|
49
|
+
|
|
50
|
+
@field_serializer('result')
|
|
51
|
+
def serialize_result(self, result):
|
|
52
|
+
return serialize_regions(result)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TaskValue(BaseModel):
|
|
56
|
+
""" """
|
|
57
|
+
|
|
58
|
+
data: Optional[dict]
|
|
59
|
+
annotations: Optional[List[AnnotationValue]]
|
|
60
|
+
predictions: Optional[List[PredictionValue]]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Dict, Optional
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Region(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
Class for Region Tag
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
-----------
|
|
17
|
+
id: str
|
|
18
|
+
The unique identifier of the region
|
|
19
|
+
x: int
|
|
20
|
+
The x coordinate of the region
|
|
21
|
+
y: int
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
26
|
+
from_tag: Any
|
|
27
|
+
to_tag: Any
|
|
28
|
+
value: Any
|
|
29
|
+
relations: Optional[List[Dict]] = []
|
|
30
|
+
|
|
31
|
+
def _dict(self):
|
|
32
|
+
""" """
|
|
33
|
+
return {
|
|
34
|
+
"id": self.id,
|
|
35
|
+
"from_name": self.from_tag.name,
|
|
36
|
+
"to_name": self.to_tag.name,
|
|
37
|
+
"type": self.from_tag.tag.lower(),
|
|
38
|
+
# TODO This needs to be improved
|
|
39
|
+
"value": self.value.dict(),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def _dict_relations(self):
|
|
43
|
+
""" """
|
|
44
|
+
# this code does not include "labels" key if no labels were passed
|
|
45
|
+
return [
|
|
46
|
+
{**{
|
|
47
|
+
"from_id": self.id,
|
|
48
|
+
"to_id": rel.get("region", {}).id,
|
|
49
|
+
"type": "relation",
|
|
50
|
+
"direction": rel.get("direction", "right")},
|
|
51
|
+
**({"labels": rel["labels"]} if rel.get("labels") else {})
|
|
52
|
+
}
|
|
53
|
+
for rel in self.relations
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def to_json(self):
|
|
57
|
+
""" """
|
|
58
|
+
return json.dumps(self._dict())
|
|
59
|
+
|
|
60
|
+
def to_json_relations(self):
|
|
61
|
+
""" """
|
|
62
|
+
return json.dumps(self._dict_relations())
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def has_relations(self):
|
|
66
|
+
return len(self.relations) > 0
|
|
67
|
+
|
|
68
|
+
def add_relation(self, region=None, direction="right", label=None):
|
|
69
|
+
""" """
|
|
70
|
+
self.relations.append({ "region": region, "direction": direction, "labels": label })
|
|
71
|
+
|
|
72
|
+
def set_relations(self, rels):
|
|
73
|
+
""" """
|
|
74
|
+
self.relations = rels
|
|
75
|
+
|
|
@@ -82,6 +82,7 @@ class ProjectsClient:
|
|
|
82
82
|
)
|
|
83
83
|
client.projects.list()
|
|
84
84
|
"""
|
|
85
|
+
page = page or 1
|
|
85
86
|
_response = self._client_wrapper.httpx_client.request(
|
|
86
87
|
"api/projects/",
|
|
87
88
|
method="GET",
|
|
@@ -95,7 +96,7 @@ class ProjectsClient:
|
|
|
95
96
|
ordering=ordering,
|
|
96
97
|
ids=ids,
|
|
97
98
|
title=title,
|
|
98
|
-
page=page + 1
|
|
99
|
+
page=page + 1,
|
|
99
100
|
page_size=page_size,
|
|
100
101
|
request_options=request_options,
|
|
101
102
|
)
|
|
@@ -475,7 +476,7 @@ class ProjectsClient:
|
|
|
475
476
|
if 200 <= _response.status_code < 300:
|
|
476
477
|
return pydantic_v1.parse_obj_as(ProjectsImportTasksResponse, _response.json()) # type: ignore
|
|
477
478
|
if _response.status_code == 400:
|
|
478
|
-
raise BadRequestError(pydantic_v1.parse_obj_as(
|
|
479
|
+
raise BadRequestError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore
|
|
479
480
|
try:
|
|
480
481
|
_response_json = _response.json()
|
|
481
482
|
except JSONDecodeError:
|
|
@@ -597,6 +598,7 @@ class AsyncProjectsClient:
|
|
|
597
598
|
)
|
|
598
599
|
await client.projects.list()
|
|
599
600
|
"""
|
|
601
|
+
page = page or 1
|
|
600
602
|
_response = await self._client_wrapper.httpx_client.request(
|
|
601
603
|
"api/projects/",
|
|
602
604
|
method="GET",
|
|
@@ -610,7 +612,7 @@ class AsyncProjectsClient:
|
|
|
610
612
|
ordering=ordering,
|
|
611
613
|
ids=ids,
|
|
612
614
|
title=title,
|
|
613
|
-
page=page + 1
|
|
615
|
+
page=page + 1,
|
|
614
616
|
page_size=page_size,
|
|
615
617
|
request_options=request_options,
|
|
616
618
|
)
|
|
@@ -992,7 +994,7 @@ class AsyncProjectsClient:
|
|
|
992
994
|
if 200 <= _response.status_code < 300:
|
|
993
995
|
return pydantic_v1.parse_obj_as(ProjectsImportTasksResponse, _response.json()) # type: ignore
|
|
994
996
|
if _response.status_code == 400:
|
|
995
|
-
raise BadRequestError(pydantic_v1.parse_obj_as(
|
|
997
|
+
raise BadRequestError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore
|
|
996
998
|
try:
|
|
997
999
|
_response_json = _response.json()
|
|
998
1000
|
except JSONDecodeError:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .client import ProjectsClient, AsyncProjectsClient
|
|
2
|
+
|
|
3
|
+
from label_studio_sdk._extensions.pager_ext import SyncPagerExt, AsyncPagerExt, T
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProjectsClientExt(ProjectsClient):
|
|
7
|
+
|
|
8
|
+
def list(self, **kwargs) -> SyncPagerExt[T]:
|
|
9
|
+
return SyncPagerExt.from_sync_pager(super().list(**kwargs))
|
|
10
|
+
|
|
11
|
+
list.__doc__ = ProjectsClient.list.__doc__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncProjectsClientExt(AsyncProjectsClient):
|
|
15
|
+
|
|
16
|
+
async def list(self, **kwargs):
|
|
17
|
+
return await AsyncPagerExt.from_async_pager(await super().list(**kwargs))
|
|
18
|
+
|
|
19
|
+
list.__doc__ = AsyncProjectsClient.list.__doc__
|
label_studio_sdk/tasks/client.py
CHANGED
|
@@ -10,6 +10,7 @@ from ..core.pagination import AsyncPager, SyncPager
|
|
|
10
10
|
from ..core.pydantic_utilities import pydantic_v1
|
|
11
11
|
from ..core.request_options import RequestOptions
|
|
12
12
|
from ..types.base_task import BaseTask
|
|
13
|
+
from ..types.data_manager_task_serializer import DataManagerTaskSerializer
|
|
13
14
|
from ..types.project_import import ProjectImport
|
|
14
15
|
from ..types.task import Task
|
|
15
16
|
from .types.tasks_list_request_fields import TasksListRequestFields
|
|
@@ -126,6 +127,7 @@ class TasksClient:
|
|
|
126
127
|
fields: typing.Optional[TasksListRequestFields] = None,
|
|
127
128
|
review: typing.Optional[bool] = None,
|
|
128
129
|
include: typing.Optional[str] = None,
|
|
130
|
+
query: typing.Optional[str] = None,
|
|
129
131
|
request_options: typing.Optional[RequestOptions] = None,
|
|
130
132
|
) -> SyncPager[Task]:
|
|
131
133
|
"""
|
|
@@ -161,6 +163,14 @@ class TasksClient:
|
|
|
161
163
|
include : typing.Optional[str]
|
|
162
164
|
Specify which fields to include in the response
|
|
163
165
|
|
|
166
|
+
query : typing.Optional[str]
|
|
167
|
+
Additional query to filter tasks. It must be JSON encoded string of dict containing one of the following parameters: `{"filters": ..., "selectedItems": ..., "ordering": ...}`. Check [Data Manager > Create View > see `data` field](#tag/Data-Manager/operation/api_dm_views_create) for more details about filters, selectedItems and ordering.
|
|
168
|
+
|
|
169
|
+
- **filters**: dict with `"conjunction"` string (`"or"` or `"and"`) and list of filters in `"items"` array. Each filter is a dictionary with keys: `"filter"`, `"operator"`, `"type"`, `"value"`. [Read more about available filters](https://labelstud.io/sdk/data_manager.html)<br/> Example: `{"conjunction": "or", "items": [{"filter": "filter:tasks:completed_at", "operator": "greater", "type": "Datetime", "value": "2021-01-01T00:00:00.000Z"}]}`
|
|
170
|
+
- **selectedItems**: dictionary with keys: `"all"`, `"included"`, `"excluded"`. If "all" is `false`, `"included"` must be used. If "all" is `true`, `"excluded"` must be used.<br/> Examples: `{"all": false, "included": [1, 2, 3]}` or `{"all": true, "excluded": [4, 5]}`
|
|
171
|
+
- **ordering**: list of fields to order by. Currently, ordering is supported by only one parameter. <br/>
|
|
172
|
+
Example: `["completed_at"]`
|
|
173
|
+
|
|
164
174
|
request_options : typing.Optional[RequestOptions]
|
|
165
175
|
Request-specific configuration.
|
|
166
176
|
|
|
@@ -178,6 +188,7 @@ class TasksClient:
|
|
|
178
188
|
)
|
|
179
189
|
client.tasks.list()
|
|
180
190
|
"""
|
|
191
|
+
page = page or 1
|
|
181
192
|
_response = self._client_wrapper.httpx_client.request(
|
|
182
193
|
"api/tasks/",
|
|
183
194
|
method="GET",
|
|
@@ -190,6 +201,7 @@ class TasksClient:
|
|
|
190
201
|
"fields": fields,
|
|
191
202
|
"review": review,
|
|
192
203
|
"include": include,
|
|
204
|
+
"query": query,
|
|
193
205
|
},
|
|
194
206
|
request_options=request_options,
|
|
195
207
|
)
|
|
@@ -197,7 +209,7 @@ class TasksClient:
|
|
|
197
209
|
_parsed_response = pydantic_v1.parse_obj_as(TasksListResponse, _response.json()) # type: ignore
|
|
198
210
|
_has_next = True
|
|
199
211
|
_get_next = lambda: self.list(
|
|
200
|
-
page=page + 1
|
|
212
|
+
page=page + 1,
|
|
201
213
|
page_size=page_size,
|
|
202
214
|
view=view,
|
|
203
215
|
project=project,
|
|
@@ -205,6 +217,7 @@ class TasksClient:
|
|
|
205
217
|
fields=fields,
|
|
206
218
|
review=review,
|
|
207
219
|
include=include,
|
|
220
|
+
query=query,
|
|
208
221
|
request_options=request_options,
|
|
209
222
|
)
|
|
210
223
|
_items = _parsed_response.tasks
|
|
@@ -272,7 +285,7 @@ class TasksClient:
|
|
|
272
285
|
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
273
286
|
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
274
287
|
|
|
275
|
-
def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) ->
|
|
288
|
+
def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> DataManagerTaskSerializer:
|
|
276
289
|
"""
|
|
277
290
|
Get task data, metadata, annotations and other attributes for a specific labeling task by task ID.
|
|
278
291
|
The task ID is available from the Label Studio URL when viewing the task, or you can retrieve it programmatically with [Get task list](list).
|
|
@@ -287,7 +300,7 @@ class TasksClient:
|
|
|
287
300
|
|
|
288
301
|
Returns
|
|
289
302
|
-------
|
|
290
|
-
|
|
303
|
+
DataManagerTaskSerializer
|
|
291
304
|
Task
|
|
292
305
|
|
|
293
306
|
Examples
|
|
@@ -305,7 +318,7 @@ class TasksClient:
|
|
|
305
318
|
f"api/tasks/{jsonable_encoder(id)}/", method="GET", request_options=request_options
|
|
306
319
|
)
|
|
307
320
|
if 200 <= _response.status_code < 300:
|
|
308
|
-
return pydantic_v1.parse_obj_as(
|
|
321
|
+
return pydantic_v1.parse_obj_as(DataManagerTaskSerializer, _response.json()) # type: ignore
|
|
309
322
|
try:
|
|
310
323
|
_response_json = _response.json()
|
|
311
324
|
except JSONDecodeError:
|
|
@@ -522,6 +535,7 @@ class AsyncTasksClient:
|
|
|
522
535
|
fields: typing.Optional[TasksListRequestFields] = None,
|
|
523
536
|
review: typing.Optional[bool] = None,
|
|
524
537
|
include: typing.Optional[str] = None,
|
|
538
|
+
query: typing.Optional[str] = None,
|
|
525
539
|
request_options: typing.Optional[RequestOptions] = None,
|
|
526
540
|
) -> AsyncPager[Task]:
|
|
527
541
|
"""
|
|
@@ -557,6 +571,14 @@ class AsyncTasksClient:
|
|
|
557
571
|
include : typing.Optional[str]
|
|
558
572
|
Specify which fields to include in the response
|
|
559
573
|
|
|
574
|
+
query : typing.Optional[str]
|
|
575
|
+
Additional query to filter tasks. It must be JSON encoded string of dict containing one of the following parameters: `{"filters": ..., "selectedItems": ..., "ordering": ...}`. Check [Data Manager > Create View > see `data` field](#tag/Data-Manager/operation/api_dm_views_create) for more details about filters, selectedItems and ordering.
|
|
576
|
+
|
|
577
|
+
- **filters**: dict with `"conjunction"` string (`"or"` or `"and"`) and list of filters in `"items"` array. Each filter is a dictionary with keys: `"filter"`, `"operator"`, `"type"`, `"value"`. [Read more about available filters](https://labelstud.io/sdk/data_manager.html)<br/> Example: `{"conjunction": "or", "items": [{"filter": "filter:tasks:completed_at", "operator": "greater", "type": "Datetime", "value": "2021-01-01T00:00:00.000Z"}]}`
|
|
578
|
+
- **selectedItems**: dictionary with keys: `"all"`, `"included"`, `"excluded"`. If "all" is `false`, `"included"` must be used. If "all" is `true`, `"excluded"` must be used.<br/> Examples: `{"all": false, "included": [1, 2, 3]}` or `{"all": true, "excluded": [4, 5]}`
|
|
579
|
+
- **ordering**: list of fields to order by. Currently, ordering is supported by only one parameter. <br/>
|
|
580
|
+
Example: `["completed_at"]`
|
|
581
|
+
|
|
560
582
|
request_options : typing.Optional[RequestOptions]
|
|
561
583
|
Request-specific configuration.
|
|
562
584
|
|
|
@@ -574,6 +596,7 @@ class AsyncTasksClient:
|
|
|
574
596
|
)
|
|
575
597
|
await client.tasks.list()
|
|
576
598
|
"""
|
|
599
|
+
page = page or 1
|
|
577
600
|
_response = await self._client_wrapper.httpx_client.request(
|
|
578
601
|
"api/tasks/",
|
|
579
602
|
method="GET",
|
|
@@ -586,6 +609,7 @@ class AsyncTasksClient:
|
|
|
586
609
|
"fields": fields,
|
|
587
610
|
"review": review,
|
|
588
611
|
"include": include,
|
|
612
|
+
"query": query,
|
|
589
613
|
},
|
|
590
614
|
request_options=request_options,
|
|
591
615
|
)
|
|
@@ -593,7 +617,7 @@ class AsyncTasksClient:
|
|
|
593
617
|
_parsed_response = pydantic_v1.parse_obj_as(TasksListResponse, _response.json()) # type: ignore
|
|
594
618
|
_has_next = True
|
|
595
619
|
_get_next = lambda: self.list(
|
|
596
|
-
page=page + 1
|
|
620
|
+
page=page + 1,
|
|
597
621
|
page_size=page_size,
|
|
598
622
|
view=view,
|
|
599
623
|
project=project,
|
|
@@ -601,6 +625,7 @@ class AsyncTasksClient:
|
|
|
601
625
|
fields=fields,
|
|
602
626
|
review=review,
|
|
603
627
|
include=include,
|
|
628
|
+
query=query,
|
|
604
629
|
request_options=request_options,
|
|
605
630
|
)
|
|
606
631
|
_items = _parsed_response.tasks
|
|
@@ -668,7 +693,9 @@ class AsyncTasksClient:
|
|
|
668
693
|
raise ApiError(status_code=_response.status_code, body=_response.text)
|
|
669
694
|
raise ApiError(status_code=_response.status_code, body=_response_json)
|
|
670
695
|
|
|
671
|
-
async def get(
|
|
696
|
+
async def get(
|
|
697
|
+
self, id: str, *, request_options: typing.Optional[RequestOptions] = None
|
|
698
|
+
) -> DataManagerTaskSerializer:
|
|
672
699
|
"""
|
|
673
700
|
Get task data, metadata, annotations and other attributes for a specific labeling task by task ID.
|
|
674
701
|
The task ID is available from the Label Studio URL when viewing the task, or you can retrieve it programmatically with [Get task list](list).
|
|
@@ -683,7 +710,7 @@ class AsyncTasksClient:
|
|
|
683
710
|
|
|
684
711
|
Returns
|
|
685
712
|
-------
|
|
686
|
-
|
|
713
|
+
DataManagerTaskSerializer
|
|
687
714
|
Task
|
|
688
715
|
|
|
689
716
|
Examples
|
|
@@ -701,7 +728,7 @@ class AsyncTasksClient:
|
|
|
701
728
|
f"api/tasks/{jsonable_encoder(id)}/", method="GET", request_options=request_options
|
|
702
729
|
)
|
|
703
730
|
if 200 <= _response.status_code < 300:
|
|
704
|
-
return pydantic_v1.parse_obj_as(
|
|
731
|
+
return pydantic_v1.parse_obj_as(DataManagerTaskSerializer, _response.json()) # type: ignore
|
|
705
732
|
try:
|
|
706
733
|
_response_json = _response.json()
|
|
707
734
|
except JSONDecodeError:
|