label-studio-sdk 1.0.5__py3-none-any.whl → 1.0.8__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 (77) hide show
  1. label_studio_sdk/__init__.py +76 -0
  2. label_studio_sdk/_extensions/eval/categorical.py +83 -0
  3. label_studio_sdk/_extensions/label_studio_tools/core/label_config.py +13 -4
  4. label_studio_sdk/_extensions/label_studio_tools/core/utils/io.py +35 -17
  5. label_studio_sdk/_extensions/label_studio_tools/core/utils/json_schema.py +86 -0
  6. label_studio_sdk/_legacy/schema/label_config_schema.json +42 -11
  7. label_studio_sdk/annotations/__init__.py +3 -0
  8. label_studio_sdk/annotations/client.py +109 -0
  9. label_studio_sdk/annotations/types/__init__.py +5 -0
  10. label_studio_sdk/annotations/types/annotations_create_bulk_response_item.py +29 -0
  11. label_studio_sdk/base_client.py +9 -0
  12. label_studio_sdk/comments/__init__.py +2 -0
  13. label_studio_sdk/comments/client.py +512 -0
  14. label_studio_sdk/converter/converter.py +11 -4
  15. label_studio_sdk/converter/imports/coco.py +14 -13
  16. label_studio_sdk/converter/utils.py +72 -3
  17. label_studio_sdk/core/client_wrapper.py +1 -1
  18. label_studio_sdk/files/client.py +26 -16
  19. label_studio_sdk/label_interface/control_tags.py +205 -10
  20. label_studio_sdk/label_interface/interface.py +117 -10
  21. label_studio_sdk/label_interface/region.py +1 -10
  22. label_studio_sdk/model_providers/__init__.py +2 -0
  23. label_studio_sdk/model_providers/client.py +708 -0
  24. label_studio_sdk/projects/client.py +32 -16
  25. label_studio_sdk/projects/exports/client.py +133 -40
  26. label_studio_sdk/prompts/__init__.py +21 -0
  27. label_studio_sdk/prompts/client.py +862 -0
  28. label_studio_sdk/prompts/indicators/__init__.py +2 -0
  29. label_studio_sdk/prompts/indicators/client.py +194 -0
  30. label_studio_sdk/prompts/runs/__init__.py +5 -0
  31. label_studio_sdk/prompts/runs/client.py +354 -0
  32. label_studio_sdk/prompts/runs/types/__init__.py +5 -0
  33. label_studio_sdk/prompts/runs/types/runs_list_request_project_subset.py +5 -0
  34. label_studio_sdk/prompts/types/__init__.py +15 -0
  35. label_studio_sdk/prompts/types/prompts_batch_failed_predictions_request_failed_predictions_item.py +42 -0
  36. label_studio_sdk/prompts/types/prompts_batch_failed_predictions_response.py +29 -0
  37. label_studio_sdk/prompts/types/prompts_batch_predictions_request_results_item.py +62 -0
  38. label_studio_sdk/prompts/types/prompts_batch_predictions_response.py +29 -0
  39. label_studio_sdk/prompts/versions/__init__.py +2 -0
  40. label_studio_sdk/prompts/versions/client.py +1046 -0
  41. label_studio_sdk/types/__init__.py +58 -0
  42. label_studio_sdk/types/comment.py +39 -0
  43. label_studio_sdk/types/comment_created_by.py +5 -0
  44. label_studio_sdk/types/inference_run.py +43 -0
  45. label_studio_sdk/types/inference_run_cost_estimate.py +57 -0
  46. label_studio_sdk/types/inference_run_created_by.py +5 -0
  47. label_studio_sdk/types/inference_run_organization.py +5 -0
  48. label_studio_sdk/types/inference_run_project_subset.py +5 -0
  49. label_studio_sdk/types/inference_run_status.py +7 -0
  50. label_studio_sdk/types/key_indicator_value.py +30 -0
  51. label_studio_sdk/types/key_indicators.py +7 -0
  52. label_studio_sdk/types/key_indicators_item.py +51 -0
  53. label_studio_sdk/types/key_indicators_item_additional_kpis_item.py +37 -0
  54. label_studio_sdk/types/key_indicators_item_extra_kpis_item.py +37 -0
  55. label_studio_sdk/types/model_provider_connection.py +71 -0
  56. label_studio_sdk/types/model_provider_connection_budget_reset_period.py +5 -0
  57. label_studio_sdk/types/model_provider_connection_created_by.py +5 -0
  58. label_studio_sdk/types/model_provider_connection_organization.py +5 -0
  59. label_studio_sdk/types/model_provider_connection_provider.py +5 -0
  60. label_studio_sdk/types/model_provider_connection_scope.py +5 -0
  61. label_studio_sdk/types/prompt.py +79 -0
  62. label_studio_sdk/types/prompt_created_by.py +5 -0
  63. label_studio_sdk/types/prompt_organization.py +5 -0
  64. label_studio_sdk/types/prompt_version.py +41 -0
  65. label_studio_sdk/types/prompt_version_created_by.py +5 -0
  66. label_studio_sdk/types/prompt_version_organization.py +5 -0
  67. label_studio_sdk/types/prompt_version_provider.py +5 -0
  68. label_studio_sdk/types/refined_prompt_response.py +64 -0
  69. label_studio_sdk/types/refined_prompt_response_refinement_status.py +7 -0
  70. label_studio_sdk/types/task.py +3 -2
  71. label_studio_sdk/types/task_comment_authors_item.py +5 -0
  72. label_studio_sdk/webhooks/client.py +245 -36
  73. label_studio_sdk/workspaces/client.py +20 -20
  74. label_studio_sdk-1.0.8.dist-info/LICENSE +201 -0
  75. {label_studio_sdk-1.0.5.dist-info → label_studio_sdk-1.0.8.dist-info}/METADATA +19 -3
  76. {label_studio_sdk-1.0.5.dist-info → label_studio_sdk-1.0.8.dist-info}/RECORD +77 -24
  77. {label_studio_sdk-1.0.5.dist-info → label_studio_sdk-1.0.8.dist-info}/WHEEL +1 -1
