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.

Files changed (38) hide show
  1. label_studio_sdk/__init__.py +4 -1
  2. label_studio_sdk/client.py +104 -85
  3. label_studio_sdk/data_manager.py +32 -23
  4. label_studio_sdk/exceptions.py +10 -0
  5. label_studio_sdk/label_interface/__init__.py +1 -0
  6. label_studio_sdk/label_interface/base.py +77 -0
  7. label_studio_sdk/label_interface/control_tags.py +756 -0
  8. label_studio_sdk/label_interface/interface.py +922 -0
  9. label_studio_sdk/label_interface/label_tags.py +72 -0
  10. label_studio_sdk/label_interface/object_tags.py +292 -0
  11. label_studio_sdk/label_interface/region.py +43 -0
  12. label_studio_sdk/objects.py +35 -0
  13. label_studio_sdk/project.py +725 -262
  14. label_studio_sdk/schema/label_config_schema.json +226 -0
  15. label_studio_sdk/users.py +15 -13
  16. label_studio_sdk/utils.py +31 -30
  17. label_studio_sdk/workspaces.py +13 -11
  18. {label_studio_sdk-0.0.30.dist-info → label_studio_sdk-0.0.34.dist-info}/METADATA +7 -5
  19. label_studio_sdk-0.0.34.dist-info/RECORD +37 -0
  20. {label_studio_sdk-0.0.30.dist-info → label_studio_sdk-0.0.34.dist-info}/WHEEL +1 -1
  21. {label_studio_sdk-0.0.30.dist-info → label_studio_sdk-0.0.34.dist-info}/top_level.txt +0 -1
  22. tests/test_client.py +21 -10
  23. tests/test_export.py +105 -0
  24. tests/test_interface/__init__.py +1 -0
  25. tests/test_interface/configs.py +137 -0
  26. tests/test_interface/mockups.py +22 -0
  27. tests/test_interface/test_compat.py +64 -0
  28. tests/test_interface/test_control_tags.py +55 -0
  29. tests/test_interface/test_data_generation.py +45 -0
  30. tests/test_interface/test_lpi.py +15 -0
  31. tests/test_interface/test_main.py +196 -0
  32. tests/test_interface/test_object_tags.py +36 -0
  33. tests/test_interface/test_region.py +36 -0
  34. tests/test_interface/test_validate_summary.py +35 -0
  35. tests/test_interface/test_validation.py +59 -0
  36. docs/__init__.py +0 -3
  37. label_studio_sdk-0.0.30.dist-info/RECORD +0 -15
  38. {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]]