6969from lib .infrastructure .annotation_adapter import MultimodalLargeAnnotationAdapter
7070from lib .infrastructure .utils import extract_project_folder
7171from lib .infrastructure .validators import wrap_error
72+ from lib .app .serializers import WMProjectSerializer
7273
7374logger = 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 ],
0 commit comments