@@ -17,10 +17,11 @@ from urllib.parse import urlparse
17
17
  import numpy as np
18
18
  import requests
19
19
  from PIL import Image
20
- from label_studio_sdk._extensions.label_studio_tools.core.utils.params import get_env
21
20
  from lxml import etree
22
21
  from nltk.tokenize.treebank import TreebankWordTokenizer
23
22
 
23
+ from label_studio_sdk._extensions.label_studio_tools.core.utils.params import get_env
24
+
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
27
  _LABEL_TAGS = {"Label", "Choice"}
@@ -422,7 +423,7 @@ def convert_annotation_to_yolo(label):
422
423
  return x, y, w, h
423
424
 
424
425
 
425
- def convert_annotation_to_yolo_obb(label):
426
+ def convert_annotation_to_yolo_obb(label, normalize=True):
426
427
  """
427
428
  Convert LS annotation to Yolo OBB format.
428
429
 
@@ -435,6 +436,7 @@ def convert_annotation_to_yolo_obb(label):
435
436
  - width (float): Width of the object in percentage of the original width.
436
437
  - height (float): Height of the object in percentage of the original height.
437
438
  - rotation (float, optional): Rotation angle of the object in degrees (default is 0).
439
+ normalize (bool, optional): Whether to normalize the coordinates to the range [0, 1] (default is True).
438
440
 
439
441
  Returns:
440
442
  list of tuple or None: List of tuples containing the coordinates of the object in Yolo OBB format.
@@ -470,4 +472,71 @@ def convert_annotation_to_yolo_obb(label):
470
472
  ]
471
473
 
472
474
  # Normalize coordinates
473
- return [(coord[0] / org_width, coord[1] / org_height) for coord in coords]
475
+ if normalize:
476
+ return [(coord[0] / org_width, coord[1] / org_height) for coord in coords]
477
+ else:
478
+ return coords
479
+
480
+
481
+ def convert_yolo_obb_to_annotation(xyxyxyxy, original_width, original_height):
482
+ """
483
+ Convert YOLO Oriented Bounding Box (OBB) format to Label Studio format.
484
+
485
+ Args:
486
+ xyxyxyxy (list): List of 8 float values representing the absolute pixel coordinates
487
+ of the OBB in the format [x1, y1, x2, y2, x3, y3, x4, y4].
488
+ original_width (int): Original width of the image.
489
+ original_height (int): Original height of the image.
490
+
491
+ Returns:
492
+ dict: Dictionary containing the converted bounding box with the following keys:
493
+ - x: X-coordinate of the top-left corner of the bounding box in percentage.
494
+ - y: Y-coordinate of the top-left corner of the bounding box in percentage.
495
+ - width: Width of the bounding box in percentage.
496
+ - height: Height of the bounding box in percentage.
497
+ - rotation: Rotation angle of the bounding box in degrees.
498
+ """
499
+ # Reshape the coordinates into a 4x2 matrix
500
+ coords = np.array(xyxyxyxy, dtype=np.float64).reshape((4, 2))
501
+
502
+ # Calculate the center of the bounding box
503
+ center_x = np.mean(coords[:, 0])
504
+ center_y = np.mean(coords[:, 1])
505
+
506
+ # Calculate the width and height of the bounding box
507
+ width = np.linalg.norm(coords[0] - coords[1])
508
+ height = np.linalg.norm(coords[0] - coords[3])
509
+
510
+ # Calculate the rotation angle
511
+ dx = coords[1, 0] - coords[0, 0]
512
+ dy = coords[1, 1] - coords[0, 1]
513
+ r = np.degrees(np.arctan2(dy, dx))
514
+
515
+ # Find the top-left corner (x, y)
516
+ top_left_x = (
517
+ center_x
518
+ - (width / 2) * np.cos(np.radians(r))
519
+ + (height / 2) * np.sin(np.radians(r))
520
+ )
521
+ top_left_y = (
522
+ center_y
523
+ - (width / 2) * np.sin(np.radians(r))
524
+ - (height / 2) * np.cos(np.radians(r))
525
+ )
526
+
527
+ # Normalize the values
528
+ x = (top_left_x / original_width) * 100
529
+ y = (top_left_y / original_height) * 100
530
+ width = (width / original_width) * 100
531
+ height = (height / original_height) * 100
532
+
533
+ # Create the dictionary for Label Studio
534
+ return {
535
+ "x": x,
536
+ "y": y,
537
+ "width": width,
538
+ "height": height,
539
+ "rotation": r,
540
+ "original_width": original_width,
541
+ "original_height": original_height,
542
+ }
@@ -17,7 +17,7 @@ class BaseClientWrapper:
17
17
  headers: typing.Dict[str, str] = {
18
18
  "X-Fern-Language": "Python",
19
19
  "X-Fern-SDK-Name": "label-studio-sdk",
20
- "X-Fern-SDK-Version": "1.0.4",
20
+ "X-Fern-SDK-Version": "1.0.8",
21
21
  }
22
22
  headers["Authorization"] = f"Token {self.api_key}"
23
23
  return headers
@@ -96,7 +96,12 @@ class FilesClient:
96
96
  raise ApiError(status_code=_response.status_code, body=_response_json)
97
97
 
98
98
  def update(
99
- self, id: int, *, request: FileUpload, request_options: typing.Optional[RequestOptions] = None
99
+ self,
100
+ id_: int,
101
+ *,
102
+ id: typing.Optional[int] = OMIT,
103
+ file: typing.Optional[str] = OMIT,
104
+ request_options: typing.Optional[RequestOptions] = None,
100
105
  ) -> FileUpload:
101
106
  """
