Skip to content

Commit 87c414e

Browse files
authored
Merge pull request #470 from superannotateai/custom_field_values
Custom field values
2 parents 9b0631f + 40d3e2b commit 87c414e

File tree

19 files changed

+501
-187
lines changed

19 files changed

+501
-187
lines changed

docs/source/superannotate.sdk.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ ______
8585
.. automethod:: superannotate.SAClient.create_custom_fields
8686
.. automethod:: superannotate.SAClient.get_custom_fields
8787
.. automethod:: superannotate.SAClient.delete_custom_fields
88+
.. automethod:: superannotate.SAClient.upload_custom_values
89+
.. automethod:: superannotate.SAClient.delete_custom_values
8890

8991
----------
9092

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 160 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import tempfile
66
from pathlib import Path
77
from typing import Callable
8+
from typing import Dict
89
from typing import Iterable
910
from typing import List
1011
from typing import Optional
@@ -2571,7 +2572,6 @@ def set_annotation_statuses(
25712572
raise AppException(response.errors)
25722573
else:
25732574
logger.info("Annotation statuses of items changed")
2574-
return response.data
25752575

25762576
def download_annotations(
25772577
self,
@@ -2666,7 +2666,8 @@ def create_custom_fields(self, project: NotEmptyStr, fields: dict):
26662666
--------------------------------------
26672667
field spec spec value
26682668
============== ======================
2669-
format “email” or “date”
2669+
format “email” (user@example.com) or “date” (YYYY-MM-DD)
2670+
26702671
enum list of strings
26712672
============== ======================
26722673
::
@@ -2724,40 +2725,42 @@ def get_custom_fields(self, project: NotEmptyStr):
27242725
27252726
Response Example:
27262727
::
2727-
{
2728-
"study_date": {
2729-
"type": "string",
2730-
"format": "date"
2731-
},
2732-
"patient_id": {
2733-
"type": "string"
2734-
},
2735-
"patient_sex": {
2736-
"type": "string",
2737-
"enum": [
2738-
"male", "female"
2739-
]
2740-
},
2741-
"patient_age": {
2742-
"type": "number"
2743-
},
2744-
"medical_specialist": {
2745-
"type": "string",
2746-
"format": "email"
2747-
},
2748-
"duration": {
2749-
"type": "number",
2750-
"minimum": 10
2751-
}
2752-
}
2728+
{
2729+
"study_date": {
2730+
"type": "string",
2731+
"format": "date"
2732+
},
2733+
"patient_id": {
2734+
"type": "string"
2735+
},
2736+
"patient_sex": {
2737+
"type": "string",
2738+
"enum": [
2739+
"male", "female"
2740+
]
2741+
},
2742+
"patient_age": {
2743+
"type": "number"
2744+
},
2745+
"medical_specialist": {
2746+
"type": "string",
2747+
"format": "email"
2748+
},
2749+
"duration": {
2750+
"type": "number",
2751+
"minimum": 10
2752+
}
2753+
}
27532754
"""
27542755
project_name, _ = extract_project_folder(project)
27552756
response = self.controller.get_custom_schema(project_name=project)
27562757
if response.errors:
27572758
raise AppException(response.errors)
27582759
return response.data
27592760

2760-
def delete_custom_fields(self, project: NotEmptyStr, fields: list):
2761+
def delete_custom_fields(
2762+
self, project: NotEmptyStr, fields: conlist(NotEmptyStr, min_items=1)
2763+
):
27612764
"""Remove custom fields from a project’s custom metadata schema.
27622765
27632766
:param project: project name (e.g., “project1”)
@@ -2771,39 +2774,143 @@ def delete_custom_fields(self, project: NotEmptyStr, fields: list):
27712774
27722775
Request Example:
27732776
::
2774-
client = SAClient()
2775-
client.delete_custom_fields(
2776-
project = "Medical Annotations",
2777-
fields = ["duration", patient_age]
2778-
)
2777+
client = SAClient()
2778+
client.delete_custom_fields(
2779+
project = "Medical Annotations",
2780+
fields = ["duration", patient_age]
2781+
)
27792782
27802783
Response Example:
27812784
::
2782-
{
2783-
"study_date": {
2784-
"type": "string",
2785-
"format": "date"
2786-
},
2787-
"patient_id": {
2788-
"type": "string"
2789-
},
2790-
"patient_sex": {
2791-
"type": "string",
2792-
"enum": [
2793-
"male", "female"
2794-
]
2795-
},
2796-
"medical_specialist": {
2797-
"type": "string",
2798-
"format": "email"
2785+
{
2786+
"study_date": {
2787+
"type": "string",
2788+
"format": "date"
2789+
},
2790+
"patient_id": {
2791+
"type": "string"
2792+
},
2793+
"patient_sex": {
2794+
"type": "string",
2795+
"enum": [
2796+
"male", "female"
2797+
]
2798+
},
2799+
"medical_specialist": {
2800+
"type": "string",
2801+
"format": "email"
2802+
}
27992803
}
2800-
}
28012804
28022805
"""
28032806
project_name, _ = extract_project_folder(project)
28042807
response = self.controller.delete_custom_schema(
2805-
project_name=project, fields=fields
2808+
project_name=project_name, fields=fields
28062809
)
28072810
if response.errors:
28082811
raise AppException(response.errors)
28092812
return response.data
2813+
2814+
def upload_custom_values(
2815+
self, project: NotEmptyStr, items: conlist(Dict[str, dict], min_items=1)
2816+
):
2817+
"""
2818+
Attach custom metadata to items.
2819+
SAClient.get_item_metadata(), SAClient.search_items(), SAClient.query() methods
2820+
will return the item metadata and custom metadata.
2821+
2822+
:param project: project name or folder path (e.g., “project1/folder1”)
2823+
:type project: str
2824+
2825+
:param items: list of name-data pairs.
2826+
The key of each dict indicates an existing item name and the value represents the custom metadata dict.
2827+
The values for the corresponding keys will be added to an item or will be overridden.
2828+
:type items: list of dicts
2829+
2830+
:return: the count of succeeded items and the list of failed item names.
2831+
:rtype: dict
2832+
2833+
Request Example:
2834+
::
2835+
client = SAClient()
2836+
2837+
items_values = [
2838+
{
2839+
"image_1.png": {
2840+
"study_date": "2021-12-31",
2841+
"patient_id": "62078f8a756ddb2ca9fc9660",
2842+
"patient_sex": "female",
2843+
"medical_specialist": "robertboxer@ms.com"
2844+
}
2845+
},
2846+
{
2847+
"image_2.png": {
2848+
"study_date": "2021-12-31",
2849+
"patient_id": "62078f8a756ddb2ca9fc9661",
2850+
"patient_sex": "female",
2851+
"medical_specialist": "robertboxer@ms.com"
2852+
}
2853+
},
2854+
{
2855+
"image_3.png": {
2856+
"study_date": "2011-10-05T14:48:00.000Z",
2857+
"patient_": "62078f8a756ddb2ca9fc9660",
2858+
"patient_sex": "female",
2859+
"medical_specialist": "robertboxer"
2860+
}
2861+
}
2862+
]
2863+
2864+
client.upload_custom_values(
2865+
project = "Medical Annotations",
2866+
items = items_values
2867+
)
2868+
Response Example:
2869+
::
2870+
{
2871+
"successful_items_count": 2,
2872+
"failed_items_names": ["image_3.png"]
2873+
}
2874+
"""
2875+
2876+
project_name, folder_name = extract_project_folder(project)
2877+
response = self.controller.upload_custom_values(
2878+
project_name=project_name, folder_name=folder_name, items=items
2879+
)
2880+
if response.errors:
2881+
raise AppException(response.errors)
2882+
return response.data
2883+
2884+
def delete_custom_values(
2885+
self, project: NotEmptyStr, items: conlist(Dict[str, List[str]], min_items=1)
2886+
):
2887+
"""
2888+
Remove custom data from items
2889+
2890+
:param project: project name or folder path (e.g., “project1/folder1”)
2891+
:type project: str
2892+
2893+
:param items: list of name-custom data dicts.
2894+
The key of each dict element indicates an existing item in the project root or folder.
2895+
The value should be the list of fields to be removed from the given item.
2896+
Please note, that the function removes pointed metadata from a given item.
2897+
To delete metadata for all items you should delete it from the custom metadata schema.
2898+
To override values for existing fields, use SAClient.upload_custom_values()
2899+
:type items: list of dicts
2900+
2901+
Request Example:
2902+
::
2903+
client.delete_custom_values(
2904+
project = "Medical Annotations",
2905+
items = [
2906+
{"image_1.png": ["study_date", "patient_sex"]},
2907+
{"image_2.png": ["study_date", "patient_sex"]}
2908+
]
2909+
)
2910+
"""
2911+
project_name, folder_name = extract_project_folder(project)
2912+
response = self.controller.delete_custom_values(
2913+
project_name=project_name, folder_name=folder_name, items=items
2914+
)
2915+
if response.errors:
2916+
raise AppException(response.errors)

src/superannotate/lib/core/entities/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from lib.core.entities.base import AttachmentEntity
2-
from lib.core.entities.base import BaseEntity as TmpBaseEntity
2+
from lib.core.entities.base import BaseItemEntity
33
from lib.core.entities.base import ProjectEntity
44
from lib.core.entities.base import SettingEntity
55
from lib.core.entities.base import SubSetEntity
66
from lib.core.entities.integrations import IntegrationEntity
77
from lib.core.entities.items import DocumentEntity
8-
from lib.core.entities.items import Entity
98
from lib.core.entities.items import TmpImageEntity
109
from lib.core.entities.items import VideoEntity
1110
from lib.core.entities.project_entities import AnnotationClassEntity
@@ -36,8 +35,7 @@
3635
# items
3736
"TmpImageEntity",
3837
"BaseEntity",
39-
"TmpBaseEntity",
40-
"Entity",
38+
"BaseItemEntity",
4139
"VideoEntity",
4240
"DocumentEntity",
4341
# Utils

src/superannotate/lib/core/entities/base.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class TimedBaseModel(BaseModel):
4040
updatedAt: StringDate = Field(None, alias="updatedAt")
4141

4242

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

5657
class Config:
5758
extra = Extra.allow
5859

60+
def add_path(self, project_name: str, folder_name: str):
61+
self.path = (
62+
f"{project_name}{f'/{folder_name}' if folder_name != 'root' else ''}"
63+
)
64+
return self
65+
66+
@staticmethod
67+
def map_fields(entity: dict) -> dict:
68+
entity["url"] = entity.get("path")
69+
entity["path"] = None
70+
entity["annotator_email"] = entity.get("annotator_id")
71+
entity["qa_email"] = entity.get("qa_id")
72+
return entity
73+
5974

6075
class AttachmentEntity(BaseModel):
6176
name: Optional[str] = Field(default_factory=lambda: str(uuid.uuid4()))
Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,13 @@
11
from typing import Optional
22

3-
from lib.core.entities.base import BaseEntity
3+
from lib.core.entities.base import BaseItemEntity
44
from lib.core.enums import ApprovalStatus
55
from lib.core.enums import SegmentationStatus
66
from pydantic import Extra
77
from pydantic import Field
88

99

10-
class Entity(BaseEntity):
11-
class Config:
12-
extra = Extra.allow
13-
14-
def add_path(self, project_name: str, folder_name: str):
15-
self.path = (
16-
f"{project_name}{f'/{folder_name}' if folder_name != 'root' else ''}"
17-
)
18-
return self
19-
20-
@staticmethod
21-
def map_fields(entity: dict) -> dict:
22-
entity["url"] = entity.get("path")
23-
entity["path"] = None
24-
entity["annotator_email"] = entity.get("annotator_id")
25-
entity["qa_email"] = entity.get("qa_id")
26-
return entity
27-
28-
29-
class TmpImageEntity(Entity):
10+
class TmpImageEntity(BaseItemEntity):
3011
prediction_status: Optional[SegmentationStatus] = Field(
3112
SegmentationStatus.NOT_STARTED
3213
)
@@ -40,13 +21,13 @@ class Config:
4021
extra = Extra.ignore
4122

4223

43-
class VideoEntity(Entity):
24+
class VideoEntity(BaseItemEntity):
4425
approval_status: Optional[ApprovalStatus] = Field(None)
4526

4627
class Config:
4728
extra = Extra.ignore
4829

4930

50-
class DocumentEntity(Entity):
31+
class DocumentEntity(BaseItemEntity):
5132
class Config:
5233
extra = Extra.ignore

0 commit comments

Comments
 (0)