33import json
44import platform
55import warnings
6+ from enum import Enum
67import typing
78from typing import Optional , Generator , Iterable , Any
89
1314from .base import BaseServerAPI
1415
1516if typing .TYPE_CHECKING :
16- from ayon_api .typing import ProjectDict , AnatomyPresetDict
17+ from ayon_api .typing import (
18+ ProjectDict ,
19+ AnatomyPresetDict ,
20+ ProjectListDict ,
21+ )
22+
23+
24+ class ProjectFetchType (Enum ):
25+ """How a project has to be fetched to get all requested data.
26+
27+ Some project data can be received only from GraphQl, and some can be
28+ received only with REST. That is based on requested fields.
29+
30+ There is also a dedicated endpoint to get information about all projects
31+ but returns very limited information about the project.
32+
33+ Enums:
34+ GraphQl: Requested project data can be received with GraphQl.
35+ REST: Requested project data can be received with /projects/{project}.
36+ RESTList: Requested project data can be received with /projects.
37+ Can be considered as a subset of 'REST'.
38+ GraphQlAndREST: It is necessary to use GraphQl and REST to get all
39+ requested data.
40+
41+ """
42+ GraphQl = "GraphQl"
43+ REST = "REST"
44+ RESTList = "RESTList"
45+ GraphQlAndREST = "GraphQlAndREST"
1746
1847
1948class ProjectsAPI (BaseServerAPI ):
@@ -156,12 +185,12 @@ def get_rest_projects(
156185 if project :
157186 yield project
158187
159- def get_project_names (
188+ def get_rest_projects_list (
160189 self ,
161190 active : Optional [bool ] = True ,
162191 library : Optional [bool ] = None ,
163- ) -> list [str ]:
164- """Receive available project names .
192+ ) -> list [ProjectListDict ]:
193+ """Receive available projects .
165194
166195 User must be logged in.
167196
@@ -172,7 +201,7 @@ def get_project_names(
172201 are returned if 'None' is passed.
173202
174203 Returns:
175- list[str ]: List of available project names .
204+ list[ProjectListDict ]: List of available projects .
176205
177206 """
178207 if active is not None :
@@ -181,16 +210,38 @@ def get_project_names(
181210 if library is not None :
182211 library = "true" if library else "false"
183212
184- query = prepare_query_string ({"active" : active , "library" : library })
185-
213+ query = prepare_query_string ({
214+ "active" : active ,
215+ "library" : library ,
216+ })
186217 response = self .get (f"projects{ query } " )
187218 response .raise_for_status ()
188219 data = response .data
189- project_names = []
190- if data :
191- for project in data ["projects" ]:
192- project_names .append (project ["name" ])
193- return project_names
220+ return data ["projects" ]
221+
222+ def get_project_names (
223+ self ,
224+ active : Optional [bool ] = True ,
225+ library : Optional [bool ] = None ,
226+ ) -> list [str ]:
227+ """Receive available project names.
228+
229+ User must be logged in.
230+
231+ Args:
232+ active (Optional[bool]): Filter active/inactive projects. Both
233+ are returned if 'None' is passed.
234+ library (Optional[bool]): Filter standard/library projects. Both
235+ are returned if 'None' is passed.
236+
237+ Returns:
238+ list[str]: List of available project names.
239+
240+ """
241+ return [
242+ project ["name" ]
243+ for project in self .get_rest_projects_list (active , library )
244+ ]
194245
195246 def get_projects (
196247 self ,
@@ -218,7 +269,11 @@ def get_projects(
218269 if fields is not None :
219270 fields = set (fields )
220271
221- graphql_fields , use_rest = self ._get_project_graphql_fields (fields )
272+ graphql_fields , fetch_type = self ._get_project_graphql_fields (fields )
273+ if fetch_type == ProjectFetchType .RESTList :
274+ yield from self .get_rest_projects_list (active , library )
275+ return
276+
222277 projects_by_name = {}
223278 if graphql_fields :
224279 projects = list (self ._get_graphql_projects (
@@ -227,7 +282,7 @@ def get_projects(
227282 fields = graphql_fields ,
228283 own_attributes = own_attributes ,
229284 ))
230- if not use_rest :
285+ if fetch_type == ProjectFetchType . GraphQl :
231286 yield from projects
232287 return
233288 projects_by_name = {p ["name" ]: p for p in projects }
@@ -236,7 +291,12 @@ def get_projects(
236291 name = project ["name" ]
237292 graphql_p = projects_by_name .get (name )
238293 if graphql_p :
239- project ["productTypes" ] = graphql_p ["productTypes" ]
294+ for key in (
295+ "productTypes" ,
296+ "usedTags" ,
297+ ):
298+ if key in graphql_p :
299+ project [key ] = graphql_p [key ]
240300 yield project
241301
242302 def get_project (
@@ -262,7 +322,7 @@ def get_project(
262322 if fields is not None :
263323 fields = set (fields )
264324
265- graphql_fields , use_rest = self ._get_project_graphql_fields (fields )
325+ graphql_fields , fetch_type = self ._get_project_graphql_fields (fields )
266326 graphql_project = None
267327 if graphql_fields :
268328 graphql_project = next (self ._get_graphql_projects (
@@ -271,14 +331,19 @@ def get_project(
271331 fields = graphql_fields ,
272332 own_attributes = own_attributes ,
273333 ), None )
274- if not graphql_project or not use_rest :
334+ if not graphql_project or fetch_type == fetch_type . GraphQl :
275335 return graphql_project
276336
277337 project = self .get_rest_project (project_name )
278338 if own_attributes :
279339 fill_own_attribs (project )
280340 if graphql_project :
281- project ["productTypes" ] = graphql_project ["productTypes" ]
341+ for key in (
342+ "productTypes" ,
343+ "usedTags" ,
344+ ):
345+ if key in graphql_project :
346+ project [key ] = graphql_project [key ]
282347 return project
283348
284349 def create_project (
@@ -585,34 +650,86 @@ def get_project_roots_by_platform(
585650
586651 def _get_project_graphql_fields (
587652 self , fields : Optional [set [str ]]
588- ) -> tuple [set [str ], bool ]:
589- """Fetch of project must be done using REST endpoint .
653+ ) -> tuple [set [str ], ProjectFetchType ]:
654+ """Find out if project can be fetched with GraphQl, REST or both .
590655
591656 Returns:
592657 set[str]: GraphQl fields.
593658
594659 """
595660 if fields is None :
596- return set (), True
597-
598- has_product_types = False
661+ return set (), ProjectFetchType .REST
662+
663+ rest_list_fields = {
664+ "name" ,
665+ "code" ,
666+ "active" ,
667+ "createdAt" ,
668+ "updatedAt" ,
669+ }
599670 graphql_fields = set ()
600- for field in fields :
671+ if len (fields - rest_list_fields ) == 0 :
672+ return graphql_fields , ProjectFetchType .RESTList
673+
674+ must_use_graphql = False
675+ for field in tuple (fields ):
601676 # Product types are available only in GraphQl
602- if field .startswith ("productTypes" ):
603- has_product_types = True
677+ if field == "usedTags" :
678+ graphql_fields .add ("usedTags" )
679+ elif field == "productTypes" :
680+ must_use_graphql = True
681+ fields .discard (field )
682+ graphql_fields .add ("productTypes.name" )
683+ graphql_fields .add ("productTypes.icon" )
684+ graphql_fields .add ("productTypes.color" )
685+
686+ elif field .startswith ("productTypes" ):
687+ must_use_graphql = True
688+ graphql_fields .add (field )
689+
690+ elif field == "productBaseTypes" :
691+ must_use_graphql = True
692+ fields .discard (field )
693+ graphql_fields .add ("productBaseTypes.name" )
694+
695+ elif field .startswith ("productBaseTypes" ):
696+ must_use_graphql = True
604697 graphql_fields .add (field )
605698
606- if not has_product_types :
607- return set (), True
699+ elif field == "bundle" or field == "bundles" :
700+ fields .discard (field )
701+ graphql_fields .add ("bundle.production" )
702+ graphql_fields .add ("bundle.staging" )
703+
704+ elif field .startswith ("bundle" ):
705+ graphql_fields .add (field )
608706
609- inters = fields & {"name" , "code" , "active" , "library" }
707+ elif field == "attrib" :
708+ fields .discard ("attrib" )
709+ graphql_fields |= self .get_attributes_fields_for_type (
710+ "project"
711+ )
712+
713+ # NOTE 'config' in GraphQl is NOT the same as from REST api.
714+ # - At the moment of this comment there is missing 'productBaseTypes'.
715+ inters = fields & {
716+ "name" ,
717+ "code" ,
718+ "active" ,
719+ "library" ,
720+ "usedTags" ,
721+ "data" ,
722+ }
610723 remainders = fields - (inters | graphql_fields )
611- if remainders :
724+ if not remainders :
725+ graphql_fields |= inters
726+ return graphql_fields , ProjectFetchType .GraphQl
727+
728+ if must_use_graphql :
612729 graphql_fields .add ("name" )
613- return graphql_fields , True
614- graphql_fields |= inters
615- return graphql_fields , False
730+ return graphql_fields , ProjectFetchType . GraphQlAndREST
731+
732+ return set (), ProjectFetchType . REST
616733
617734 def _fill_project_entity_data (self , project : dict [str , Any ]) -> None :
618735 # Add fake scope to statuses if not available
@@ -632,13 +749,15 @@ def _fill_project_entity_data(self, project: dict[str, Any]) -> None:
632749 # Convert 'data' from string to dict if needed
633750 if "data" in project :
634751 project_data = project ["data" ]
635- if isinstance (project_data , str ):
752+ if project_data is None :
753+ project ["data" ] = {}
754+ elif isinstance (project_data , str ):
636755 project_data = json .loads (project_data )
637756 project ["data" ] = project_data
638757
639758 # Fill 'bundle' from data if is not filled
640759 if "bundle" not in project :
641- bundle_data = project ["data" ].get ("bundle" , {})
760+ bundle_data = project ["data" ].get ("bundle" ) or {}
642761 prod_bundle = bundle_data .get ("production" )
643762 staging_bundle = bundle_data .get ("staging" )
644763 project ["bundle" ] = {
@@ -647,9 +766,12 @@ def _fill_project_entity_data(self, project: dict[str, Any]) -> None:
647766 }
648767
649768 # Convert 'config' from string to dict if needed
650- config = project .get ("config" )
651- if isinstance (config , str ):
652- project ["config" ] = json .loads (config )
769+ if "config" in project :
770+ config = project ["config" ]
771+ if config is None :
772+ project ["config" ] = {}
773+ elif isinstance (config , str ):
774+ project ["config" ] = json .loads (config )
653775
654776 # Unifiy 'linkTypes' data structure from REST and GraphQL
655777 if "linkTypes" in project :
0 commit comments