ayon-python-api 1.2.4__tar.gz → 1.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/PKG-INFO +7 -2
  2. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/__init__.py +4 -0
  3. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api.py +41 -0
  4. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/base.py +9 -0
  5. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/products.py +35 -3
  6. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/projects.py +165 -39
  7. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/constants.py +7 -0
  8. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/entity_hub.py +44 -1
  9. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/graphql_queries.py +3 -0
  10. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/operations.py +47 -18
  11. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/server_api.py +13 -0
  12. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/typing.py +8 -0
  13. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/version.py +1 -1
  14. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_python_api.egg-info/PKG-INFO +8 -3
  15. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_python_api.egg-info/SOURCES.txt +6 -1
  16. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/pyproject.toml +1 -1
  17. ayon_python_api-1.2.5/tests/test_entity_hub.py +1289 -0
  18. ayon_python_api-1.2.5/tests/test_folder_hierarchy.py +545 -0
  19. ayon_python_api-1.2.5/tests/test_get_events.py +159 -0
  20. ayon_python_api-1.2.5/tests/test_graphql_queries.py +127 -0
  21. ayon_python_api-1.2.5/tests/test_server.py +940 -0
  22. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/LICENSE +0 -0
  23. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/README.md +0 -0
  24. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/__init__.py +0 -0
  25. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/actions.py +0 -0
  26. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/activities.py +0 -0
  27. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/attributes.py +0 -0
  28. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/bundles_addons.py +0 -0
  29. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/dependency_packages.py +0 -0
  30. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/events.py +0 -0
  31. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/folders.py +0 -0
  32. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/installers.py +0 -0
  33. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/links.py +0 -0
  34. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/lists.py +0 -0
  35. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/representations.py +0 -0
  36. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/secrets.py +0 -0
  37. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/tasks.py +0 -0
  38. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/thumbnails.py +0 -0
  39. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/versions.py +0 -0
  40. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/_api_helpers/workfiles.py +0 -0
  41. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/events.py +0 -0
  42. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/exceptions.py +0 -0
  43. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/graphql.py +0 -0
  44. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_api/utils.py +0 -0
  45. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  46. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_python_api.egg-info/requires.txt +0 -0
  47. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/ayon_python_api.egg-info/top_level.txt +0 -0
  48. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/setup.cfg +0 -0
  49. {ayon_python_api-1.2.4 → ayon_python_api-1.2.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: ayon_python_api
3
- Version: 1.2.4
3
+ Version: 1.2.5
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -215,6 +215,11 @@ Classifier: Programming Language :: Python
215
215
  Classifier: Programming Language :: Python :: 3
216
216
  Description-Content-Type: text/markdown
217
217
  License-File: LICENSE
218
+ Requires-Dist: requests>=2.27.1
219
+ Requires-Dist: Unidecode>=1.3.0
220
+ Dynamic: author
221
+ Dynamic: home-page
222
+ Dynamic: license-file
218
223
 
219
224
  # AYON server API
220
225
  Python client for connection server. The client is using REST and GraphQl to communicate with server with `requests` module.
@@ -54,6 +54,7 @@ from ._api import (
54
54
  get_info,
55
55
  get_server_version,
56
56
  get_server_version_tuple,
57
+ is_product_base_type_supported,
57
58
  get_users,
58
59
  get_user_by_name,
59
60
  get_user,
@@ -152,6 +153,7 @@ from ._api import (
152
153
  get_build_in_anatomy_preset,
153
154
  get_rest_project,
154
155
  get_rest_projects,
156
+ get_rest_projects_list,
155
157
  get_project_names,
156
158
  get_projects,
157
159
  get_project,
@@ -331,6 +333,7 @@ __all__ = (
331
333
  "get_info",
332
334
  "get_server_version",
333
335
  "get_server_version_tuple",
336
+ "is_product_base_type_supported",
334
337
  "get_users",
335
338
  "get_user_by_name",
336
339
  "get_user",
@@ -429,6 +432,7 @@ __all__ = (
429
432
  "get_build_in_anatomy_preset",
430
433
  "get_rest_project",
431
434
  "get_rest_projects",
435
+ "get_rest_projects_list",
432
436
  "get_project_names",
433
437
  "get_projects",
434
438
  "get_project",
@@ -64,6 +64,7 @@ if typing.TYPE_CHECKING:
64
64
  BundlesInfoDict,
65
65
  AnatomyPresetDict,
66
66
  SecretDict,
67
+ ProjectListDict,
67
68
  AnyEntityDict,
68
69
  ProjectDict,
69
70
  FolderDict,
@@ -720,6 +721,13 @@ def get_server_version_tuple() -> ServerVersion:
720
721
  return con.get_server_version_tuple()
721
722
 
722
723
 
724
+ def is_product_base_type_supported() -> bool:
725
+ """Product base types are available on server.
726
+ """
727
+ con = get_server_api_connection()
728
+ return con.is_product_base_type_supported()
729
+
730
+
723
731
  def get_users(
724
732
  project_name: Optional[str] = None,
725
733
  usernames: Optional[Iterable[str]] = None,
@@ -3573,6 +3581,31 @@ def get_rest_projects(
3573
3581
  )
3574
3582
 
3575
3583
 
3584
+ def get_rest_projects_list(
3585
+ active: Optional[bool] = True,
3586
+ library: Optional[bool] = None,
3587
+ ) -> list[ProjectListDict]:
3588
+ """Receive available projects.
3589
+
3590
+ User must be logged in.
3591
+
3592
+ Args:
3593
+ active (Optional[bool]): Filter active/inactive projects. Both
3594
+ are returned if 'None' is passed.
3595
+ library (Optional[bool]): Filter standard/library projects. Both
3596
+ are returned if 'None' is passed.
3597
+
3598
+ Returns:
3599
+ list[ProjectListDict]: List of available projects.
3600
+
3601
+ """
3602
+ con = get_server_api_connection()
3603
+ return con.get_rest_projects_list(
3604
+ active=active,
3605
+ library=library,
3606
+ )
3607
+
3608
+
3576
3609
  def get_project_names(
3577
3610
  active: Optional[bool] = True,
3578
3611
  library: Optional[bool] = None,
@@ -4874,6 +4907,7 @@ def get_products(
4874
4907
  product_names: Optional[Iterable[str]] = None,
4875
4908
  folder_ids: Optional[Iterable[str]] = None,
4876
4909
  product_types: Optional[Iterable[str]] = None,
4910
+ product_base_types: Optional[Iterable[str]] = None,
4877
4911
  product_name_regex: Optional[str] = None,
4878
4912
  product_path_regex: Optional[str] = None,
4879
4913
  names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None,
@@ -4926,6 +4960,7 @@ def get_products(
4926
4960
  product_names=product_names,
4927
4961
  folder_ids=folder_ids,
4928
4962
  product_types=product_types,
4963
+ product_base_types=product_base_types,
4929
4964
  product_name_regex=product_name_regex,
4930
4965
  product_path_regex=product_path_regex,
4931
4966
  names_by_folder_ids=names_by_folder_ids,
@@ -5084,6 +5119,7 @@ def create_product(
5084
5119
  tags: Optional[Iterable[str]] = None,
5085
5120
  status: Optional[str] = None,
5086
5121
  active: Optional[bool] = None,
5122
+ product_base_type: Optional[str] = None,
5087
5123
  product_id: Optional[str] = None,
5088
5124
  ) -> str:
5089
5125
  """Create new product.
@@ -5098,6 +5134,7 @@ def create_product(
5098
5134
  tags (Optional[Iterable[str]]): Product tags.
5099
5135
  status (Optional[str]): Product status.
5100
5136
  active (Optional[bool]): Product active state.
5137
+ product_base_type (Optional[str]): Product base type.
5101
5138
  product_id (Optional[str]): Product id. If not passed new id is
5102
5139
  generated.
5103
5140
 
@@ -5116,6 +5153,7 @@ def create_product(
5116
5153
  tags=tags,
5117
5154
  status=status,
5118
5155
  active=active,
5156
+ product_base_type=product_base_type,
5119
5157
  product_id=product_id,
5120
5158
  )
5121
5159
 
@@ -5126,6 +5164,7 @@ def update_product(
5126
5164
  name: Optional[str] = None,
5127
5165
  folder_id: Optional[str] = None,
5128
5166
  product_type: Optional[str] = None,
5167
+ product_base_type: Optional[str] = None,
5129
5168
  attrib: Optional[dict[str, Any]] = None,
5130
5169
  data: Optional[dict[str, Any]] = None,
5131
5170
  tags: Optional[Iterable[str]] = None,
@@ -5145,6 +5184,7 @@ def update_product(
5145
5184
  name (Optional[str]): New product name.
5146
5185
  folder_id (Optional[str]): New product id.
5147
5186
  product_type (Optional[str]): New product type.
5187
+ product_base_type (Optional[str]): New product base type.
5148
5188
  attrib (Optional[dict[str, Any]]): New product attributes.
5149
5189
  data (Optional[dict[str, Any]]): New product data.
5150
5190
  tags (Optional[Iterable[str]]): New product tags.
@@ -5159,6 +5199,7 @@ def update_product(
5159
5199
  name=name,
5160
5200
  folder_id=folder_id,
5161
5201
  product_type=product_type,
5202
+ product_base_type=product_base_type,
5162
5203
  attrib=attrib,
5163
5204
  data=data,
5164
5205
  tags=tags,
@@ -14,6 +14,7 @@ if typing.TYPE_CHECKING:
14
14
  ServerVersion,
15
15
  ProjectDict,
16
16
  StreamType,
17
+ AttributeScope,
17
18
  )
18
19
 
19
20
  _PLACEHOLDER = object()
@@ -24,6 +25,9 @@ class BaseServerAPI:
24
25
  def log(self) -> logging.Logger:
25
26
  raise NotImplementedError()
26
27
 
28
+ def is_product_base_type_supported(self) -> bool:
29
+ raise NotImplementedError()
30
+
27
31
  def get_server_version(self) -> str:
28
32
  raise NotImplementedError()
29
33
 
@@ -125,6 +129,11 @@ class BaseServerAPI:
125
129
  ) -> Optional[dict[str, Any]]:
126
130
  raise NotImplementedError()
127
131
 
132
+ def get_attributes_fields_for_type(
133
+ self, entity_type: AttributeScope
134
+ ) -> set[str]:
135
+ raise NotImplementedError()
136
+
128
137
  def _prepare_fields(
129
138
  self,
130
139
  entity_type: str,
@@ -5,6 +5,7 @@ import warnings
5
5
  import typing
6
6
  from typing import Optional, Iterable, Generator, Any
7
7
 
8
+ from ayon_api.exceptions import UnsupportedServerVersion
8
9
  from ayon_api.utils import (
9
10
  prepare_list_filters,
10
11
  create_entity_id,
@@ -32,9 +33,10 @@ class ProductsAPI(BaseServerAPI):
32
33
  self,
33
34
  project_name: str,
34
35
  product_ids: Optional[Iterable[str]] = None,
35
- product_names: Optional[Iterable[str]]=None,
36
- folder_ids: Optional[Iterable[str]]=None,
37
- product_types: Optional[Iterable[str]]=None,
36
+ product_names: Optional[Iterable[str]] = None,
37
+ folder_ids: Optional[Iterable[str]] = None,
38
+ product_types: Optional[Iterable[str]] = None,
39
+ product_base_types: Optional[Iterable[str]] = None,
38
40
  product_name_regex: Optional[str] = None,
39
41
  product_path_regex: Optional[str] = None,
40
42
  names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None,
@@ -59,6 +61,8 @@ class ProductsAPI(BaseServerAPI):
59
61
  Use 'None' if folder is direct child of project.
60
62
  product_types (Optional[Iterable[str]]): Product types used for
61
63
  filtering.
64
+ product_base_types (Optional[Iterable[str]]): Product base types
65
+ used for filtering.
62
66
  product_name_regex (Optional[str]): Filter products by name regex.
63
67
  product_path_regex (Optional[str]): Filter products by path regex.
64
68
  Path starts with folder path and ends with product name.
@@ -83,6 +87,11 @@ class ProductsAPI(BaseServerAPI):
83
87
  if not project_name:
84
88
  return
85
89
 
90
+ if product_base_types and not self.is_product_base_type_supported():
91
+ raise UnsupportedServerVersion(
92
+ "Product base type is not supported for your server version."
93
+ )
94
+
86
95
  # Prepare these filters before 'name_by_filter_ids' filter
87
96
  filter_product_names = None
88
97
  if product_names is not None:
@@ -150,6 +159,7 @@ class ProductsAPI(BaseServerAPI):
150
159
  filters,
151
160
  ("productIds", product_ids),
152
161
  ("productTypes", product_types),
162
+ ("productBaseTypes", product_base_types),
153
163
  ("productStatuses", statuses),
154
164
  ("productTags", tags),
155
165
  ):
@@ -378,6 +388,7 @@ class ProductsAPI(BaseServerAPI):
378
388
  tags: Optional[Iterable[str]] =None,
379
389
  status: Optional[str] = None,
380
390
  active: Optional[bool] = None,
391
+ product_base_type: Optional[str] = None,
381
392
  product_id: Optional[str] = None,
382
393
  ) -> str:
383
394
  """Create new product.
@@ -392,6 +403,7 @@ class ProductsAPI(BaseServerAPI):
392
403
  tags (Optional[Iterable[str]]): Product tags.
393
404
  status (Optional[str]): Product status.
394
405
  active (Optional[bool]): Product active state.
406
+ product_base_type (Optional[str]): Product base type.
395
407
  product_id (Optional[str]): Product id. If not passed new id is
396
408
  generated.
397
409
 
@@ -399,6 +411,14 @@ class ProductsAPI(BaseServerAPI):
399
411
  str: Product id.
400
412
 
401
413
  """
414
+ if (
415
+ product_base_type is not None
416
+ and not self.is_product_base_type_supported()
417
+ ):
418
+ raise UnsupportedServerVersion(
419
+ "Product base type is not supported for your server version."
420
+ )
421
+
402
422
  if not product_id:
403
423
  product_id = create_entity_id()
404
424
  create_data = {
@@ -408,6 +428,7 @@ class ProductsAPI(BaseServerAPI):
408
428
  "folderId": folder_id,
409
429
  }
410
430
  for key, value in (
431
+ ("productBaseType", product_base_type),
411
432
  ("attrib", attrib),
412
433
  ("data", data),
413
434
  ("tags", tags),
@@ -431,6 +452,7 @@ class ProductsAPI(BaseServerAPI):
431
452
  name: Optional[str] = None,
432
453
  folder_id: Optional[str] = None,
433
454
  product_type: Optional[str] = None,
455
+ product_base_type: Optional[str] = None,
434
456
  attrib: Optional[dict[str, Any]] = None,
435
457
  data: Optional[dict[str, Any]] = None,
436
458
  tags: Optional[Iterable[str]] = None,
@@ -450,6 +472,7 @@ class ProductsAPI(BaseServerAPI):
450
472
  name (Optional[str]): New product name.
451
473
  folder_id (Optional[str]): New product id.
452
474
  product_type (Optional[str]): New product type.
475
+ product_base_type (Optional[str]): New product base type.
453
476
  attrib (Optional[dict[str, Any]]): New product attributes.
454
477
  data (Optional[dict[str, Any]]): New product data.
455
478
  tags (Optional[Iterable[str]]): New product tags.
@@ -457,10 +480,19 @@ class ProductsAPI(BaseServerAPI):
457
480
  active (Optional[bool]): New product active state.
458
481
 
459
482
  """
483
+ if (
484
+ product_base_type is not None
485
+ and not self.is_product_base_type_supported()
486
+ ):
487
+ raise UnsupportedServerVersion(
488
+ "Product base type is not supported for your server version."
489
+ )
490
+
460
491
  update_data = {}
461
492
  for key, value in (
462
493
  ("name", name),
463
494
  ("productType", product_type),
495
+ ("productBaseType", product_base_type),
464
496
  ("folderId", folder_id),
465
497
  ("attrib", attrib),
466
498
  ("data", data),
@@ -3,17 +3,50 @@ from __future__ import annotations
3
3
  import json
4
4
  import platform
5
5
  import warnings
6
+ from enum import Enum
6
7
  import typing
7
8
  from typing import Optional, Generator, Iterable, Any
8
9
 
9
- from ayon_api.constants import PROJECT_NAME_REGEX
10
+ from ayon_api.constants import (
11
+ PROJECT_NAME_REGEX,
12
+ DEFAULT_PRODUCT_BASE_TYPE_FIELDS,
13
+ DEFAULT_PRODUCT_TYPE_FIELDS,
14
+ )
10
15
  from ayon_api.utils import prepare_query_string, fill_own_attribs
11
16
  from ayon_api.graphql_queries import projects_graphql_query
12
17
 
13
18
  from .base import BaseServerAPI
14
19
 
15
20
  if typing.TYPE_CHECKING:
16
- from ayon_api.typing import ProjectDict, AnatomyPresetDict
21
+ from ayon_api.typing import (
22
+ ProjectDict,
23
+ AnatomyPresetDict,
24
+ ProjectListDict,
25
+ )
26
+
27
+
28
+ class ProjectFetchType(Enum):
29
+ """How a project has to be fetched to get all requested data.
30
+
31
+ Some project data can be received only from GraphQl, and some can be
32
+ received only with REST. That is based on requested fields.
33
+
34
+ There is also a dedicated endpoint to get information about all projects
35
+ but returns very limited information about the project.
36
+
37
+ Enums:
38
+ GraphQl: Requested project data can be received with GraphQl.
39
+ REST: Requested project data can be received with /projects/{project}.
40
+ RESTList: Requested project data can be received with /projects.
41
+ Can be considered as a subset of 'REST'.
42
+ GraphQlAndREST: It is necessary to use GraphQl and REST to get all
43
+ requested data.
44
+
45
+ """
46
+ GraphQl = "GraphQl"
47
+ REST = "REST"
48
+ RESTList = "RESTList"
49
+ GraphQlAndREST = "GraphQlAndREST"
17
50
 
18
51
 
19
52
  class ProjectsAPI(BaseServerAPI):
@@ -156,12 +189,12 @@ class ProjectsAPI(BaseServerAPI):
156
189
  if project:
157
190
  yield project
158
191
 
159
- def get_project_names(
192
+ def get_rest_projects_list(
160
193
  self,
161
194
  active: Optional[bool] = True,
162
195
  library: Optional[bool] = None,
163
- ) -> list[str]:
164
- """Receive available project names.
196
+ ) -> list[ProjectListDict]:
197
+ """Receive available projects.
165
198
 
166
199
  User must be logged in.
167
200
 
@@ -172,7 +205,7 @@ class ProjectsAPI(BaseServerAPI):
172
205
  are returned if 'None' is passed.
173
206
 
174
207
  Returns:
175
- list[str]: List of available project names.
208
+ list[ProjectListDict]: List of available projects.
176
209
 
177
210
  """
178
211
  if active is not None:
@@ -181,16 +214,38 @@ class ProjectsAPI(BaseServerAPI):
181
214
  if library is not None:
182
215
  library = "true" if library else "false"
183
216
 
184
- query = prepare_query_string({"active": active, "library": library})
185
-
217
+ query = prepare_query_string({
218
+ "active": active,
219
+ "library": library,
220
+ })
186
221
  response = self.get(f"projects{query}")
187
222
  response.raise_for_status()
188
223
  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
224
+ return data["projects"]
225
+
226
+ def get_project_names(
227
+ self,
228
+ active: Optional[bool] = True,
229
+ library: Optional[bool] = None,
230
+ ) -> list[str]:
231
+ """Receive available project names.
232
+
233
+ User must be logged in.
234
+
235
+ Args:
236
+ active (Optional[bool]): Filter active/inactive projects. Both
237
+ are returned if 'None' is passed.
238
+ library (Optional[bool]): Filter standard/library projects. Both
239
+ are returned if 'None' is passed.
240
+
241
+ Returns:
242
+ list[str]: List of available project names.
243
+
244
+ """
245
+ return [
246
+ project["name"]
247
+ for project in self.get_rest_projects_list(active, library)
248
+ ]
194
249
 
195
250
  def get_projects(
196
251
  self,
@@ -218,7 +273,11 @@ class ProjectsAPI(BaseServerAPI):
218
273
  if fields is not None:
219
274
  fields = set(fields)
220
275
 
221
- graphql_fields, use_rest = self._get_project_graphql_fields(fields)
276
+ graphql_fields, fetch_type = self._get_project_graphql_fields(fields)
277
+ if fetch_type == ProjectFetchType.RESTList:
278
+ yield from self.get_rest_projects_list(active, library)
279
+ return
280
+
222
281
  projects_by_name = {}
223
282
  if graphql_fields:
224
283
  projects = list(self._get_graphql_projects(
@@ -227,7 +286,7 @@ class ProjectsAPI(BaseServerAPI):
227
286
  fields=graphql_fields,
228
287
  own_attributes=own_attributes,
229
288
  ))
230
- if not use_rest:
289
+ if fetch_type == ProjectFetchType.GraphQl:
231
290
  yield from projects
232
291
  return
233
292
  projects_by_name = {p["name"]: p for p in projects}
@@ -236,7 +295,12 @@ class ProjectsAPI(BaseServerAPI):
236
295
  name = project["name"]
237
296
  graphql_p = projects_by_name.get(name)
238
297
  if graphql_p:
239
- project["productTypes"] = graphql_p["productTypes"]
298
+ for key in (
299
+ "productTypes",
300
+ "usedTags",
301
+ ):
302
+ if key in graphql_p:
303
+ project[key] = graphql_p[key]
240
304
  yield project
241
305
 
242
306
  def get_project(
@@ -262,7 +326,7 @@ class ProjectsAPI(BaseServerAPI):
262
326
  if fields is not None:
263
327
  fields = set(fields)
264
328
 
265
- graphql_fields, use_rest = self._get_project_graphql_fields(fields)
329
+ graphql_fields, fetch_type = self._get_project_graphql_fields(fields)
266
330
  graphql_project = None
267
331
  if graphql_fields:
268
332
  graphql_project = next(self._get_graphql_projects(
@@ -271,14 +335,19 @@ class ProjectsAPI(BaseServerAPI):
271
335
  fields=graphql_fields,
272
336
  own_attributes=own_attributes,
273
337
  ), None)
274
- if not graphql_project or not use_rest:
338
+ if not graphql_project or fetch_type == fetch_type.GraphQl:
275
339
  return graphql_project
276
340
 
277
341
  project = self.get_rest_project(project_name)
278
342
  if own_attributes:
279
343
  fill_own_attribs(project)
280
344
  if graphql_project:
281
- project["productTypes"] = graphql_project["productTypes"]
345
+ for key in (
346
+ "productTypes",
347
+ "usedTags",
348
+ ):
349
+ if key in graphql_project:
350
+ project[key] = graphql_project[key]
282
351
  return project
283
352
 
284
353
  def create_project(
@@ -585,34 +654,86 @@ class ProjectsAPI(BaseServerAPI):
585
654
 
586
655
  def _get_project_graphql_fields(
587
656
  self, fields: Optional[set[str]]
588
- ) -> tuple[set[str], bool]:
589
- """Fetch of project must be done using REST endpoint.
657
+ ) -> tuple[set[str], ProjectFetchType]:
658
+ """Find out if project can be fetched with GraphQl, REST or both.
590
659
 
591
660
  Returns:
592
661
  set[str]: GraphQl fields.
593
662
 
594
663
  """
595
664
  if fields is None:
596
- return set(), True
597
-
598
- has_product_types = False
665
+ return set(), ProjectFetchType.REST
666
+
667
+ rest_list_fields = {
668
+ "name",
669
+ "code",
670
+ "active",
671
+ "createdAt",
672
+ "updatedAt",
673
+ }
599
674
  graphql_fields = set()
600
- for field in fields:
675
+ if len(fields - rest_list_fields) == 0:
676
+ return graphql_fields, ProjectFetchType.RESTList
677
+
678
+ must_use_graphql = False
679
+ for field in tuple(fields):
601
680
  # Product types are available only in GraphQl
602
- if field.startswith("productTypes"):
603
- has_product_types = True
681
+ if field == "usedTags":
682
+ graphql_fields.add("usedTags")
683
+ elif field == "productTypes":
684
+ must_use_graphql = True
685
+ fields.discard(field)
686
+ for f_name in DEFAULT_PRODUCT_TYPE_FIELDS:
687
+ fields.add(f"{field}.{f_name}")
688
+
689
+ elif field.startswith("productTypes"):
690
+ must_use_graphql = True
691
+ graphql_fields.add(field)
692
+
693
+ elif field == "productBaseTypes":
694
+ must_use_graphql = True
695
+ fields.discard(field)
696
+ for f_name in DEFAULT_PRODUCT_BASE_TYPE_FIELDS:
697
+ fields.add(f"{field}.{f_name}")
698
+
699
+ elif field.startswith("productBaseTypes"):
700
+ must_use_graphql = True
604
701
  graphql_fields.add(field)
605
702
 
606
- if not has_product_types:
607
- return set(), True
703
+ elif field == "bundle" or field == "bundles":
704
+ fields.discard(field)
705
+ graphql_fields.add("bundle.production")
706
+ graphql_fields.add("bundle.staging")
707
+
708
+ elif field.startswith("bundle"):
709
+ graphql_fields.add(field)
608
710
 
609
- inters = fields & {"name", "code", "active", "library"}
711
+ elif field == "attrib":
712
+ fields.discard("attrib")
713
+ graphql_fields |= self.get_attributes_fields_for_type(
714
+ "project"
715
+ )
716
+
717
+ # NOTE 'config' in GraphQl is NOT the same as from REST api.
718
+ # - At the moment of this comment there is missing 'productBaseTypes'.
719
+ inters = fields & {
720
+ "name",
721
+ "code",
722
+ "active",
723
+ "library",
724
+ "usedTags",
725
+ "data",
726
+ }
610
727
  remainders = fields - (inters | graphql_fields)
611
- if remainders:
728
+ if not remainders:
729
+ graphql_fields |= inters
730
+ return graphql_fields, ProjectFetchType.GraphQl
731
+
732
+ if must_use_graphql:
612
733
  graphql_fields.add("name")
613
- return graphql_fields, True
614
- graphql_fields |= inters
615
- return graphql_fields, False
734
+ return graphql_fields, ProjectFetchType.GraphQlAndREST
735
+
736
+ return set(), ProjectFetchType.REST
616
737
 
617
738
  def _fill_project_entity_data(self, project: dict[str, Any]) -> None:
618
739
  # Add fake scope to statuses if not available
@@ -632,13 +753,15 @@ class ProjectsAPI(BaseServerAPI):
632
753
  # Convert 'data' from string to dict if needed
633
754
  if "data" in project:
634
755
  project_data = project["data"]
635
- if isinstance(project_data, str):
756
+ if project_data is None:
757
+ project["data"] = {}
758
+ elif isinstance(project_data, str):
636
759
  project_data = json.loads(project_data)
637
760
  project["data"] = project_data
638
761
 
639
762
  # Fill 'bundle' from data if is not filled
640
763
  if "bundle" not in project:
641
- bundle_data = project["data"].get("bundle", {})
764
+ bundle_data = project["data"].get("bundle") or {}
642
765
  prod_bundle = bundle_data.get("production")
643
766
  staging_bundle = bundle_data.get("staging")
644
767
  project["bundle"] = {
@@ -647,9 +770,12 @@ class ProjectsAPI(BaseServerAPI):
647
770
  }
648
771
 
649
772
  # Convert 'config' from string to dict if needed
650
- config = project.get("config")
651
- if isinstance(config, str):
652
- project["config"] = json.loads(config)
773
+ if "config" in project:
774
+ config = project["config"]
775
+ if config is None:
776
+ project["config"] = {}
777
+ elif isinstance(config, str):
778
+ project["config"] = json.loads(config)
653
779
 
654
780
  # Unifiy 'linkTypes' data structure from REST and GraphQL
655
781
  if "linkTypes" in project:
@@ -82,6 +82,13 @@ DEFAULT_PRODUCT_TYPE_FIELDS = {
82
82
  "color",
83
83
  }
84
84
 
85
+ # --- Product base type ---
86
+ DEFAULT_PRODUCT_BASE_TYPE_FIELDS = {
87
+ # Ignore 'icon' and 'color'
88
+ # - current server implementation always returns 'null'
89
+ "name",
90
+ }
91
+
85
92
  # --- Project ---
86
93
  DEFAULT_PROJECT_FIELDS = {
87
94
  "active",