label-studio-sdk 0.0.30__tar.gz → 0.0.34__tar.gz
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-0.0.34/MANIFEST.in +2 -0
- {label-studio-sdk-0.0.30/label_studio_sdk.egg-info → label-studio-sdk-0.0.34}/PKG-INFO +1 -1
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/README.md +2 -13
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk/__init__.py +4 -1
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk/client.py +104 -85
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk/data_manager.py +32 -23
- label-studio-sdk-0.0.34/label_studio_sdk/exceptions.py +10 -0
- label-studio-sdk-0.0.34/label_studio_sdk/label_interface/__init__.py +1 -0
- label-studio-sdk-0.0.34/label_studio_sdk/label_interface/base.py +77 -0
- label-studio-sdk-0.0.34/label_studio_sdk/label_interface/control_tags.py +756 -0
- label-studio-sdk-0.0.34/label_studio_sdk/label_interface/interface.py +922 -0
- label-studio-sdk-0.0.34/label_studio_sdk/label_interface/label_tags.py +72 -0
- label-studio-sdk-0.0.34/label_studio_sdk/label_interface/object_tags.py +292 -0
- label-studio-sdk-0.0.34/label_studio_sdk/label_interface/region.py +43 -0
- label-studio-sdk-0.0.34/label_studio_sdk/objects.py +35 -0
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk/project.py +725 -262
- label-studio-sdk-0.0.34/label_studio_sdk/schema/label_config_schema.json +226 -0
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk/users.py +15 -13
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk/utils.py +31 -30
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk/workspaces.py +13 -11
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34/label_studio_sdk.egg-info}/PKG-INFO +1 -1
- label-studio-sdk-0.0.34/label_studio_sdk.egg-info/SOURCES.txt +42 -0
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk.egg-info/requires.txt +2 -0
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk.egg-info/top_level.txt +0 -1
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/requirements.txt +2 -0
- label-studio-sdk-0.0.34/setup.py +35 -0
- label-studio-sdk-0.0.34/tests/test_client.py +37 -0
- label-studio-sdk-0.0.34/tests/test_export.py +105 -0
- label-studio-sdk-0.0.34/tests/test_interface/__init__.py +1 -0
- label-studio-sdk-0.0.34/tests/test_interface/configs.py +137 -0
- label-studio-sdk-0.0.34/tests/test_interface/mockups.py +22 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_compat.py +64 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_control_tags.py +55 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_data_generation.py +45 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_lpi.py +15 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_main.py +196 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_object_tags.py +36 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_region.py +36 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_validate_summary.py +35 -0
- label-studio-sdk-0.0.34/tests/test_interface/test_validation.py +59 -0
- label-studio-sdk-0.0.30/MANIFEST.in +0 -1
- label-studio-sdk-0.0.30/docs/__init__.py +0 -3
- label-studio-sdk-0.0.30/label_studio_sdk.egg-info/SOURCES.txt +0 -20
- label-studio-sdk-0.0.30/setup.py +0 -33
- label-studio-sdk-0.0.30/tests/test_client.py +0 -26
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/LICENSE +0 -0
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/label_studio_sdk.egg-info/dependency_links.txt +0 -0
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/setup.cfg +0 -0
- {label-studio-sdk-0.0.30 → label-studio-sdk-0.0.34}/tests/__init__.py +0 -0
|
@@ -18,7 +18,7 @@ This is the first release of the Label Studio SDK. It supports Label Studio Ente
|
|
|
18
18
|
|
|
19
19
|
- **Find a bug?** [Create a GitHub issue](https://github.com/heartexlabs/label-studio-sdk/issues)!
|
|
20
20
|
- **Have a question?** [Join the Slack Community](https://slack.labelstud.io/?source=github-sdk)!
|
|
21
|
-
- **Want to contribute?** [See the contributing guide](
|
|
21
|
+
- **Want to contribute?** [See the contributing guide](CONTRIBUTING.md)
|
|
22
22
|
|
|
23
23
|
## Quickstart
|
|
24
24
|
To start using the SDK in your machine learning and data science projects and pipelines, do the following:
|
|
@@ -57,15 +57,4 @@ If you want to extend the SDK:
|
|
|
57
57
|
|
|
58
58
|
## Examples
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
### Active learning example
|
|
63
|
-
|
|
64
|
-
If you want to write a Python script to set up an active learning workflow for labeling and training, review this [working active learning example as a Jupyter notebook](https://github.com/heartexlabs/label-studio-sdk/blob/master/examples/active_learning/active_learning.ipynb) or start with the [active learning python script example](https://github.com/heartexlabs/label-studio-sdk/blob/master/examples/active_learning/active_learning.py).
|
|
65
|
-
|
|
66
|
-
### Weak supervision example
|
|
67
|
-
|
|
68
|
-
If you want to write a Python script to perform programmatic labeling and use weak supervision to correct the noisy labels, refer to this [working weak supervision example as a Jupyter notebook](https://github.com/heartexlabs/label-studio-sdk/blob/master/examples/weak_supervision/weak_supervision.ipynb) or start with the [weak supervision python script example](https://github.com/heartexlabs/label-studio-sdk/blob/master/examples/weak_supervision/weak_supervision.py).
|
|
69
|
-
|
|
70
|
-
<img src="https://labelstud.io/images/opossum/other/5.svg" width="400px">
|
|
71
|
-
|
|
60
|
+
Please check [the examples folder](examples), there are many of very useful codes that you can learn from.
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
""" .. include::../docs/client.md
|
|
2
2
|
"""
|
|
3
|
-
import os
|
|
4
3
|
|
|
5
4
|
import json
|
|
6
|
-
import warnings
|
|
7
5
|
import logging
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
10
|
from typing import Optional
|
|
11
11
|
from pydantic import BaseModel, constr, root_validator
|
|
12
12
|
from requests.adapters import HTTPAdapter
|
|
13
|
-
from types import SimpleNamespace
|
|
14
13
|
|
|
15
14
|
logger = logging.getLogger(__name__)
|
|
16
15
|
|
|
17
16
|
MAX_RETRIES = 3
|
|
18
|
-
TIMEOUT = (10.0, 180
|
|
17
|
+
TIMEOUT = (10.0, int(os.environ.get('TIMEOUT', 180)))
|
|
19
18
|
HEADERS = {}
|
|
20
|
-
LABEL_STUDIO_DEFAULT_URL =
|
|
19
|
+
LABEL_STUDIO_DEFAULT_URL = "http://localhost:8080"
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class ClientCredentials(BaseModel):
|
|
@@ -28,11 +27,11 @@ class ClientCredentials(BaseModel):
|
|
|
28
27
|
@root_validator(pre=True, allow_reuse=True)
|
|
29
28
|
def either_key_or_email_password(cls, values):
|
|
30
29
|
assert (
|
|
31
|
-
|
|
32
|
-
),
|
|
30
|
+
"email" in values or "api_key" in values
|
|
31
|
+
), "At least one of email or api_key should be included"
|
|
33
32
|
assert (
|
|
34
|
-
|
|
35
|
-
),
|
|
33
|
+
"email" not in values or "password" in values
|
|
34
|
+
), "Provide both email and password for login auth"
|
|
36
35
|
return values
|
|
37
36
|
|
|
38
37
|
|
|
@@ -74,17 +73,27 @@ class Client(object):
|
|
|
74
73
|
If true, make_request will raise exceptions on request errors
|
|
75
74
|
"""
|
|
76
75
|
if not url:
|
|
77
|
-
url = os.getenv(
|
|
78
|
-
self.url = url.rstrip(
|
|
76
|
+
url = os.getenv("LABEL_STUDIO_URL", LABEL_STUDIO_DEFAULT_URL)
|
|
77
|
+
self.url = url.rstrip("/")
|
|
79
78
|
self.make_request_raise = make_request_raise
|
|
80
79
|
self.session = session or self.get_session()
|
|
81
80
|
|
|
81
|
+
# set cookies
|
|
82
|
+
self.cookies = cookies
|
|
83
|
+
|
|
82
84
|
# set api key or get it using credentials (username and password)
|
|
83
85
|
if api_key is None and credentials is None:
|
|
84
|
-
api_key = os.getenv(
|
|
86
|
+
api_key = os.getenv("LABEL_STUDIO_API_KEY")
|
|
85
87
|
|
|
86
88
|
if api_key is not None:
|
|
87
89
|
credentials = ClientCredentials(api_key=api_key)
|
|
90
|
+
|
|
91
|
+
if api_key is None and credentials is None:
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
"If neither 'api_key' nor 'credentials' are provided, 'LABEL_STUDIO_API_KEY' environment variable must "
|
|
94
|
+
"be set"
|
|
95
|
+
)
|
|
96
|
+
|
|
88
97
|
self.api_key = (
|
|
89
98
|
credentials.api_key
|
|
90
99
|
if credentials.api_key
|
|
@@ -92,18 +101,15 @@ class Client(object):
|
|
|
92
101
|
)
|
|
93
102
|
|
|
94
103
|
# set headers
|
|
95
|
-
self.headers = {
|
|
104
|
+
self.headers = {"Authorization": f"Token {self.api_key}"}
|
|
96
105
|
if oidc_token:
|
|
97
|
-
self.headers.update({
|
|
106
|
+
self.headers.update({"Proxy-Authorization": f"Bearer {oidc_token}"})
|
|
98
107
|
if extra_headers:
|
|
99
108
|
self.headers.update(extra_headers)
|
|
100
109
|
|
|
101
|
-
# set cookies
|
|
102
|
-
self.cookies = cookies
|
|
103
|
-
|
|
104
110
|
# set versions from /version endpoint
|
|
105
111
|
self.versions = versions if versions else self.get_versions()
|
|
106
|
-
self.is_enterprise =
|
|
112
|
+
self.is_enterprise = "label-studio-enterprise-backend" in self.versions
|
|
107
113
|
|
|
108
114
|
def get_versions(self):
|
|
109
115
|
"""Call /version api and get all Label Studio component versions
|
|
@@ -113,14 +119,14 @@ class Client(object):
|
|
|
113
119
|
dict with Label Studio component names and their versions
|
|
114
120
|
|
|
115
121
|
"""
|
|
116
|
-
self.versions = self.make_request(
|
|
122
|
+
self.versions = self.make_request("GET", "/api/version").json()
|
|
117
123
|
return self.versions
|
|
118
124
|
|
|
119
125
|
def get_api_key(self, credentials: ClientCredentials):
|
|
120
126
|
login_url = self.get_url("/user/login")
|
|
121
127
|
# Retrieve and set the CSRF token first
|
|
122
128
|
self.session.get(login_url)
|
|
123
|
-
csrf_token = self.session.cookies.get(
|
|
129
|
+
csrf_token = self.session.cookies.get("csrftoken", None)
|
|
124
130
|
login_data = dict(**credentials.dict(), csrfmiddlewaretoken=csrf_token)
|
|
125
131
|
self.session.post(
|
|
126
132
|
login_url,
|
|
@@ -143,7 +149,7 @@ class Client(object):
|
|
|
143
149
|
dict
|
|
144
150
|
Status string like "UP"
|
|
145
151
|
"""
|
|
146
|
-
response = self.make_request(
|
|
152
|
+
response = self.make_request("GET", "/health")
|
|
147
153
|
return response.json()
|
|
148
154
|
|
|
149
155
|
def get_projects(self, **query_params):
|
|
@@ -164,7 +170,7 @@ class Client(object):
|
|
|
164
170
|
dict
|
|
165
171
|
Status string
|
|
166
172
|
"""
|
|
167
|
-
response = self.make_request(
|
|
173
|
+
response = self.make_request("DELETE", f"/api/projects/{project_id}/")
|
|
168
174
|
return response
|
|
169
175
|
|
|
170
176
|
def delete_all_projects(self):
|
|
@@ -176,7 +182,7 @@ class Client(object):
|
|
|
176
182
|
List of (dict) status strings
|
|
177
183
|
"""
|
|
178
184
|
responses = []
|
|
179
|
-
project_ids = [project.get_params()[
|
|
185
|
+
project_ids = [project.get_params()["id"] for project in self.list_projects()]
|
|
180
186
|
for project_id in project_ids:
|
|
181
187
|
response = self.delete_project(project_id)
|
|
182
188
|
responses.append(response)
|
|
@@ -192,14 +198,14 @@ class Client(object):
|
|
|
192
198
|
"""
|
|
193
199
|
from .project import Project
|
|
194
200
|
|
|
195
|
-
params = {
|
|
201
|
+
params = {"page_size": 10000000}
|
|
196
202
|
params.update(query_params)
|
|
197
|
-
response = self.make_request(
|
|
203
|
+
response = self.make_request("GET", "/api/projects", params=params)
|
|
198
204
|
if response.status_code == 200:
|
|
199
205
|
projects = []
|
|
200
|
-
for data in response.json()[
|
|
206
|
+
for data in response.json()["results"]:
|
|
201
207
|
project = Project._create_from_id(
|
|
202
|
-
client=self, project_id=data[
|
|
208
|
+
client=self, project_id=data["id"], params=data
|
|
203
209
|
)
|
|
204
210
|
projects.append(project)
|
|
205
211
|
logger.debug(
|
|
@@ -264,10 +270,10 @@ class Client(object):
|
|
|
264
270
|
"""
|
|
265
271
|
from .users import User
|
|
266
272
|
|
|
267
|
-
response = self.make_request(
|
|
273
|
+
response = self.make_request("GET", "/api/users")
|
|
268
274
|
users = []
|
|
269
275
|
for user_data in response.json():
|
|
270
|
-
user_data[
|
|
276
|
+
user_data["client"] = self
|
|
271
277
|
users.append(User(**user_data))
|
|
272
278
|
return users
|
|
273
279
|
|
|
@@ -292,28 +298,28 @@ class Client(object):
|
|
|
292
298
|
|
|
293
299
|
payload = (
|
|
294
300
|
{
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
301
|
+
"username": user.username if user.username else user.email,
|
|
302
|
+
"email": user.email,
|
|
303
|
+
"first_name": user.first_name,
|
|
304
|
+
"last_name": user.last_name,
|
|
305
|
+
"phone": user.phone,
|
|
300
306
|
}
|
|
301
307
|
if isinstance(user, User)
|
|
302
308
|
else user
|
|
303
309
|
)
|
|
304
310
|
|
|
305
311
|
response = self.make_request(
|
|
306
|
-
|
|
312
|
+
"POST", "/api/users", json=payload, raise_exceptions=False
|
|
307
313
|
)
|
|
308
314
|
user_data = response.json()
|
|
309
|
-
user_data[
|
|
315
|
+
user_data["client"] = self
|
|
310
316
|
|
|
311
317
|
if response.status_code < 400:
|
|
312
318
|
return User(**user_data)
|
|
313
319
|
else:
|
|
314
|
-
if
|
|
320
|
+
if "already exists" in response.text and exist_ok is True:
|
|
315
321
|
return None
|
|
316
|
-
logger.error(
|
|
322
|
+
logger.error("Create user error: " + str(response.json()))
|
|
317
323
|
return None
|
|
318
324
|
|
|
319
325
|
def get_workspaces(self):
|
|
@@ -333,13 +339,48 @@ class Client(object):
|
|
|
333
339
|
self.is_enterprise
|
|
334
340
|
), "Workspaces are available only for Enterprise instance of Label Studio"
|
|
335
341
|
|
|
336
|
-
response = self.make_request(
|
|
342
|
+
response = self.make_request("GET", "/api/workspaces")
|
|
337
343
|
workspaces = []
|
|
338
344
|
for workspace_data in response.json():
|
|
339
|
-
workspace_data[
|
|
345
|
+
workspace_data["client"] = self
|
|
340
346
|
workspaces.append(Workspace(**workspace_data))
|
|
341
347
|
return workspaces
|
|
342
348
|
|
|
349
|
+
# write function get_workspace_by_title
|
|
350
|
+
def get_workspace_by_title(self, title):
|
|
351
|
+
"""Return workspace by title from the current organization account
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
title: str
|
|
356
|
+
Workspace title
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
`label_studio_sdk.workspaces.Workspace` or None
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
workspaces = self.get_workspaces()
|
|
364
|
+
for workspace in workspaces:
|
|
365
|
+
if workspace.title == title:
|
|
366
|
+
return workspace
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
def get_organization(self):
|
|
370
|
+
"""Return active organization for the current user
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
dict
|
|
375
|
+
"""
|
|
376
|
+
# get organization id from the current user api
|
|
377
|
+
response = self.make_request('GET', '/api/current-user/whoami').json()
|
|
378
|
+
organization_id = response['active_organization']
|
|
379
|
+
|
|
380
|
+
# get organization data by id
|
|
381
|
+
response = self.make_request("GET", f"/api/organizations/{organization_id}")
|
|
382
|
+
return response.json()
|
|
383
|
+
|
|
343
384
|
def get_session(self):
|
|
344
385
|
"""Create a session with requests.Session()
|
|
345
386
|
|
|
@@ -350,8 +391,8 @@ class Client(object):
|
|
|
350
391
|
"""
|
|
351
392
|
session = requests.Session()
|
|
352
393
|
session.headers.update(HEADERS)
|
|
353
|
-
session.mount(
|
|
354
|
-
session.mount(
|
|
394
|
+
session.mount("http://", HTTPAdapter(max_retries=MAX_RETRIES))
|
|
395
|
+
session.mount("https://", HTTPAdapter(max_retries=MAX_RETRIES))
|
|
355
396
|
return session
|
|
356
397
|
|
|
357
398
|
def get_url(self, suffix):
|
|
@@ -384,14 +425,14 @@ class Client(object):
|
|
|
384
425
|
Response object for the relevant endpoint.
|
|
385
426
|
|
|
386
427
|
"""
|
|
387
|
-
if
|
|
388
|
-
kwargs[
|
|
428
|
+
if "timeout" not in kwargs:
|
|
429
|
+
kwargs["timeout"] = TIMEOUT
|
|
389
430
|
|
|
390
431
|
raise_exceptions = self.make_request_raise
|
|
391
|
-
if
|
|
392
|
-
raise_exceptions = kwargs.pop(
|
|
432
|
+
if "raise_exceptions" in kwargs: # kwargs have higher priority
|
|
433
|
+
raise_exceptions = kwargs.pop("raise_exceptions")
|
|
393
434
|
|
|
394
|
-
logger.debug(f
|
|
435
|
+
logger.debug(f"{method}: {url} with args={args}, kwargs={kwargs}")
|
|
395
436
|
response = self.session.request(
|
|
396
437
|
method,
|
|
397
438
|
self.get_url(url),
|
|
@@ -403,49 +444,27 @@ class Client(object):
|
|
|
403
444
|
|
|
404
445
|
if raise_exceptions:
|
|
405
446
|
if response.status_code >= 400:
|
|
406
|
-
|
|
407
|
-
content = json.dumps(json.loads(response.content), indent=2)
|
|
408
|
-
except:
|
|
409
|
-
content = response.text
|
|
410
|
-
|
|
411
|
-
logger.error(
|
|
412
|
-
f'\n--------------------------------------------\n'
|
|
413
|
-
f'Request URL: {response.url}\n'
|
|
414
|
-
f'Response status code: {response.status_code}\n'
|
|
415
|
-
f'Response content:\n{content}\n\n'
|
|
416
|
-
f'SDK error traceback:'
|
|
417
|
-
)
|
|
447
|
+
self.log_response_error(response)
|
|
418
448
|
response.raise_for_status()
|
|
419
449
|
|
|
420
450
|
return response
|
|
421
451
|
|
|
422
|
-
def
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
containing the same fields as in the original storage request and:
|
|
436
|
-
|
|
437
|
-
id: int
|
|
438
|
-
Storage ID
|
|
439
|
-
type: str
|
|
440
|
-
Type of storage
|
|
441
|
-
created_at: str
|
|
442
|
-
Creation time
|
|
443
|
-
last_sync: str
|
|
444
|
-
Time last sync finished, can be empty.
|
|
445
|
-
last_sync_count: int
|
|
446
|
-
Number of tasks synced in the last sync
|
|
447
|
-
"""
|
|
452
|
+
def log_response_error(self, response):
|
|
453
|
+
try:
|
|
454
|
+
content = json.dumps(json.loads(response.content), indent=2)
|
|
455
|
+
except:
|
|
456
|
+
content = response.text
|
|
457
|
+
|
|
458
|
+
logger.error(
|
|
459
|
+
f"\n--------------------------------------------\n"
|
|
460
|
+
f"Request URL: {response.url}\n"
|
|
461
|
+
f"Response status code: {response.status_code}\n"
|
|
462
|
+
f"Response content:\n{content}\n\n"
|
|
463
|
+
f"SDK error traceback:"
|
|
464
|
+
)
|
|
448
465
|
|
|
466
|
+
def sync_storage(self, storage_type, storage_id):
|
|
467
|
+
"""See project.sync_storage for more info"""
|
|
449
468
|
response = self.make_request(
|
|
450
469
|
"POST", f"/api/storages/{storage_type}/{str(storage_id)}/sync"
|
|
451
470
|
)
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
tasks = project.get_tasks(filters=filters)
|
|
32
32
|
```
|
|
33
33
|
"""
|
|
34
|
+
|
|
34
35
|
from datetime import datetime
|
|
35
36
|
|
|
36
|
-
DATETIME_FORMAT =
|
|
37
|
+
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class Filters:
|
|
@@ -41,9 +42,9 @@ class Filters:
|
|
|
41
42
|
Use the methods and variables in this class to create and combine filters for tasks on the Label Studio Data Manager.
|
|
42
43
|
"""
|
|
43
44
|
|
|
44
|
-
OR =
|
|
45
|
+
OR = "or"
|
|
45
46
|
"""Combine filters with an OR"""
|
|
46
|
-
AND =
|
|
47
|
+
AND = "and"
|
|
47
48
|
"""Combine filters with an AND"""
|
|
48
49
|
|
|
49
50
|
@staticmethod
|
|
@@ -85,7 +86,7 @@ class Filters:
|
|
|
85
86
|
dict
|
|
86
87
|
"""
|
|
87
88
|
return {
|
|
88
|
-
"filter":
|
|
89
|
+
"filter": "filter:" + name,
|
|
89
90
|
"operator": operator,
|
|
90
91
|
"type": column_type,
|
|
91
92
|
"value": value,
|
|
@@ -106,7 +107,7 @@ class Filters:
|
|
|
106
107
|
datetime in `'%Y-%m-%dT%H:%M:%S.%fZ'` format
|
|
107
108
|
|
|
108
109
|
"""
|
|
109
|
-
assert isinstance(dt, datetime),
|
|
110
|
+
assert isinstance(dt, datetime), "dt must be datetime type"
|
|
110
111
|
return dt.strftime(DATETIME_FORMAT)
|
|
111
112
|
|
|
112
113
|
@classmethod
|
|
@@ -133,7 +134,7 @@ class Filters:
|
|
|
133
134
|
if maximum is not None:
|
|
134
135
|
if isinstance(maximum, datetime):
|
|
135
136
|
maximum = cls.datetime(maximum)
|
|
136
|
-
return {
|
|
137
|
+
return {"min": value, "max": maximum}
|
|
137
138
|
|
|
138
139
|
return value
|
|
139
140
|
|
|
@@ -160,13 +161,13 @@ class Operator:
|
|
|
160
161
|
class Type:
|
|
161
162
|
"""Specify the type of data in a column."""
|
|
162
163
|
|
|
163
|
-
Number =
|
|
164
|
-
Datetime =
|
|
165
|
-
Boolean =
|
|
166
|
-
String =
|
|
164
|
+
Number = "Number"
|
|
165
|
+
Datetime = "Datetime"
|
|
166
|
+
Boolean = "Boolean"
|
|
167
|
+
String = "String"
|
|
167
168
|
List = "List"
|
|
168
169
|
|
|
169
|
-
Unknown =
|
|
170
|
+
Unknown = "Unknown"
|
|
170
171
|
""" Unknown is explicitly converted to string format. """
|
|
171
172
|
|
|
172
173
|
|
|
@@ -175,6 +176,8 @@ class Column:
|
|
|
175
176
|
|
|
176
177
|
id = "tasks:id"
|
|
177
178
|
"""Task ID"""
|
|
179
|
+
inner_id = "tasks:inner_id"
|
|
180
|
+
"""Task Inner ID, it starts from 1 for all projects"""
|
|
178
181
|
ground_truth = "tasks:ground_truth"
|
|
179
182
|
"""Ground truth status of the tasks"""
|
|
180
183
|
annotations_results = "tasks:annotations_results"
|
|
@@ -191,6 +194,8 @@ class Column:
|
|
|
191
194
|
"""Name of the file uploaded to create the tasks"""
|
|
192
195
|
created_at = "tasks:created_at"
|
|
193
196
|
"""Time the task was created at"""
|
|
197
|
+
updated_at = "tasks:updated_at"
|
|
198
|
+
"""Time the task was updated at (e.g. new annotation was created, review added, etc)"""
|
|
194
199
|
annotators = "tasks:annotators"
|
|
195
200
|
"""Annotators that completed the task (Community). Can include assigned annotators (Enterprise only)"""
|
|
196
201
|
total_predictions = "tasks:total_predictions"
|
|
@@ -209,6 +214,10 @@ class Column:
|
|
|
209
214
|
"""Number of annotations rejected for a task in review (Enterprise only)"""
|
|
210
215
|
reviews_accepted = "tasks:reviews_accepted"
|
|
211
216
|
"""Number of annotations accepted for a task in review (Enterprise only)"""
|
|
217
|
+
comments = "tasks:comments"
|
|
218
|
+
"""Number of comments in a task"""
|
|
219
|
+
unresolved_comment_count = "tasks:unresolved_comment_count"
|
|
220
|
+
"""Number of unresolved comments in a task"""
|
|
212
221
|
|
|
213
222
|
@staticmethod
|
|
214
223
|
def data(task_field):
|
|
@@ -246,21 +255,21 @@ def _test():
|
|
|
246
255
|
)
|
|
247
256
|
|
|
248
257
|
assert filters == {
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
"conjunction": "or",
|
|
259
|
+
"items": [
|
|
251
260
|
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
"filter": "filter:tasks:id",
|
|
262
|
+
"operator": "greater",
|
|
263
|
+
"type": "Number",
|
|
264
|
+
"value": 42,
|
|
256
265
|
},
|
|
257
266
|
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
"filter": "filter:tasks:completed_at",
|
|
268
|
+
"operator": "in",
|
|
269
|
+
"type": "Datetime",
|
|
270
|
+
"value": {
|
|
271
|
+
"min": "2021-11-01T00:00:00.000000Z",
|
|
272
|
+
"max": "2021-11-05T00:00:00.000000Z",
|
|
264
273
|
},
|
|
265
274
|
},
|
|
266
275
|
],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .interface import LabelInterface
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Dict, Optional, List, Tuple, Any, Callable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LabelStudioTag(BaseModel):
|
|
6
|
+
"""
|
|
7
|
+
Base class for a LabelStudio Tag
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
-----------
|
|
11
|
+
attr: Optional[Dict]
|
|
12
|
+
A dictionary of attributes for the tag
|
|
13
|
+
tag: Optional[str]
|
|
14
|
+
The tag name
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
attr: Optional[Dict]
|
|
18
|
+
tag: Optional[str]
|
|
19
|
+
|
|
20
|
+
def match(
|
|
21
|
+
self,
|
|
22
|
+
tag_type: str,
|
|
23
|
+
name: Optional[str] = None,
|
|
24
|
+
name_filter_fn: Optional[Callable] = None,
|
|
25
|
+
to_name: Optional[str] = None,
|
|
26
|
+
to_name_filter_fn: Optional[Callable] = None,
|
|
27
|
+
) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
This method checks if the current instance of LabelStudioTag matches the provided parameters.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
-----------
|
|
33
|
+
tag_type : str
|
|
34
|
+
The type of the tag to match. It can be a string or a tuple of strings.
|
|
35
|
+
name : Optional[str]
|
|
36
|
+
The name of the tag to match. If provided, it should match the name of the current instance.
|
|
37
|
+
name_filter_fn : Optional[Callable]
|
|
38
|
+
A function to filter the name of the tag. If provided, the function should return True for a match.
|
|
39
|
+
to_name : Optional[str]
|
|
40
|
+
The 'to_name' attribute of the tag to match. If provided, it should match the 'to_name' of the current instance.
|
|
41
|
+
to_name_filter_fn : Optional[Callable]
|
|
42
|
+
A function to filter the 'to_name' of the tag. If provided, the function should return True for a match.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
--------
|
|
46
|
+
bool
|
|
47
|
+
True if the current instance matches the provided parameters, False otherwise.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(tag_type, str):
|
|
50
|
+
tag_type = tag_type.lower()
|
|
51
|
+
elif isinstance(tag_type, tuple):
|
|
52
|
+
tag_type = tuple(t.lower() for t in tag_type)
|
|
53
|
+
|
|
54
|
+
if name:
|
|
55
|
+
name = name.lower()
|
|
56
|
+
|
|
57
|
+
if to_name:
|
|
58
|
+
to_name = to_name.lower()
|
|
59
|
+
|
|
60
|
+
if (isinstance(tag_type, str) and self.tag.lower() != tag_type) or (
|
|
61
|
+
isinstance(tag_type, tuple) and self.tag.lower() not in tag_type
|
|
62
|
+
):
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
if name and self.name.lower() != name:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
if name_filter_fn and not name_filter_fn(self.name.lower()):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
if to_name and self.to_name.lower() != to_name:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if to_name_filter_fn and not to_name_filter_fn(self.name.lower()):
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
return True
|