Skip to content

Commit d342cd9

Browse files
authored
Merge pull request #737 from superannotateai/FRIDAY-3370
Project custom fields
2 parents 85dcdf7 + c19eda0 commit d342cd9

File tree

22 files changed

+1130
-312
lines changed

22 files changed

+1130
-312
lines changed

docs/source/api_reference/api_project.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Projects
55
.. _ref_search_projects:
66
.. automethod:: superannotate.SAClient.create_project
77
.. automethod:: superannotate.SAClient.search_projects
8+
.. automethod:: superannotate.SAClient.list_projects
89
.. automethod:: superannotate.SAClient.clone_project
910
.. automethod:: superannotate.SAClient.rename_project
1011
.. automethod:: superannotate.SAClient.delete_project
@@ -15,6 +16,7 @@ Projects
1516
.. automethod:: superannotate.SAClient.upload_images_to_project
1617
.. automethod:: superannotate.SAClient.attach_items_from_integrated_storage
1718
.. automethod:: superannotate.SAClient.upload_image_to_project
19+
.. automethod:: superannotate.SAClient.set_project_custom_field
1820
.. _ref_upload_images_from_folder_to_project:
1921
.. automethod:: superannotate.SAClient.upload_images_from_folder_to_project
2022
.. automethod:: superannotate.SAClient.upload_video_to_project

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ minversion = 3.7
33
log_cli=true
44
python_files = test_*.py
55
;pytest_plugins = ['pytest_profiling']
6-
addopts = -n 4 --dist loadscope
6+
;addopts = -n 4 --dist loadscope

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

Lines changed: 109 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from lib.infrastructure.annotation_adapter import MultimodalLargeAnnotationAdapter
7070
from lib.infrastructure.utils import extract_project_folder
7171
from lib.infrastructure.validators import wrap_error
72+
from lib.app.serializers import WMProjectSerializer
7273

7374
logger = logging.getLogger("sa")
7475

