"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
"""
|
from enum import Enum
|
from typing import Any, List, Optional, Union
|
|
from pydantic import BaseModel, StrictBool, StrictFloat, StrictInt, StrictStr
|
|
|
class FilterIn(BaseModel):
|
min: Union[StrictInt, StrictFloat, StrictStr]
|
max: Union[StrictInt, StrictFloat, StrictStr]
|
|
|
class Filter(BaseModel):
|
child_filter: Optional['Filter'] = None
|
|
filter: str
|
operator: str
|
type: str
|
value: Union[StrictInt, StrictFloat, StrictBool, StrictStr, FilterIn, list]
|
|
|
class ConjunctionEnum(Enum):
|
OR = 'or'
|
AND = 'and'
|
|
|
class Filters(BaseModel):
|
conjunction: ConjunctionEnum
|
items: List[Filter]
|
|
|
class SelectedItems(BaseModel):
|
all: bool
|
included: List[int] = []
|
excluded: List[int] = []
|
|
|
class PrepareParams(BaseModel):
|
project: Union[int, List[int]] # Support both single project and multiple projects
|
ordering: List[str] = []
|
selectedItems: Optional[SelectedItems] = None
|
filters: Optional[Filters] = None
|
data: Optional[dict] = None
|
request: Optional[Any] = None
|
|
@property
|
def projects(self) -> List[int]:
|
"""Get project IDs as a list, whether single or multiple were provided."""
|
if isinstance(self.project, list):
|
return self.project
|
return [self.project]
|
|
@property
|
def is_multi_project(self) -> bool:
|
"""Check if this PrepareParams includes multiple projects."""
|
return isinstance(self.project, list) and len(self.project) > 1
|
|
|
class CustomEnum(Enum):
|
def __init__(self, value, description):
|
self._value_ = value
|
self.description = description
|
|
@classmethod
|
def enums(cls):
|
return sorted([item.value for item in cls])
|
|
@classmethod
|
def descriptions(cls):
|
return {item.value: item.description for item in sorted(cls, key=lambda x: x.value)}
|
|
|
class Column(Enum):
|
ID = 'id', 'Number', 'Task ID'
|
INNER_ID = 'inner_id', 'Number', 'Task Inner ID, it starts from 1 for all projects'
|
GROUND_TRUTH = 'ground_truth', 'Boolean', 'Ground truth status of the tasks'
|
ANNOTATIONS_RESULTS = 'annotations_results', 'String', 'Annotation results for the tasks'
|
REVIEWED = 'reviewed', 'Boolean', 'Whether the tasks have been reviewed (Enterprise only)'
|
PREDICTIONS_SCORE = 'predictions_score', 'Number', 'Prediction score for the task'
|
PREDICTIONS_MODEL_VERSIONS = 'predictions_model_versions', 'String', 'Model version used for the predictions'
|
PREDICTIONS_RESULTS = 'predictions_results', 'String', 'Prediction results for the tasks'
|
FILE_UPLOAD = 'file_upload', 'String', 'Name of the file uploaded to create the tasks'
|
CREATED_AT = 'created_at', 'Datetime', 'Time the task was created at'
|
UPDATED_AT = (
|
'updated_at',
|
'Datetime',
|
'Time the task was updated at (e.g. new annotation was created, review added, etc)',
|
)
|
ANNOTATORS = (
|
'annotators',
|
'List',
|
'Annotators that completed the task (Community). Can include assigned annotators (Enterprise only). '
|
'Important note: the filter `type` should be List, but the filter `value` is integer',
|
)
|
TOTAL_PREDICTIONS = 'total_predictions', 'Number', 'Total number of predictions for the task'
|
CANCELLED_ANNOTATIONS = (
|
'cancelled_annotations',
|
'Number',
|
'Number of cancelled or skipped annotations for the task',
|
)
|
TOTAL_ANNOTATIONS = 'total_annotations', 'Number', 'Total number of annotations on a task'
|
COMPLETED_AT = 'completed_at', 'Datetime', 'Time when a task was fully annotated'
|
AGREEMENT = 'agreement', 'Number', 'Agreement for annotation results for a specific task (Enterprise only)'
|
REVIEWERS = (
|
'reviewers',
|
'String',
|
'Reviewers that reviewed the task, or assigned reviewers (Enterprise only). '
|
'Important note: the filter `type` should be List, but the filter `value` is integer',
|
)
|
REVIEWS_REJECTED = (
|
'reviews_rejected',
|
'Number',
|
'Number of annotations rejected for a task in review (Enterprise only)',
|
)
|
REVIEWS_ACCEPTED = (
|
'reviews_accepted',
|
'Number',
|
'Number of annotations accepted for a task in review (Enterprise only)',
|
)
|
COMMENTS = 'comments', 'Number', 'Number of comments in a task'
|
UNRESOLVED_COMMENT_COUNT = 'unresolved_comment_count', 'Number', 'Number of unresolved comments in a task'
|
|
def __init__(self, value, value_type, description):
|
self._value_ = value
|
self.type = value_type
|
self.description = description
|
|
@classmethod
|
def enums_for_filters(cls):
|
return sorted(['filter:tasks:' + str(item.value) for item in cls])
|
|
@classmethod
|
def enums_for_ordering(cls):
|
return sorted([('tasks:' + str(item.value)) for item in cls])
|
|
@classmethod
|
def descriptions_for_filters(cls):
|
return {
|
'filter:tasks:' + item.value: f'({item.type}) ' + item.description
|
for item in sorted(cls, key=lambda x: x.value)
|
}
|
|
|
class Operator(CustomEnum):
|
EQUAL = 'equal', 'Equal to'
|
NOT_EQUAL = 'not_equal', 'Not equal to'
|
GREATER = 'greater', 'Greater than'
|
GREATER_OR_EQUAL = 'greater_or_equal', 'Greater than or equal to'
|
LESS = 'less', 'Less than'
|
LESS_OR_EQUAL = 'less_or_equal', 'Less than or equal to'
|
CONTAINS = 'contains', 'Contains'
|
NOT_CONTAINS = 'not_contains', 'Does not contain'
|
EXISTS = 'exists', 'Exists'
|
NOT_EXISTS = 'not_exists', 'Does not exist'
|
STARTS_WITH = 'starts_with', 'Starts with'
|
ENDS_WITH = 'ends_with', 'Ends with'
|
IS_BETWEEN = 'in', 'Is between min and max values, so the filter `value` should be e.g. `{"min": 1, "max": 7}`'
|
NOT_BETWEEN = (
|
'not_in',
|
'Is not between min and max values, so the filter `value` should be e.g. `{"min": 1, "max": 7}`',
|
)
|
|
|
class Type(CustomEnum):
|
Number = 'Number', 'Float or Integer'
|
Datetime = 'Datetime', "Datetime string in `strftime('%Y-%m-%dT%H:%M:%S.%fZ')` format"
|
Boolean = 'Boolean', 'Boolean'
|
String = 'String', 'String'
|
List = 'List', 'List of items'
|
Unknown = 'Unknown', 'Unknown is explicitly converted to string format'
|
|
|
# Example request and response
|
example_request_1 = {
|
'filters': {
|
'conjunction': 'or',
|
'items': [{'filter': 'filter:tasks:id', 'operator': 'greater', 'type': 'Number', 'value': 123}],
|
},
|
'selectedItems': {'all': True, 'excluded': [124, 125, 126]},
|
'ordering': ['tasks:total_annotations'],
|
}
|
|
example_request_2 = {
|
'filters': {
|
'conjunction': 'or',
|
'items': [
|
{
|
'filter': 'filter:tasks:completed_at',
|
'operator': 'in',
|
'type': 'Datetime',
|
'value': {'min': '2021-01-01T00:00:00.000Z', 'max': '2025-01-01T00:00:00.000Z'},
|
}
|
],
|
},
|
'selectedItems': {'all': False, 'included': [1, 2, 3]},
|
'ordering': ['-tasks:completed_at'],
|
}
|
|
# Define the schemas for filters and selectedItems
|
filters_schema = {
|
'type': 'object',
|
'properties': {
|
'conjunction': {
|
'type': 'string',
|
'enum': ['or', 'and'],
|
'description': (
|
'Logical conjunction for the filters. This conjunction (either "or" or "and") '
|
'will be applied to all items in the filters list. It is not possible to combine '
|
'"or" and "and" within one list of filters. All filters will be either combined with "or" '
|
'or with "and", but not a mix of both.'
|
),
|
},
|
'items': {
|
'type': 'array',
|
'items': {
|
'type': 'object',
|
'properties': {
|
'filter': {
|
'type': 'string',
|
'enum': Column.enums_for_filters(),
|
'description': (
|
'Filter identifier, it should start with `filter:tasks:` prefix, '
|
'e.g. `filter:tasks:agreement`. '
|
'For `task.data` fields it may look like `filter:tasks:data.field_name`. '
|
'If you need more info about columns, check the '
|
'[Get data manager columns](#tag/Data-Manager/operation/api_dm_columns_list) API endpoint. '
|
'Possible values:<br>'
|
+ '<br>'.join(
|
[
|
f'<li>`{key}`<br> {desc}</li>'
|
for key, desc in Column.descriptions_for_filters().items()
|
]
|
)
|
),
|
},
|
'operator': {
|
'type': 'string',
|
'enum': Operator.enums(),
|
'description': (
|
'Filter operator. Possible values:<br>'
|
+ '<br>'.join(
|
[f'<li>`{key}`<br> {desc}</li>' for key, desc in Operator.descriptions().items()]
|
)
|
),
|
},
|
'type': {
|
'type': 'string',
|
'description': 'Type of the filter value. Possible values:<br>'
|
+ '<br>'.join([f'<li>`{key}`<br> {desc}</li>' for key, desc in Type.descriptions().items()]),
|
},
|
'value': {
|
'type': 'object',
|
'oneOf': [
|
{'type': 'string', 'title': 'String', 'description': 'String'},
|
{'type': 'integer', 'title': 'Integer', 'description': 'Integer'},
|
{'type': 'number', 'title': 'Float', 'format': 'float', 'description': 'Float'},
|
{'type': 'boolean', 'title': 'Boolean', 'description': 'Boolean'},
|
{
|
'type': 'object',
|
'title': 'Dictionary',
|
'description': 'Dictionary is used for some operator types, e.g. `in` and `not_in`',
|
},
|
{
|
'type': 'object',
|
'title': 'List',
|
'description': 'List of strings or integers',
|
},
|
],
|
'description': 'Value to filter by',
|
},
|
},
|
'required': ['filter', 'operator', 'type', 'value'],
|
'example': example_request_1['filters']['items'][0],
|
},
|
'description': 'List of filter items',
|
},
|
},
|
'required': ['conjunction', 'items'],
|
'description': (
|
'Filters to apply on tasks. '
|
'You can use [the helper class `Filters` from this page](https://labelstud.io/sdk/data_manager.html) '
|
'to create Data Manager Filters.<br>'
|
'Example: `{"conjunction": "or", "items": [{"filter": "filter:tasks:completed_at", "operator": "greater", '
|
'"type": "Datetime", "value": "2021-01-01T00:00:00.000Z"}]}`'
|
),
|
}
|
|
selected_items_schema = {
|
'type': 'object',
|
'required': ['all'],
|
'description': 'Task selection by IDs. If filters are applied, the selection will be applied to the filtered tasks.'
|
'If "all" is `false`, `"included"` must be used. If "all" is `true`, `"excluded"` must be used.<br>'
|
'Examples: `{"all": false, "included": [1, 2, 3]}` or `{"all": true, "excluded": [4, 5]}`',
|
'oneOf': [
|
{
|
'title': 'all: false',
|
'type': 'object',
|
'properties': {
|
'all': {'type': 'boolean', 'enum': [False], 'description': 'No tasks are selected'},
|
'included': {
|
'type': 'array',
|
'items': {'type': 'integer'},
|
'description': 'List of included task IDs',
|
},
|
},
|
'required': ['all'],
|
},
|
{
|
'title': 'all: true',
|
'type': 'object',
|
'properties': {
|
'all': {'type': 'boolean', 'enum': [True], 'description': 'All tasks are selected'},
|
'excluded': {
|
'type': 'array',
|
'items': {'type': 'integer'},
|
'description': 'List of excluded task IDs',
|
},
|
},
|
'required': ['all'],
|
},
|
],
|
}
|
|
# Define ordering schema
|
ordering_schema = {
|
'type': 'array',
|
'items': {
|
'type': 'string',
|
'enum': Column.enums_for_ordering(),
|
},
|
'description': 'List of fields to order by. Fields are similar to filters but without the `filter:` prefix. '
|
'To reverse the order, add a minus sign before the field name, e.g. `-tasks:created_at`.',
|
}
|
|
# Define the main schema for the data payload
|
data_schema = {
|
'type': 'object',
|
'properties': {'filters': filters_schema, 'selectedItems': selected_items_schema, 'ordering': ordering_schema},
|
'description': 'Additional query to filter and order tasks',
|
}
|
|
prepare_params_schema = {
|
'type': 'object',
|
'properties': {'filters': filters_schema, 'selectedItems': selected_items_schema, 'ordering': ordering_schema},
|
'description': 'Data payload containing task filters, selected task items, and ordering',
|
'example': example_request_1,
|
}
|