102
107
  Update a specific uploaded file. To get the file upload ID, use [Get files list](list).
@@ -109,10 +114,12 @@ class FilesClient:
109
114
 
110
115
  Parameters
111
116
  ----------
112
- id : int
117
+ id_ : int
113
118
  A unique integer value identifying this file upload.
114
119
 
115
- request : FileUpload
120
+ id : typing.Optional[int]
121
+
122
+ file : typing.Optional[str]
116
123
 
117
124
  request_options : typing.Optional[RequestOptions]
118
125
  Request-specific configuration.
@@ -124,21 +131,19 @@ class FilesClient:
124
131
 
125
132
  Examples
126
133
  --------
127
- from label_studio_sdk import FileUpload
128
134
  from label_studio_sdk.client import LabelStudio
129
135
 
130
136
  client = LabelStudio(
131
137
  api_key="YOUR_API_KEY",
132
138
  )
133
139
  client.files.update(
134
- id=1,
135
- request=FileUpload(),
140
+ id_=1,
136
141
  )
137
142
  """
138
143
  _response = self._client_wrapper.httpx_client.request(
139
- f"api/import/file-upload/{jsonable_encoder(id)}",
144
+ f"api/import/file-upload/{jsonable_encoder(id_)}",
140
145
  method="PATCH",
141
- json=request,
146
+ json={"id": id, "file": file},
142
147
  request_options=request_options,
143
148
  omit=OMIT,
144
149
  )
@@ -367,7 +372,12 @@ class AsyncFilesClient:
367
372
  raise ApiError(status_code=_response.status_code, body=_response_json)
368
373
 
369
374
  async def update(
370
- self, id: int, *, request: FileUpload, request_options: typing.Optional[RequestOptions] = None
375
+ self,
376
+ id_: int,
377
+ *,
378
+ id: typing.Optional[int] = OMIT,
379
+ file: typing.Optional[str] = OMIT,
380
+ request_options: typing.Optional[RequestOptions] = None,
371
381
  ) -> FileUpload:
372
382
  """
