Skip to content

Commit a62ff08

Browse files
authored
Merge pull request #289 from ynput/enhancement/better-project-fetching
Projects: Better projects fetching
2 parents ae71136 + cb7038b commit a62ff08

File tree

5 files changed

+202
-38
lines changed

5 files changed

+202
-38
lines changed

ayon_api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
get_build_in_anatomy_preset,
153153
get_rest_project,
154154
get_rest_projects,
155+
get_rest_projects_list,
155156
get_project_names,
156157
get_projects,
157158
get_project,
@@ -429,6 +430,7 @@
429430
"get_build_in_anatomy_preset",
430431
"get_rest_project",
431432
"get_rest_projects",
433+
"get_rest_projects_list",
432434
"get_project_names",
433435
"get_projects",
434436
"get_project",

ayon_api/_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
BundlesInfoDict,
6565
AnatomyPresetDict,
6666
SecretDict,
67+
ProjectListDict,
6768
AnyEntityDict,
6869
ProjectDict,
6970
FolderDict,
@@ -3573,6 +3574,31 @@ def get_rest_projects(
35733574
)
35743575

35753576

3577+
def get_rest_projects_list(
3578+
active: Optional[bool] = True,
3579+
library: Optional[bool] = None,
3580+
) -> list[ProjectListDict]:
3581+
"""Receive available projects.
3582+
3583+
User must be logged in.
3584+
3585+
Args:
3586+
active (Optional[bool]): Filter active/inactive projects. Both
3587+
are returned if 'None' is passed.
3588+
library (Optional[bool]): Filter standard/library projects. Both
3589+
are returned if 'None' is passed.
3590+
3591+
Returns:
3592+
list[ProjectListDict]: List of available projects.
3593+
3594+
"""
3595+
con = get_server_api_connection()
3596+
return con.get_rest_projects_list(
3597+
active=active,
3598+
library=library,
3599+
)
3600+
3601+
35763602
def get_project_names(
35773603
active: Optional[bool] = True,
35783604
library: Optional[bool] = None,

ayon_api/_api_helpers/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ServerVersion,
1515
ProjectDict,
1616
StreamType,
17+
AttributeScope,
1718
)
1819

1920
_PLACEHOLDER = object()
@@ -125,6 +126,11 @@ def get_user(
125126
) -> Optional[dict[str, Any]]:
126127
raise NotImplementedError()
127128

129+
def get_attributes_fields_for_type(
130+
self, entity_type: AttributeScope
131+
) -> set[str]:
132+
raise NotImplementedError()
133+
128134
def _prepare_fields(
129135
self,
130136
entity_type: str,

ayon_api/_api_helpers/projects.py

Lines changed: 160 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import platform
55
import warnings
6+
from enum import Enum
67
import typing
78
from typing import Optional, Generator, Iterable, Any
89

@@ -13,7 +14,35 @@
1314
from .base import BaseServerAPI
1415

1516
if 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

1948
class 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

Comments
 (0)