label-studio-sdk 0.0.32__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.

Files changed (49) hide show
  1. label-studio-sdk-0.0.34/MANIFEST.in +2 -0
  2. {label-studio-sdk-0.0.32/label_studio_sdk.egg-info → label-studio-sdk-0.0.34}/PKG-INFO +1 -1
  3. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/README.md +2 -13
  4. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk/__init__.py +4 -1
  5. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk/client.py +94 -78
  6. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk/data_manager.py +32 -23
  7. label-studio-sdk-0.0.34/label_studio_sdk/exceptions.py +10 -0
  8. label-studio-sdk-0.0.34/label_studio_sdk/label_interface/__init__.py +1 -0
  9. label-studio-sdk-0.0.34/label_studio_sdk/label_interface/base.py +77 -0
  10. label-studio-sdk-0.0.34/label_studio_sdk/label_interface/control_tags.py +756 -0
  11. label-studio-sdk-0.0.34/label_studio_sdk/label_interface/interface.py +922 -0
  12. label-studio-sdk-0.0.34/label_studio_sdk/label_interface/label_tags.py +72 -0
  13. label-studio-sdk-0.0.34/label_studio_sdk/label_interface/object_tags.py +292 -0
  14. label-studio-sdk-0.0.34/label_studio_sdk/label_interface/region.py +43 -0
  15. label-studio-sdk-0.0.34/label_studio_sdk/objects.py +35 -0
  16. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk/project.py +711 -258
  17. label-studio-sdk-0.0.34/label_studio_sdk/schema/label_config_schema.json +226 -0
  18. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk/users.py +15 -13
  19. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk/utils.py +31 -30
  20. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk/workspaces.py +13 -11
  21. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34/label_studio_sdk.egg-info}/PKG-INFO +1 -1
  22. label-studio-sdk-0.0.34/label_studio_sdk.egg-info/SOURCES.txt +42 -0
  23. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk.egg-info/requires.txt +2 -0
  24. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk.egg-info/top_level.txt +0 -1
  25. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/requirements.txt +2 -0
  26. label-studio-sdk-0.0.34/setup.py +35 -0
  27. label-studio-sdk-0.0.34/tests/test_client.py +37 -0
  28. label-studio-sdk-0.0.34/tests/test_export.py +105 -0
  29. label-studio-sdk-0.0.34/tests/test_interface/__init__.py +1 -0
  30. label-studio-sdk-0.0.34/tests/test_interface/configs.py +137 -0
  31. label-studio-sdk-0.0.34/tests/test_interface/mockups.py +22 -0
  32. label-studio-sdk-0.0.34/tests/test_interface/test_compat.py +64 -0
  33. label-studio-sdk-0.0.34/tests/test_interface/test_control_tags.py +55 -0
  34. label-studio-sdk-0.0.34/tests/test_interface/test_data_generation.py +45 -0
  35. label-studio-sdk-0.0.34/tests/test_interface/test_lpi.py +15 -0
  36. label-studio-sdk-0.0.34/tests/test_interface/test_main.py +196 -0
  37. label-studio-sdk-0.0.34/tests/test_interface/test_object_tags.py +36 -0
  38. label-studio-sdk-0.0.34/tests/test_interface/test_region.py +36 -0
  39. label-studio-sdk-0.0.34/tests/test_interface/test_validate_summary.py +35 -0
  40. label-studio-sdk-0.0.34/tests/test_interface/test_validation.py +59 -0
  41. label-studio-sdk-0.0.32/MANIFEST.in +0 -1
  42. label-studio-sdk-0.0.32/docs/__init__.py +0 -3
  43. label-studio-sdk-0.0.32/label_studio_sdk.egg-info/SOURCES.txt +0 -20
  44. label-studio-sdk-0.0.32/setup.py +0 -33
  45. label-studio-sdk-0.0.32/tests/test_client.py +0 -26
  46. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/LICENSE +0 -0
  47. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/label_studio_sdk.egg-info/dependency_links.txt +0 -0
  48. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/setup.cfg +0 -0
  49. {label-studio-sdk-0.0.32 → label-studio-sdk-0.0.34}/tests/__init__.py +0 -0