@@ -300,12 +301,17 @@ def retrieve_context(
300301
try:
301302
for component in component_data:
302303
if (
303-
component["type"] == "webComponent"
304-
and component["id"] == component_pk
304+
component["type"] == "webComponent"
305+
and component["id"] == component_pk
305306
):
306307
return True, component.get("context")
307-
if component["type"] in ("group", "grid") and "children" in component:
308-
found, val = retrieve_context(component["children"], component_pk)
308+
if (
309+
component["type"] in ("group", "grid")
310+
and "children" in component
311+
):
312+
found, val = retrieve_context(
313+
component["children"], component_pk
314+
)
309315
if found:
310316
return found, val
311317
except KeyError as e:
@@ -768,6 +774,7 @@ def get_project_metadata(
768774
include_workflow: Optional[bool] = False,
769775
include_contributors: Optional[bool] = False,
770776
include_complete_item_count: Optional[bool] = False,
777+
include_custom_fields: Optional[bool] = False,
771778
):
772779
"""Returns project metadata
773780
@@ -793,6 +800,9 @@ def get_project_metadata(
793800
the key "completed_items_count"
794801
:type include_complete_item_count: bool
795802
803+
:param include_custom_fields: include custom fields that have been created for the project.
804+
:type include_custom_fields: bool
805+
796806
:return: metadata of project
797807
:rtype: dict
798808
"""
@@ -811,6 +821,7 @@ def get_project_metadata(
811821
include_settings,
812822
include_contributors,
813823
include_complete_item_count,
824+
include_custom_fields,
814825
)
815826
if response.errors:
816827
raise AppException(response.errors)
@@ -945,6 +956,30 @@ def set_project_status(self, project: NotEmptyStr, status: PROJECT_STATUS):
945956
raise AppException(f"Failed to change {project.name} status.")
946957
logger.info(f"Successfully updated {project.name} status to {status}")
947958

959+
def set_project_custom_field(
960+
self, project: Union[NotEmptyStr, int], custom_field_name: str, value: Any
961+
):
962+
"""Sets or updates the value of a custom field for a specified project.
963+
964+
:param project: The name or ID of the project for which the custom field should be set or updated.
965+
:type project: str or int
966+
967+
:param custom_field_name: The name of the custom field to update or set.
968+
This field must already exist for the project.
969+
:type custom_field_name: str
970+
971+
:param value: The value assigned to the custom field, with the type depending on the field's configuration.
972+
:type value: Any
973+
"""
974+
project = (
975+
self.controller.get_project_by_id(project).data
976+
if isinstance(project, int)
977+
else self.controller.get_project(project)
978+
)
979+
self.controller.projects.set_project_custom_field(
980+
project, custom_field_name, value
981+
)
982+
948983
def set_folder_status(
949984
self, project: NotEmptyStr, folder: NotEmptyStr, status: FOLDER_STATUS
950985
):
@@ -1941,14 +1976,13 @@ def download_image(
19411976
logger.info(f"Downloaded image {image_name} to {local_dir_path} ")
19421977
return response.data
19431978

1944-
19451979
def upload_annotations(
1946-
self,
1947-
project: NotEmptyStr,
1948-
annotations: List[dict],
1949-
keep_status: bool = None,
1950-
*,
1951-
data_spec: Literal['default', 'multimodal'] = 'default'
1980+
self,
1981+
project: NotEmptyStr,
1982+
annotations: List[dict],
1983+
keep_status: bool = None,
1984+
*,
1985+
data_spec: Literal["default", "multimodal"] = "default",
19521986
):
19531987
"""Uploads a list of annotation dictionaries to the specified SuperAnnotate project or folder.
19541988
@@ -2022,7 +2056,7 @@ def upload_annotations(
20222056
annotations=annotations,
20232057
keep_status=keep_status,
20242058
user=self.controller.current_user,
2025-
output_format=data_spec
2059+
output_format=data_spec,
20262060
)
20272061
if response.errors:
20282062
raise AppException(response.errors)
@@ -2498,7 +2532,7 @@ def get_annotations(
24982532
project: Union[NotEmptyStr, int],
24992533
items: Optional[Union[List[NotEmptyStr], List[int]]] = None,
25002534
*,
2501-
data_spec: Literal['default', 'multimodal'] = 'default'
2535+
data_spec: Literal["default", "multimodal"] = "default",
25022536
):
25032537
"""Returns annotations for the given list of items.
25042538
@@ -2542,8 +2576,10 @@ def get_annotations(
25422576
project_id=project.id, folder_id=project.folder_id
25432577
).data
25442578
response = self.controller.annotations.list(
2545-
project, folder, items,
2546-
transform_version='llmJsonV2' if data_spec == 'multimodal' else None
2579+
project,
2580+
folder,
2581+
items,
2582+
transform_version="llmJsonV2" if data_spec == "multimodal" else None,
25472583
)
25482584
if response.errors:
25492585
raise AppException(response.errors)
@@ -3004,11 +3040,66 @@ def list_items(
30043040
for i in res:
30053041
i.custom_metadata = item_custom_fields[i.id]
30063042
exclude = {"meta", "annotator_email", "qa_email"}
3007-
if include:
3008-
if "custom_metadata" not in include:
3009-
exclude.add("custom_metadata")
3043+
if not include_custom_metadata:
3044+
exclude.add("custom_metadata")
30103045
return BaseSerializer.serialize_iterable(res, exclude=exclude)
30113046

3047+
def list_projects(
3048+
self,
3049+
*,
3050+
include: List[Literal["custom_fields"]] = None,
3051+
**filters,
3052+
):
3053+
# TODO finalize doc
3054+
"""
3055+
Search projects by filtering criteria.
3056+
3057+
:param include: Specifies additional fields to include in the response.
3058+
3059+
Possible values are
3060+
3061+
- "custom_fields": Includes custom field added to the project.
3062+
:type include: list of str, optional
3063+
3064+
:param filters: Specifies filtering criteria (e.g., name, ID, status), with all conditions combined using
3065+
logical AND. Only projects matching all criteria are returned. If no operation is specified,
3066+
an exact match is applied.
3067+
3068+
3069+
Supported operations:
3070+
- __ne: Value is not equal.
3071+
- __in: Value is in the list.
3072+
- __notin: Value is not in the list.
3073+
- __contains: Value has the substring.
3074+
- __starts: Value starts with the prefix.
3075+
- __ends: Value ends with the suffix.
3076+
3077+
Filter params::
3078+
3079+
- id: int
3080+
- id__in: list[int]
3081+
- name: str
3082+
- name__in: list[str]
3083+
- name__contains: str
3084+
- name__starts: str
3085+
- name__ends: str
3086+
- status: Literal[“NotStarted”, “InProgress”, “Completed”, “OnHold”]
3087+
- status__ne: Literal[“NotStarted”, “InProgress”, “Completed”, “OnHold”]
3088+
- status__in: List[Literal[“NotStarted”, “InProgress”, “Completed”, “OnHold”]]
3089+
- status__notin: List[Literal[“NotStarted”, “InProgress”, “Completed”, “OnHold”]]
3090+
- custom_field: Optional[dict] – Specifies custom fields attributes to filter projects by.
3091+
Custom fields can be accessed using the `custom_field__` prefix followed by the attribute name.
3092+
3093+
:type filters: ProjectFilters
3094+
3095+
:return: A list of project metadata that matches the filtering criteria.
3096+
:rtype: list of dicts
3097+
"""
3098+
return [
3099+
WMProjectSerializer(p).serialize()
3100+
for p in self.controller.projects.list_projects(include=include, **filters)
3101+
]
3102+
30123103
def attach_items(
30133104
self,
30143105
project: Union[NotEmptyStr, dict],

src/superannotate/lib/app/serializers.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from abc import ABC
2+
from enum import Enum
23
from typing import Any
34
from typing import List
45
from typing import Set
56
from typing import Union
67

78
import lib.core as constance
89
from lib.core.entities import BaseEntity
9-
from lib.core.enums import BaseTitledEnum
1010
from lib.core.pydantic_v1 import BaseModel
1111

1212

@@ -18,7 +18,7 @@ def __init__(self, entity: BaseEntity):
1818
def _fill_enum_values(data: dict):
1919
if isinstance(data, dict):
2020
for key, value in data.items():
21-
if isinstance(value, BaseTitledEnum):
21+
if isinstance(value, Enum):
2222
data[key] = value.name
2323
return data
2424

@@ -133,6 +133,26 @@ def serialize(
133133
return data
134134

135135

136+
class WMProjectSerializer(BaseSerializer):
137+
def serialize(
138+
self,
139+
fields: List[str] = None,
140+
by_alias: bool = False,
141+
flat: bool = False,
142+
exclude: Set[str] = None,
143+
exclude_unset=False,
144+
):
145+
146+
to_exclude = {"sync_status": True, "unverified_users": True, "classes": True}
147+
if exclude:
148+
for field in exclude:
149+
to_exclude[field] = True
150+
data = super().serialize(fields, by_alias, flat, to_exclude)
151+
if not data.get("status"):
152+
data["status"] = "Undefined"
153+
return data
154+
155+
136156
class FolderSerializer(BaseSerializer):
137157
def serialize(
138158
self,

0 commit comments

Comments
 (0)