373
383
  Update a specific uploaded file. To get the file upload ID, use [Get files list](list).
@@ -380,10 +390,12 @@ class AsyncFilesClient:
380
390
 
381
391
  Parameters
382
392
  ----------
383
- id : int
393
+ id_ : int
384
394
  A unique integer value identifying this file upload.
385
395
 
386
- request : FileUpload
396
+ id : typing.Optional[int]
397
+
398
+ file : typing.Optional[str]
387
399
 
388
400
  request_options : typing.Optional[RequestOptions]
389
401
  Request-specific configuration.
@@ -395,21 +407,19 @@ class AsyncFilesClient:
395
407
 
396
408
  Examples
397
409
  --------
398
- from label_studio_sdk import FileUpload
399
410
  from label_studio_sdk.client import AsyncLabelStudio
400
411
 
401
412
  client = AsyncLabelStudio(
402
413
  api_key="YOUR_API_KEY",
403
414
  )
404
415
  await client.files.update(
405
- id=1,
406
- request=FileUpload(),
416
+ id_=1,
407
417
  )
408
418
  """
409
419
  _response = await self._client_wrapper.httpx_client.request(
410
- f"api/import/file-upload/{jsonable_encoder(id)}",
420
+ f"api/import/file-upload/{jsonable_encoder(id_)}",
411
421
  method="PATCH",
412
- json=request,
422
+ json={"id": id, "file": file},
413
423
  request_options=request_options,
414
424
  omit=OMIT,
415
425
  )
@@ -93,6 +93,15 @@ class ControlTag(LabelStudioTag):
93
93
  and tag.tag not in _NOT_CONTROL_TAGS
94
94
  )
95
95
 
96
+ def to_json_schema(self):
97
+ """
98
+ Converts the current ControlTag instance into a JSON Schema.
99
+
100
+ Returns:
101
+ dict: A dictionary representing the JSON Schema.
102
+ """
103
+ return {"type": "string"}
104
+
96
105
  @classmethod
97
106
  def parse_node(cls, tag: xml.etree.ElementTree.Element, tags_mapping=None) -> "ControlTag":
98
107
  """
@@ -261,11 +270,11 @@ class ControlTag(LabelStudioTag):
261
270
 
262
271
  def _validate_labels(self, labels):
263
272
  """Check that labels is a subset of self.labels, used for
264
- example when you're validate the annotaion or prediction to
273
+ example when you're validate the annotation or prediction to
265
274
  make sure there no undefined labels used.
266
275
 
267
276
  """
268
- if not self.labels:
277
+ if not self.labels or not labels:
269
278
  return True
270
279
 
271
280
  return set(labels).issubset(set(self.labels))
@@ -335,10 +344,11 @@ class ControlTag(LabelStudioTag):
335
344
 
336
345
  return to_name
337
346
  else:
338
- if len(self.to_name) > 1:
339
- raise Exception(
340
- "Multiple to_name in control tag, specify to_name in function"
341
- )
347
+ # TODO: "Pairwise" tag has multiple to_name
348
+ # if len(self.to_name) > 1:
349
+ # raise Exception(
350
+ # "Multiple to_name in control tag, specify to_name in function"
351
+ # )
342
352
 
343
353
  return self.to_name[0]
344
354
 
@@ -415,7 +425,7 @@ class ControlTag(LabelStudioTag):
415
425
  )
416
426
 
417
427
  kwargs[self._label_attr_name] = label
418
-
428
+
419
429
  return self._label_simple(to_name=to_name, **kwargs)
420
430
 