@@ -0,0 +1,2 @@
1
+ include requirements.txt
2
+ include label_studio_sdk/schema/*.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: label-studio-sdk
3
- Version: 0.0.32
3
+ Version: 0.0.34
4
4
  Summary: Label Studio annotation tool
5
5
  Home-page: https://github.com/heartexlabs/label-studio-sdk
6
6
  Author: Heartex
@@ -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](https://github.com/heartexlabs/label-studio-sdk/CONTRIBUTING.md)
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
- These end-to-end examples demonstrate how to use the SDK for specific use cases.
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,8 +1,11 @@
1
1
  """ .. include::../docs/index.md
2
2
  """
3
+
3
4
  from .client import Client
4
5
  from .project import Project
5
6
  from .utils import parse_config
6
7
 
8
+ __pdoc__ = {"label_interface": False}
9
+
7
10
 
8
- __version__ = '0.0.32'
11
+ __version__ = '0.0.34'
@@ -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.0)
17
+ TIMEOUT = (10.0, int(os.environ.get('TIMEOUT', 180)))
19
18
  HEADERS = {}
20
- LABEL_STUDIO_DEFAULT_URL = 'http://localhost:8080'
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
- 'email' in values or 'api_key' in values
32
- ), 'At least one of email or api_key should be included'
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
- 'email' not in values or 'password' in values
35
- ), 'Provide both email and password for login auth'
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('LABEL_STUDIO_URL', LABEL_STUDIO_DEFAULT_URL)
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('LABEL_STUDIO_API_KEY')
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 = {'Authorization': f'Token {self.api_key}'}
104
+ self.headers = {"Authorization": f"Token {self.api_key}"}
96
105
  if oidc_token:
97
- self.headers.update({'Proxy-Authorization': f'Bearer {oidc_token}'})
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 = 'label-studio-enterprise-backend' in self.versions
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('GET', '/api/version').json()
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('csrftoken', None)
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('GET', '/health')
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('DELETE', f'/api/projects/{project_id}/')
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()['id'] for project in self.list_projects()]
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 = {'page_size': 10000000}
201
+ params = {"page_size": 10000000}
196
202
  params.update(query_params)
197
- response = self.make_request('GET', '/api/projects', params=params)
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()['results']:
206
+ for data in response.json()["results"]:
201
207
  project = Project._create_from_id(
202
- client=self, project_id=data['id'], params=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('GET', '/api/users')
273
+ response = self.make_request("GET", "/api/users")
268
274
  users = []
269
275
  for user_data in response.json():
270
- user_data['client'] = self
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
- 'username': user.username if user.username else user.email,
296
- 'email': user.email,
297
- 'first_name': user.first_name,
298
- 'last_name': user.last_name,
299
- 'phone': user.phone,
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
- 'POST', '/api/users', json=payload, raise_exceptions=False
312
+ "POST", "/api/users", json=payload, raise_exceptions=False
307
313
  )
308
314
  user_data = response.json()
309
- user_data['client'] = self
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 'already exists' in response.text and exist_ok is True:
320
+ if "already exists" in response.text and exist_ok is True:
315
321
  return None
316
- logger.error('Create user error: ' + str(response.json()))
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('GET', '/api/workspaces')
342
+ response = self.make_request("GET", "/api/workspaces")
337
343
  workspaces = []
338
344
  for workspace_data in response.json():
339
- workspace_data['client'] = self
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('http://', HTTPAdapter(max_retries=MAX_RETRIES))
354
- session.mount('https://', HTTPAdapter(max_retries=MAX_RETRIES))
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 'timeout' not in kwargs:
388
- kwargs['timeout'] = TIMEOUT
428
+ if "timeout" not in kwargs:
429
+ kwargs["timeout"] = TIMEOUT
389
430
 
390
431
  raise_exceptions = self.make_request_raise
391
- if 'raise_exceptions' in kwargs: # kwargs have higher priority
392
- raise_exceptions = kwargs.pop('raise_exceptions')
432
+ if "raise_exceptions" in kwargs: # kwargs have higher priority
433
+ raise_exceptions = kwargs.pop("raise_exceptions")
393
434
 
394
- logger.debug(f'{method}: {url} with args={args}, kwargs={kwargs}')
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),
@@ -415,40 +456,15 @@ class Client(object):
415
456
  content = response.text
416
457
 
417
458
  logger.error(
418
- f'\n--------------------------------------------\n'
419
- f'Request URL: {response.url}\n'
420
- f'Response status code: {response.status_code}\n'
421
- f'Response content:\n{content}\n\n'
422
- f'SDK error traceback:'
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:"
423
464
  )
424
465
 
425
466
  def sync_storage(self, storage_type, storage_id):
426
- """Synchronize Cloud Storage.
427
-
428
- Parameters
429
- ----------
430
- storage_type: string
431
- Specify the type of the storage container.
432
- storage_id: int
433
- Specify the storage ID of the storage container.
434
-
435
- Returns
436
- -------
437
- dict:
438
- containing the same fields as in the original storage request and:
439
-
440
- id: int
441
- Storage ID
442
- type: str
443
- Type of storage
444
- created_at: str
445
- Creation time
446
- last_sync: str
447
- Time last sync finished, can be empty.
448
- last_sync_count: int
449
- Number of tasks synced in the last sync
450
- """
451
-
467
+ """See project.sync_storage for more info"""
452
468
  response = self.make_request(
453
469
  "POST", f"/api/storages/{storage_type}/{str(storage_id)}/sync"
454
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 = '%Y-%m-%dT%H:%M:%S.%fZ'
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 = 'or'
45
+ OR = "or"
45
46
  """Combine filters with an OR"""
46
- AND = '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": 'filter:' + name,
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), 'dt must be datetime type'
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 {'min': value, 'max': maximum}
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 = 'Number'
164
- Datetime = 'Datetime'
165
- Boolean = 'Boolean'
166
- String = 'String'
164
+ Number = "Number"
165
+ Datetime = "Datetime"
166
+ Boolean = "Boolean"
167
+ String = "String"
167
168
  List = "List"
168
169
 
169
- Unknown = '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
- 'conjunction': 'or',
250
- 'items': [
258
+ "conjunction": "or",
259
+ "items": [
251
260
  {
252
- 'filter': 'filter:tasks:id',
253
- 'operator': 'greater',
254
- 'type': 'Number',
255
- 'value': 42,
261
+ "filter": "filter:tasks:id",
262
+ "operator": "greater",
263
+ "type": "Number",
264
+ "value": 42,
256
265
  },
257
266
  {
258
- 'filter': 'filter:tasks:completed_at',
259
- 'operator': 'in',
260
- 'type': 'Datetime',
261
- 'value': {
262
- 'min': '2021-11-01T00:00:00.000000Z',
263
- 'max': '2021-11-05T00:00:00.000000Z',
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,10 @@
1
+ class LSConfigParseException(Exception):
2
+ """ """
3
+
4
+
5
+ class LabelStudioXMLSyntaxErrorSentryIgnored(Exception):
6
+ """ """
7
+
8
+
9
+ class LabelStudioValidationErrorSentryIgnored(Exception):
10
+ """ """
@@ -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