Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/source/superannotate.sdk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ ______

----------

Custom Metadata
______

.. automethod:: superannotate.SAClient.create_custom_fields
.. automethod:: superannotate.SAClient.get_custom_fields
.. automethod:: superannotate.SAClient.delete_custom_fields
.. automethod:: superannotate.SAClient.upload_custom_values
.. automethod:: superannotate.SAClient.delete_custom_values

----------

Subsets
______

Expand Down
2 changes: 1 addition & 1 deletion src/superannotate/lib/app/interface/base_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(self, token: str = None, config_path: str = None):
version = os.environ.get("SA_VERSION", "v1")
_token, _config_path = None, None
_host = os.environ.get("SA_URL", constants.BACKEND_URL)
_ssl_verify = os.environ.get("SA_SSL", "True") in ("false", "f", "0")
_ssl_verify = not os.environ.get("SA_SSL", "True").lower() in ("false", "f", "0")
if token:
_token = Controller.validate_token(token=token)
elif config_path:
Expand Down
284 changes: 282 additions & 2 deletions src/superannotate/lib/app/interface/sdk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import json
import os
import tempfile
import warnings
from pathlib import Path
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
Expand Down Expand Up @@ -2572,7 +2572,6 @@ def set_annotation_statuses(
raise AppException(response.errors)
else:
logger.info("Annotation statuses of items changed")
return response.data

def download_annotations(
self,
Expand Down Expand Up @@ -2634,3 +2633,284 @@ def get_subsets(self, project: Union[NotEmptyStr, dict]):
if response.errors:
raise AppException(response.errors)
return BaseSerializer.serialize_iterable(response.data, ["name"])

def create_custom_fields(self, project: NotEmptyStr, fields: dict):
"""Create custom fields for items in a project in addition to built-in metadata.
Using this function again with a different schema won't override the existing fields, but add new ones.
Use the upload_custom_values() function to fill them with values for each item.

:param project: project name (e.g., “project1”)
:type project: str

:param fields: dictionary describing the fields and their specifications added to the project.
You can see the schema structure <here>.
:type fields: dict

:return: custom fields actual schema of the project
:rtype: dict

Supported Types:

============== ======================
number
--------------------------------------
field spec spec value
============== ======================
minimum any number (int or float)
maximum any number (int or float)
enum list of numbers (int or float)
============== ======================

============== ======================
string
--------------------------------------
field spec spec value
============== ======================
format “email” (user@example.com) or “date” (YYYY-MM-DD)

enum list of strings
============== ======================
::

custom_fields = {
"study_date": {
"type": "string",
"format": "date"
},
"patient_id": {
"type": "string"
},
"patient_sex": {
"type": "string",
"enum": [
"male", "female"
]
},
"patient_age": {
"type": "number"
},
"medical_specialist": {
"type": "string",
"format": "email"
},
"duration": {
"type": "number",
"minimum": 10
}
}

client = SAClient()
client.create_custom_fields(
project="Medical Annotations",
fields=custom_fields
)

"""
project_name, _ = extract_project_folder(project)
response = self.controller.create_custom_schema(
project_name=project, schema=fields
)
if response.errors:
raise AppException(response.errors)
return response.data

def get_custom_fields(self, project: NotEmptyStr):
"""Get the schema of the custom fields defined for the project

:param project: project name (e.g., “project1”)
:type project: str

:return: custom fields actual schema of the project
:rtype: dict

Response Example:
::
{
"study_date": {
"type": "string",
"format": "date"
},
"patient_id": {
"type": "string"
},
"patient_sex": {
"type": "string",
"enum": [
"male", "female"
]
},
"patient_age": {
"type": "number"
},
"medical_specialist": {
"type": "string",
"format": "email"
},
"duration": {
"type": "number",
"minimum": 10
}
}
"""
project_name, _ = extract_project_folder(project)
response = self.controller.get_custom_schema(project_name=project)
if response.errors:
raise AppException(response.errors)
return response.data

def delete_custom_fields(
self, project: NotEmptyStr, fields: conlist(NotEmptyStr, min_items=1)
):
"""Remove custom fields from a project’s custom metadata schema.

:param project: project name (e.g., “project1”)
:type project: str

:param fields: list of field names to remove
:type fields: list of strs

:return: custom fields actual schema of the project
:rtype: dict

Request Example:
::
client = SAClient()
client.delete_custom_fields(
project = "Medical Annotations",
fields = ["duration", patient_age]
)

Response Example:
::
{
"study_date": {
"type": "string",
"format": "date"
},
"patient_id": {
"type": "string"
},
"patient_sex": {
"type": "string",
"enum": [
"male", "female"
]
},
"medical_specialist": {
"type": "string",
"format": "email"
}
}

"""
project_name, _ = extract_project_folder(project)
response = self.controller.delete_custom_schema(
project_name=project_name, fields=fields
)
if response.errors:
raise AppException(response.errors)
return response.data

def upload_custom_values(
self, project: NotEmptyStr, items: conlist(Dict[str, dict], min_items=1)
):
"""
Attach custom metadata to items.
SAClient.get_item_metadata(), SAClient.search_items(), SAClient.query() methods
will return the item metadata and custom metadata.

:param project: project name or folder path (e.g., “project1/folder1”)
:type project: str

:param items: list of name-data pairs.
The key of each dict indicates an existing item name and the value represents the custom metadata dict.
The values for the corresponding keys will be added to an item or will be overridden.
:type items: list of dicts

:return: the count of succeeded items and the list of failed item names.
:rtype: dict

Request Example:
::
client = SAClient()

items_values = [
{
"image_1.png": {
"study_date": "2021-12-31",
"patient_id": "62078f8a756ddb2ca9fc9660",
"patient_sex": "female",
"medical_specialist": "robertboxer@ms.com"
}
},
{
"image_2.png": {
"study_date": "2021-12-31",
"patient_id": "62078f8a756ddb2ca9fc9661",
"patient_sex": "female",
"medical_specialist": "robertboxer@ms.com"
}
},
{
"image_3.png": {
"study_date": "2011-10-05T14:48:00.000Z",
"patient_": "62078f8a756ddb2ca9fc9660",
"patient_sex": "female",
"medical_specialist": "robertboxer"
}
}
]

client.upload_custom_values(
project = "Medical Annotations",
items = items_values
)
Response Example:
::
{
"successful_items_count": 2,
"failed_items_names": ["image_3.png"]
}
"""

project_name, folder_name = extract_project_folder(project)
response = self.controller.upload_custom_values(
project_name=project_name, folder_name=folder_name, items=items
)
if response.errors:
raise AppException(response.errors)
return response.data

def delete_custom_values(
self, project: NotEmptyStr, items: conlist(Dict[str, List[str]], min_items=1)
):
"""
Remove custom data from items

:param project: project name or folder path (e.g., “project1/folder1”)
:type project: str

:param items: list of name-custom data dicts.
The key of each dict element indicates an existing item in the project root or folder.
The value should be the list of fields to be removed from the given item.
Please note, that the function removes pointed metadata from a given item.
To delete metadata for all items you should delete it from the custom metadata schema.
To override values for existing fields, use SAClient.upload_custom_values()
:type items: list of dicts

Request Example:
::
client.delete_custom_values(
project = "Medical Annotations",
items = [
{"image_1.png": ["study_date", "patient_sex"]},
{"image_2.png": ["study_date", "patient_sex"]}
]
)
"""
project_name, folder_name = extract_project_folder(project)
response = self.controller.delete_custom_values(
project_name=project_name, folder_name=folder_name, items=items
)
if response.errors:
raise AppException(response.errors)
6 changes: 2 additions & 4 deletions src/superannotate/lib/core/entities/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from lib.core.entities.base import AttachmentEntity
from lib.core.entities.base import BaseEntity as TmpBaseEntity
from lib.core.entities.base import BaseItemEntity
from lib.core.entities.base import ProjectEntity
from lib.core.entities.base import SettingEntity
from lib.core.entities.base import SubSetEntity
from lib.core.entities.integrations import IntegrationEntity
from lib.core.entities.items import DocumentEntity
from lib.core.entities.items import Entity
from lib.core.entities.items import TmpImageEntity
from lib.core.entities.items import VideoEntity
from lib.core.entities.project_entities import AnnotationClassEntity
Expand Down Expand Up @@ -36,8 +35,7 @@
# items
"TmpImageEntity",
"BaseEntity",
"TmpBaseEntity",
"Entity",
"BaseItemEntity",
"VideoEntity",
"DocumentEntity",
# Utils
Expand Down
17 changes: 16 additions & 1 deletion src/superannotate/lib/core/entities/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class TimedBaseModel(BaseModel):
updatedAt: StringDate = Field(None, alias="updatedAt")


class BaseEntity(TimedBaseModel):
class BaseItemEntity(TimedBaseModel):
name: str
path: Optional[str] = Field(
None, description="Item’s path in SuperAnnotate project"
Expand All @@ -52,10 +52,25 @@ class BaseEntity(TimedBaseModel):
entropy_value: Optional[float] = Field(description="Priority score of given item")
createdAt: str = Field(description="Date of creation")
updatedAt: str = Field(description="Update date")
custom_metadata: Optional[dict]

class Config:
extra = Extra.allow

def add_path(self, project_name: str, folder_name: str):
self.path = (
f"{project_name}{f'/{folder_name}' if folder_name != 'root' else ''}"
)
return self

@staticmethod
def map_fields(entity: dict) -> dict:
entity["url"] = entity.get("path")
entity["path"] = None
entity["annotator_email"] = entity.get("annotator_id")
entity["qa_email"] = entity.get("qa_id")
return entity


class AttachmentEntity(BaseModel):
name: Optional[str] = Field(default_factory=lambda: str(uuid.uuid4()))
Expand Down
Loading