label-studio-sdk 0.0.30__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 +104 -85
- 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 +725 -262
- 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.30.dist-info → label_studio_sdk-0.0.34.dist-info}/METADATA +7 -5
- label_studio_sdk-0.0.34.dist-info/RECORD +37 -0
- {label_studio_sdk-0.0.30.dist-info → label_studio_sdk-0.0.34.dist-info}/WHEEL +1 -1
- {label_studio_sdk-0.0.30.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.30.dist-info/RECORD +0 -15
- {label_studio_sdk-0.0.30.dist-info → label_studio_sdk-0.0.34.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import xml.etree.ElementTree
|
|
5
|
+
from typing import Dict, Optional, List, Tuple, Any
|
|
6
|
+
from .base import LabelStudioTag
|
|
7
|
+
from .region import Region
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_LABEL_TAGS = {"Label", "Choice", "Relation"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_parent_control_tag_name(tag, controls):
|
|
14
|
+
""" """
|
|
15
|
+
# Find parental <Choices> tag for nested tags like <Choices><View><View><Choice>...
|
|
16
|
+
parent = tag
|
|
17
|
+
while True:
|
|
18
|
+
parent = parent.getparent()
|
|
19
|
+
if parent is None:
|
|
20
|
+
return
|
|
21
|
+
name = parent.attrib.get("name")
|
|
22
|
+
if name in controls:
|
|
23
|
+
return name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LabelTag(LabelStudioTag):
|
|
27
|
+
"""
|
|
28
|
+
Class for Label Tag
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
value: Optional[str] = None
|
|
32
|
+
parent_name: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def validate_node(cls, tag: xml.etree.ElementTree.Element) -> bool:
|
|
36
|
+
"""Check if tag is input"""
|
|
37
|
+
return tag.tag in _LABEL_TAGS
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def parse_node(
|
|
41
|
+
cls,
|
|
42
|
+
tag: xml.etree.ElementTree.Element,
|
|
43
|
+
controls_context: Dict[str, "ControlTag"],
|
|
44
|
+
) -> "LabelTag":
|
|
45
|
+
"""
|
|
46
|
+
This class method parses a node and returns a LabelTag object if the node has a parent control tag and a value.
|
|
47
|
+
It first gets the name of the parent control tag.
|
|
48
|
+
If a parent control tag is found, it gets the value of the node from its 'alias' or 'value' attribute.
|
|
49
|
+
If a value is found, it returns a new LabelTag object with the tag name, attributes, parent control tag name, and value.
|
|
50
|
+
|
|
51
|
+
Parameters:
|
|
52
|
+
-----------
|
|
53
|
+
tag : xml.etree.ElementTree.Element
|
|
54
|
+
The node to be parsed.
|
|
55
|
+
controls_context : Dict[str, 'ControlTag']
|
|
56
|
+
The context of control tags.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
--------
|
|
60
|
+
LabelTag
|
|
61
|
+
A new LabelTag object with the tag name, attributes, parent control tag name, and value.
|
|
62
|
+
"""
|
|
63
|
+
parent_name = _get_parent_control_tag_name(tag, controls_context)
|
|
64
|
+
if parent_name is not None:
|
|
65
|
+
actual_value = tag.attrib.get("alias") or tag.attrib.get("value")
|
|
66
|
+
if actual_value is not None:
|
|
67
|
+
return LabelTag(
|
|
68
|
+
tag=tag.tag,
|
|
69
|
+
attr=dict(tag.attrib),
|
|
70
|
+
parent_name=parent_name,
|
|
71
|
+
value=actual_value,
|
|
72
|
+
)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import xml.etree.ElementTree
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .base import LabelStudioTag
|
|
12
|
+
|
|
13
|
+
_TAG_TO_CLASS = {
|
|
14
|
+
"audio": "AudioTag",
|
|
15
|
+
"image": "ImageTag",
|
|
16
|
+
"table": "TableTag",
|
|
17
|
+
"text": "TextTag",
|
|
18
|
+
"video": "VideoTag",
|
|
19
|
+
"hypertext": "HyperTextTag",
|
|
20
|
+
"list": "ListTag",
|
|
21
|
+
"paragraphs": "ParagraphsTag",
|
|
22
|
+
"timeseries": "TimeSeriesTag",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_DATA_EXAMPLES = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_strftime_string(s):
|
|
29
|
+
"""simple way to detect strftime format"""
|
|
30
|
+
return "%" in s
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_time_series_json(time_column, value_columns, time_format=None):
|
|
34
|
+
"""Generate sample for time series"""
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
n = 100
|
|
38
|
+
if time_format is not None and not _is_strftime_string(time_format):
|
|
39
|
+
time_fmt_map = {"yyyy-MM-dd": "%Y-%m-%d"}
|
|
40
|
+
time_format = time_fmt_map.get(time_format)
|
|
41
|
+
|
|
42
|
+
if time_format is None:
|
|
43
|
+
times = np.arange(n).tolist()
|
|
44
|
+
else:
|
|
45
|
+
raise NotImplementedError(
|
|
46
|
+
"time_format is not implemented yet - you need to install pandas for this."
|
|
47
|
+
)
|
|
48
|
+
# import pandas as pd
|
|
49
|
+
# times = pd.date_range('2020-01-01', periods=n, freq='D').strftime(time_format).tolist()
|
|
50
|
+
ts = {time_column: times}
|
|
51
|
+
for value_col in value_columns:
|
|
52
|
+
ts[value_col] = np.random.randn(n).tolist()
|
|
53
|
+
return ts
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def data_examples(
|
|
57
|
+
mode: str = "upload", hostname: str = "http://localhost:8080"
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""Data examples for editor preview and task upload examples"""
|
|
60
|
+
global _DATA_EXAMPLES
|
|
61
|
+
|
|
62
|
+
if _DATA_EXAMPLES is None:
|
|
63
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
64
|
+
file_path = os.path.join(base_dir, "data_examples.json")
|
|
65
|
+
|
|
66
|
+
with open(file_path, encoding="utf-8") as f:
|
|
67
|
+
_DATA_EXAMPLES = json.load(f)
|
|
68
|
+
|
|
69
|
+
roots = ["editor_preview", "upload"]
|
|
70
|
+
for root in roots:
|
|
71
|
+
for key, value in _DATA_EXAMPLES[root].items():
|
|
72
|
+
if isinstance(value, str):
|
|
73
|
+
_DATA_EXAMPLES[root][key] = value.replace(
|
|
74
|
+
"<HOSTNAME>", hostname
|
|
75
|
+
) # TODO settings.HOSTNAME
|
|
76
|
+
|
|
77
|
+
return _DATA_EXAMPLES[mode]
|
|
78
|
+
|
|
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
|
+
class ObjectTag(LabelStudioTag):
|
|
87
|
+
"""
|
|
88
|
+
Class that represents a ObjectTag in Label Studio
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
-----------
|
|
92
|
+
name: Optional[str]
|
|
93
|
+
The name of the tag
|
|
94
|
+
value: Optional[str]
|
|
95
|
+
The value of the tag
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
name: Optional[str] = None
|
|
99
|
+
value: Optional[str] = None
|
|
100
|
+
# value_type: Optional[str] = None,
|
|
101
|
+
|
|
102
|
+
# TODO needs to set during parsing
|
|
103
|
+
# self._value_type = value_type
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def parse_node(cls, tag: xml.etree.ElementTree.Element) -> "ObjectTag":
|
|
107
|
+
"""
|
|
108
|
+
This class method parses a node and returns a ObjectTag object if the node has a name and a value.
|
|
109
|
+
|
|
110
|
+
Parameters:
|
|
111
|
+
-----------
|
|
112
|
+
tag : xml.etree.ElementTree.Element
|
|
113
|
+
The node to be parsed.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
--------
|
|
117
|
+
ObjectTag
|
|
118
|
+
A new ObjectTag object with the tag name, attributes, name, and value.
|
|
119
|
+
"""
|
|
120
|
+
tag_class = get_tag_class(tag.tag) or cls
|
|
121
|
+
|
|
122
|
+
return tag_class(
|
|
123
|
+
tag=tag.tag,
|
|
124
|
+
attr=dict(tag.attrib),
|
|
125
|
+
name=tag.attrib.get("name"),
|
|
126
|
+
value=tag.attrib["value"],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def validate_node(cls, tag: xml.etree.ElementTree.Element) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Check if tag is input
|
|
133
|
+
"""
|
|
134
|
+
return bool(tag.attrib.get("name") and tag.attrib.get("value"))
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def value_type(self):
|
|
138
|
+
return self.attr.get("valueType") or self.attr.get("valuetype")
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def value_name(self):
|
|
142
|
+
""" """
|
|
143
|
+
# TODO this needs a check for URL
|
|
144
|
+
return self.value[1:]
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def value_is_variable(self) -> bool:
|
|
148
|
+
"""Check if value has variable"""
|
|
149
|
+
pattern = re.compile(r"^\$[A-Za-z_]+$")
|
|
150
|
+
return bool(pattern.fullmatch(self.value))
|
|
151
|
+
|
|
152
|
+
# TODO this should not be here as soon as we cover all the tags
|
|
153
|
+
|
|
154
|
+
# and have generate_example in each
|
|
155
|
+
def generate_example_value(self, mode="upload", secure_mode=False):
|
|
156
|
+
""" """
|
|
157
|
+
examples = data_examples(mode=mode)
|
|
158
|
+
only_urls = secure_mode or self.value_type == "url"
|
|
159
|
+
if hasattr(self, "_generate_example"):
|
|
160
|
+
return self._generate_example(examples, only_urls=only_urls)
|
|
161
|
+
example_from_field_name = examples.get("$" + self.value, None)
|
|
162
|
+
if example_from_field_name:
|
|
163
|
+
return example_from_field_name
|
|
164
|
+
|
|
165
|
+
if self.tag.lower().endswith("labels"):
|
|
166
|
+
return examples["Labels"]
|
|
167
|
+
|
|
168
|
+
if self.tag.lower() == "choices":
|
|
169
|
+
allow_nested = (
|
|
170
|
+
self.attr.get("allowNested") or self.attr.get("allownested") or "false"
|
|
171
|
+
)
|
|
172
|
+
return examples["NestedChoices" if allow_nested == "true" else "Choices"]
|
|
173
|
+
|
|
174
|
+
# patch for valueType="url"
|
|
175
|
+
examples["Text"] = examples["TextUrl"] if only_urls else examples["TextRaw"]
|
|
176
|
+
# not found by name, try get example by type
|
|
177
|
+
return examples.get(self.tag, "Something")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class AudioTag(ObjectTag):
|
|
181
|
+
""" """
|
|
182
|
+
|
|
183
|
+
def _generate_example(self, examples, only_urls=False):
|
|
184
|
+
""" """
|
|
185
|
+
return examples.get("Audio")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ImageTag(ObjectTag):
|
|
189
|
+
""" """
|
|
190
|
+
|
|
191
|
+
def _generate_example(self, examples, only_urls=False):
|
|
192
|
+
""" """
|
|
193
|
+
return examples.get("Image")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TableTag(ObjectTag):
|
|
197
|
+
""" """
|
|
198
|
+
|
|
199
|
+
def _generate_example(self, examples, only_urls=False):
|
|
200
|
+
""" """
|
|
201
|
+
return examples.get("Table")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TextTag(ObjectTag):
|
|
205
|
+
""" """
|
|
206
|
+
|
|
207
|
+
def _generate_example(self, examples, only_urls=False):
|
|
208
|
+
""" """
|
|
209
|
+
if only_urls:
|
|
210
|
+
return examples.get("TextUrl")
|
|
211
|
+
else:
|
|
212
|
+
return examples.get("TextRaw")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class VideoTag(ObjectTag):
|
|
216
|
+
""" """
|
|
217
|
+
|
|
218
|
+
def _generate_example(self, examples, only_urls=False):
|
|
219
|
+
""" """
|
|
220
|
+
return examples.get("Video")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class HyperTextTag(ObjectTag):
|
|
224
|
+
""" """
|
|
225
|
+
|
|
226
|
+
def _generate_example(self, examples, only_urls=False):
|
|
227
|
+
""" """
|
|
228
|
+
examples = data_examples(mode="upload")
|
|
229
|
+
if self.value == "video":
|
|
230
|
+
return examples.get("$videoHack")
|
|
231
|
+
else:
|
|
232
|
+
return examples["HyperTextUrl" if only_urls else "HyperText"]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ListTag(ObjectTag):
|
|
236
|
+
""" """
|
|
237
|
+
|
|
238
|
+
def _generate_example(self, examples, only_urls=False):
|
|
239
|
+
""" """
|
|
240
|
+
examples = data_examples(mode="upload")
|
|
241
|
+
return examples.get("List")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class ParagraphsTag(ObjectTag):
|
|
245
|
+
""" """
|
|
246
|
+
|
|
247
|
+
def _generate_example(self, examples, only_urls=False):
|
|
248
|
+
""" """
|
|
249
|
+
# Paragraphs special case - replace nameKey/textKey if presented
|
|
250
|
+
p = self.attr
|
|
251
|
+
|
|
252
|
+
name_key = p.get("nameKey") or p.get("namekey") or "author"
|
|
253
|
+
text_key = p.get("textKey") or p.get("textkey") or "text"
|
|
254
|
+
|
|
255
|
+
if only_urls:
|
|
256
|
+
params = {"nameKey": name_key, "textKey": text_key}
|
|
257
|
+
return examples.get("ParagraphsUrl") + urlencode(params)
|
|
258
|
+
|
|
259
|
+
return [
|
|
260
|
+
{name_key: item["author"], text_key: item["text"]}
|
|
261
|
+
for item in examples.get("Paragraphs")
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class TimeSeriesTag(ObjectTag):
|
|
266
|
+
""" """
|
|
267
|
+
|
|
268
|
+
def _generate_example(self, examples, only_urls=False):
|
|
269
|
+
""" """
|
|
270
|
+
p = self.attr
|
|
271
|
+
|
|
272
|
+
time_column = p.get("timeColumn", "time")
|
|
273
|
+
value_columns = []
|
|
274
|
+
for ts_child in p:
|
|
275
|
+
if ts_child.tag != "Channel":
|
|
276
|
+
continue
|
|
277
|
+
value_columns.append(ts_child.get("column"))
|
|
278
|
+
sep = p.get("sep")
|
|
279
|
+
time_format = p.get("timeFormat")
|
|
280
|
+
|
|
281
|
+
if only_urls:
|
|
282
|
+
# data is URL
|
|
283
|
+
params = {"time": time_column, "values": ",".join(value_columns)}
|
|
284
|
+
if sep:
|
|
285
|
+
params["sep"] = sep
|
|
286
|
+
if time_format:
|
|
287
|
+
params["tf"] = time_format
|
|
288
|
+
|
|
289
|
+
return "/samples/time-series.csv?" + urlencode(params)
|
|
290
|
+
else:
|
|
291
|
+
# data is JSON
|
|
292
|
+
return generate_time_series_json(time_column, value_columns, time_format)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
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
|
+
|
|
30
|
+
def _dict(self):
|
|
31
|
+
""" """
|
|
32
|
+
return {
|
|
33
|
+
"id": self.id,
|
|
34
|
+
"from_name": self.from_tag.name,
|
|
35
|
+
"to_name": self.to_tag.name,
|
|
36
|
+
"type": self.from_tag.tag.lower(),
|
|
37
|
+
# TODO This needs to be improved
|
|
38
|
+
"value": self.value.dict(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def to_json(self):
|
|
42
|
+
""" """
|
|
43
|
+
return json.dumps(self._dict())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Type, Dict, Optional, List, Tuple, Any, Union
|
|
2
|
+
from pydantic import BaseModel, confloat
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PredictionValue(BaseModel):
|
|
6
|
+
""" """
|
|
7
|
+
|
|
8
|
+
model_version: Optional[Any] = None
|
|
9
|
+
score: Optional[float] = 0.00
|
|
10
|
+
result: Optional[List[Any]] = []
|
|
11
|
+
# cluster: Optional[Any] = None
|
|
12
|
+
# neighbors: Optional[Any] = None
|
|
13
|
+
|
|
14
|
+
def serialize(self):
|
|
15
|
+
from label_studio_sdk.label_interface.region import Region
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
"model_version": self.model_version,
|
|
19
|
+
"score": self.score,
|
|
20
|
+
"result": [r._dict() if isinstance(r, Region) else r for r in self.result],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AnnotationValue(BaseModel):
|
|
25
|
+
""" """
|
|
26
|
+
|
|
27
|
+
result: Optional[List[dict]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TaskValue(BaseModel):
|
|
31
|
+
""" """
|
|
32
|
+
|
|
33
|
+
data: Optional[dict]
|
|
34
|
+
annotations: Optional[List[AnnotationValue]]
|
|
35
|
+
predictions: Optional[List[PredictionValue]]
|