421
431
  def label(
@@ -446,12 +456,29 @@ class ControlTag(LabelStudioTag):
446
456
  Region
447
457
  A new Region object with the specified label applied.
448
458
  """
449
- if hasattr(self, "_label_attr_name"):
459
+ if hasattr(self, "_label_attr_name") and label is not None:
450
460
  return self._label_with_labels(
451
461
  label=label, to_name=to_name, *args, **kwargs
452
462
  )
453
463
  else:
454
464
  return self._label_simple(to_name=to_name, *args, **kwargs)
465
+
466
+ def get_labels(self, regions: List[Dict]):
467
+ """
468
+ Returns the simplified representation of the label. Sort of a reverse to label() method to retrieve an input `label` from an output regions
469
+ """
470
+ values = [region.get('value') for region in regions if region.get('from_name') == self.name]
471
+ values = list(filter(lambda x: x is not None, values))
472
+ if not hasattr(self, "_label_attr_name"):
473
+ return values
474
+ labels = []
475
+ for value in values:
476
+ if len(value) == 1 and self._label_attr_name in value:
477
+ v = value[self._label_attr_name]
478
+ labels.append(v[0] if len(v) == 1 else v)
479
+ else:
480
+ labels.append(value)
481
+ return labels[0] if len(labels) == 1 else labels
455
482
 
456
483
  def as_tuple(self):
457
484
  """ """
@@ -485,6 +512,34 @@ class ChoicesTag(ControlTag):
485
512
  _label_attr_name: str = "choices"
486
513
  _value_class: Type[ChoicesValue] = ChoicesValue
487
514
 
515
+ @property
516
+ def is_multiple_choice(self):
517
+ return self.attr.get("choice") == "multiple"
518
+
519
+ def to_json_schema(self):
520
+ """
521
+ Converts the current ChoicesTag instance into a JSON Schema.
522
+
523
+ Returns:
524
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
525
+ """
526
+ if self.is_multiple_choice:
527
+ return {
528
+ "type": "array",
529
+ "items": {
530
+ "type": "string",
531
+ "enum": self.labels,
532
+ },
533
+ "uniqueItems": True,
534
+ "description": f"Choices for {self.to_name[0]}",
535
+ }
536
+
537
+ return {
538
+ "type": "string",
539
+ "enum": self.labels,
540
+ "description": f"Choices for {self.to_name[0]}"
541
+ }
542
+
488
543
 
489
544
  class LabelsValue(SpanSelection):
490
545
  labels: List[str]
@@ -496,6 +551,41 @@ class LabelsTag(ControlTag):
496
551
  _label_attr_name: str = "labels"
497
552
  _value_class: Type[LabelsValue] = LabelsValue
498
553
 
554
+ def to_json_schema(self):
555
+ """
556
+ Converts the current LabelsTag instance into a JSON Schema.
557
+
558
+ Returns:
559
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
560
+ """
561
+ return {
562
+ "type": "array",
563
+ "items": {
564
+ "type": "object",
565
+ "required": ["start", "end", "labels"],
566
+ "properties": {
567
+ "start": {
568
+ "type": "integer",
569
+ "minimum": 0
570
+ },
571
+ "end": {
572
+ "type": "integer",
573
+ "minimum": 0
574
+ },
575
+ "labels": {
576
+ "type": "array",
577
+ "items": {
578
+ "type": "string",
579
+ "enum": self.labels
580
+ }
581
+ },
582
+ "text": {
583
+ "type": "string"
584
+ }
585
+ }
586
+ },
587
+ "description": f"Labels and span indices for {self.to_name[0]}"
588
+ }
499
589
 
500
590
  ## Image tags
501
591
 
@@ -684,6 +774,26 @@ class NumberTag(ControlTag):
684
774
  """ """
685
775
  tag: str = "Number"
686
776
  _value_class: Type[NumberValue] = NumberValue
777
+ _label_attr_name: str = "number"
778
+
779
+ def to_json_schema(self):
780
+ """
781
+ Converts the current NumberTag instance into a JSON Schema.
782
+
783
+ Returns:
784
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
785
+ """
786
+ schema = {
787
+ "type": "number",
788
+ "description": f"Number for {self.to_name[0]}"
789
+ }
790
+
791
+ if 'min' in self.attr:
792
+ schema["minimum"] = float(self.attr['min'])
793
+ if 'max' in self.attr:
794
+ schema["maximum"] = float(self.attr['max'])
795
+
796
+ return schema
687
797
 
688
798
 
689
799
  class DateTimeValue(BaseModel):
@@ -694,6 +804,25 @@ class DateTimeTag(ControlTag):
694
804
  """ """
695
805
  tag: str = "DateTime"
696
806
  _value_class: Type[DateTimeValue] = DateTimeValue
807
+ _label_attr_name: str = "datetime"
808
+
809
+ def _label_simple(self, to_name: Optional[str] = None, *args, **kwargs) -> Region:
810
+ # TODO: temporary fix to force datetime to be a string
811
+ kwargs['datetime'] = kwargs['datetime'][0]
812
+ return super()._label_simple(to_name, *args, **kwargs)
813
+
814
+ def to_json_schema(self):
815
+ """
816
+ Converts the current DateTimeTag instance into a JSON Schema.
817
+
818
+ Returns:
819
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
820
+ """
821
+ return {
822
+ "type": "string",
823
+ "format": "date-time",
824
+ "description": f"Date and time for {self.to_name[0]}"
825
+ }
697
826
 
698
827
 
699
828
  class HyperTextLabelsValue(SpanSelectionOffsets):
@@ -715,12 +844,27 @@ class PairwiseTag(ControlTag):
715
844
  """ """
716
845
  tag: str = "Pairwise"
717
846
  _value_class: Type[PairwiseValue] = PairwiseValue
847
+ _label_attr_name: str = "selected"
718
848
 
719
- def label(self, side):
849
+ def label(self, label):
720
850
  """ """
721
- value = PairwiseValue(selected=side)
851
+ value = PairwiseValue(selected=label)
852
+ # <Pairwise> tag has equal from_name and to_name, and string label that's not a list of strings
722
853
  return Region(from_tag=self, to_tag=self, value=value)
723
854
 
855
+ def to_json_schema(self):
856
+ """
857
+ Converts the current PairwiseTag instance into a JSON Schema.
858
+
859
+ Returns:
860
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
861
+ """
862
+ return {
863
+ "type": "string",
864
+ "enum": ["left", "right"],
865
+ "description": f"Pairwise selection between {self.to_name[0]} (left) and {self.to_name[1]} (right)"
866
+ }
867
+
724
868
 
725
869
  class ParagraphLabelsValue(SpanSelectionOffsets):
726
870
  paragraphlabels: List[str]
@@ -759,6 +903,22 @@ class RatingTag(ControlTag):
759
903
  """ """
760
904
  tag: str = "Rating"
761
905
  _value_class: Type[RatingValue] = RatingValue
906
+ _label_attr_name: str = "rating"
907
+
908
+ def to_json_schema(self):
909
+ """
910
+ Converts the current RatingTag instance into a JSON Schema.
911
+
912
+ Returns:
913
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
914
+ """
915
+ max_rating = int(self.attr.get('maxRating', 5)) # Default to 5 if not specified
916
+ return {
917
+ "type": "integer",
918
+ "minimum": 0,
919
+ "maximum": max_rating,
920
+ "description": f"Rating for {self.to_name[0]} (0 to {max_rating})"
921
+ }
762
922
 
763
923
 
764
924
  class RelationsTag(ControlTag):
@@ -784,6 +944,27 @@ class TaxonomyTag(ControlTag):
784
944
  """ """
785
945
  tag: str = "Taxonomy"
786
946
  _value_class: Type[TaxonomyValue] = TaxonomyValue
947
+ _label_attr_name: str = "taxonomy"
948
+
949
+ def to_json_schema(self):
950
+ """
951
+ Converts the current TaxonomyTag instance into a JSON Schema.
952
+
953
+ Returns:
954
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
955
+ """
956
+ return {
957
+ "type": "array",
958
+ "items": {
959
+ "type": "array",
960
+ "items": {
961
+ "type": "string",
962
+ # TODO: enforce the order of the enums according to the taxonomy tree
963
+ "enum": self.labels
964
+ }
965
+ },
966
+ "description": f"Taxonomy for {self.to_name[0]}. Each item is a path from root to selected node."
967
+ }
787
968
 
788
969
 
789
970
  class TextAreaValue(BaseModel):
@@ -794,6 +975,20 @@ class TextAreaTag(ControlTag):
794
975
  """ """
795
976
  tag: str = "TextArea"
796
977
  _value_class: Type[TextAreaValue] = TextAreaValue
978
+ _label_attr_name: str = "text"
979
+
980
+ def to_json_schema(self):
981
+ """
982
+ Converts the current TextAreaTag instance into a JSON Schema.
983
+
984
+ Returns:
985
+ dict: A dictionary representing the JSON Schema compatible with OpenAPI 3.
986
+ """
987
+ return {
988
+ "type": "string",
989
+ "description": f"Text for {self.to_name[0]}"
990
+ }
991
+
797
992
 
798
993
 
799
994
  class TimeSeriesValue(SpanSelection):