Ayon-Python-Api 1.0.9__tar.gz → 1.0.10__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 (23) hide show
  1. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/PKG-INFO +1 -1
  2. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/__init__.py +8 -0
  3. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/_api.py +93 -8
  4. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/entity_hub.py +113 -28
  5. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/events.py +0 -1
  6. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/graphql.py +1 -1
  7. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/graphql_queries.py +3 -1
  8. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/server_api.py +225 -51
  9. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/utils.py +50 -1
  10. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/version.py +1 -1
  11. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_python_api.egg-info/PKG-INFO +1 -1
  12. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/pyproject.toml +5 -2
  13. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/setup.py +1 -1
  14. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/LICENSE +0 -0
  15. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/README.md +0 -0
  16. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/constants.py +0 -0
  17. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/exceptions.py +0 -0
  18. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_api/operations.py +0 -0
  19. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_python_api.egg-info/SOURCES.txt +0 -0
  20. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  21. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_python_api.egg-info/requires.txt +0 -0
  22. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/ayon_python_api.egg-info/top_level.txt +0 -0
  23. {ayon-python-api-1.0.9 → ayon-python-api-1.0.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 1.0.9
3
+ Version: 1.0.10
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -7,6 +7,8 @@ from .utils import (
7
7
  is_token_valid,
8
8
  validate_url,
9
9
  login_to_server,
10
+ take_web_action_event,
11
+ abort_web_action_event,
10
12
  )
11
13
  from .server_api import (
12
14
  RequestTypes,
@@ -85,6 +87,7 @@ from ._api import (
85
87
  get_attributes_fields_for_type,
86
88
  get_default_fields_for_type,
87
89
  get_addons_info,
90
+ get_addon_endpoint,
88
91
  get_addon_url,
89
92
  download_addon_private_file,
90
93
  get_installers,
@@ -103,6 +106,7 @@ from ._api import (
103
106
  get_bundles,
104
107
  create_bundle,
105
108
  update_bundle,
109
+ check_bundle_compatibility,
106
110
  delete_bundle,
107
111
  get_project_anatomy_presets,
108
112
  get_default_anatomy_preset_name,
@@ -238,6 +242,8 @@ __all__ = (
238
242
  "is_token_valid",
239
243
  "validate_url",
240
244
  "login_to_server",
245
+ "take_web_action_event",
246
+ "abort_web_action_event",
241
247
 
242
248
  "RequestTypes",
243
249
  "ServerAPI",
@@ -313,6 +319,7 @@ __all__ = (
313
319
  "get_attributes_fields_for_type",
314
320
  "get_default_fields_for_type",
315
321
  "get_addons_info",
322
+ "get_addon_endpoint",
316
323
  "get_addon_url",
317
324
  "download_addon_private_file",
318
325
  "get_installers",
@@ -331,6 +338,7 @@ __all__ = (
331
338
  "get_bundles",
332
339
  "create_bundle",
333
340
  "update_bundle",
341
+ "check_bundle_compatibility",
334
342
  "delete_bundle",
335
343
  "get_project_anatomy_presets",
336
344
  "get_default_anatomy_preset_name",
@@ -780,11 +780,11 @@ def dispatch_event(*args, **kwargs):
780
780
  sender (Optional[str]): Sender of event.
781
781
  event_hash (Optional[str]): Event hash.
782
782
  project_name (Optional[str]): Project name.
783
- username (Optional[str]): Username which triggered event.
784
783
  depends_on (Optional[str]): Add dependency to another event.
784
+ username (Optional[str]): Username which triggered event.
785
785
  description (Optional[str]): Description of event.
786
- summary (Optional[dict[str, Any]]): Summary of event that can be used
787
- for simple filtering on listeners.
786
+ summary (Optional[dict[str, Any]]): Summary of event that can
787
+ be used for simple filtering on listeners.
788
788
  payload (Optional[dict[str, Any]]): Full payload of event data with
789
789
  all details.
790
790
  finished (Optional[bool]): Mark event as finished on dispatch.
@@ -875,7 +875,8 @@ def download_file_to_stream(*args, **kwargs):
875
875
 
876
876
  Args:
877
877
  endpoint (str): Endpoint or URL to file that should be downloaded.
878
- stream (Union[io.BytesIO, BinaryIO]): Stream where output will be stored.
878
+ stream (Union[io.BytesIO, BinaryIO]): Stream where output will
879
+ be stored.
879
880
  chunk_size (Optional[int]): Size of chunks that are received
880
881
  in single loop.
881
882
  progress (Optional[TransferProgress]): Object that gives ability
@@ -1155,6 +1156,30 @@ def get_addons_info(*args, **kwargs):
1155
1156
  return con.get_addons_info(*args, **kwargs)
1156
1157
 
1157
1158
 
1159
+ def get_addon_endpoint(*args, **kwargs):
1160
+ """Calculate endpoint to addon route.
1161
+
1162
+ Examples:
1163
+
1164
+ >>> api = ServerAPI("https://your.url.com")
1165
+ >>> api.get_addon_url(
1166
+ ... "example", "1.0.0", "private", "my.zip")
1167
+ 'addons/example/1.0.0/private/my.zip'
1168
+
1169
+ Args:
1170
+ addon_name (str): Name of addon.
1171
+ addon_version (str): Version of addon.
1172
+ *subpaths (str): Any amount of subpaths that are added to
1173
+ addon url.
1174
+
1175
+ Returns:
1176
+ str: Final url.
1177
+
1178
+ """
1179
+ con = get_server_api_connection()
1180
+ return con.get_addon_endpoint(*args, **kwargs)
1181
+
1182
+
1158
1183
  def get_addon_url(*args, **kwargs):
1159
1184
  """Calculate url to addon route.
1160
1185
 
@@ -1163,13 +1188,14 @@ def get_addon_url(*args, **kwargs):
1163
1188
  >>> api = ServerAPI("https://your.url.com")
1164
1189
  >>> api.get_addon_url(
1165
1190
  ... "example", "1.0.0", "private", "my.zip")
1166
- 'https://your.url.com/addons/example/1.0.0/private/my.zip'
1191
+ 'https://your.url.com/api/addons/example/1.0.0/private/my.zip'
1167
1192
 
1168
1193
  Args:
1169
1194
  addon_name (str): Name of addon.
1170
1195
  addon_version (str): Version of addon.
1171
1196
  *subpaths (str): Any amount of subpaths that are added to
1172
1197
  addon url.
1198
+ use_rest (Optional[bool]): Use rest endpoint.
1173
1199
 
1174
1200
  Returns:
1175
1201
  str: Final url.
@@ -1504,7 +1530,23 @@ def create_bundle(*args, **kwargs):
1504
1530
  """Create bundle on server.
1505
1531
 
1506
1532
  Bundle cannot be changed once is created. Only isProduction, isStaging
1507
- and dependency packages can change after creation.
1533
+ and dependency packages can change after creation. In case dev bundle
1534
+ is created, it is possible to change anything, but it is not possible
1535
+ to mark bundle as dev and production or staging at the same time.
1536
+
1537
+ Development addon config can define custom path to client code. It is
1538
+ used only for dev bundles.
1539
+
1540
+ Example of 'dev_addons_config'::
1541
+
1542
+ ```json
1543
+ {
1544
+ "core": {
1545
+ "enabled": true,
1546
+ "path": "/path/to/ayon-core/client"
1547
+ }
1548
+ }
1549
+ ```
1508
1550
 
1509
1551
  Args:
1510
1552
  name (str): Name of bundle.
@@ -1516,6 +1558,11 @@ def create_bundle(*args, **kwargs):
1516
1558
  is_production (Optional[bool]): Bundle will be marked as
1517
1559
  production.
1518
1560
  is_staging (Optional[bool]): Bundle will be marked as staging.
1561
+ is_dev (Optional[bool]): Bundle will be marked as dev.
1562
+ dev_active_user (Optional[str]): Username that will be assigned
1563
+ to dev bundle. Can be used only if 'is_dev' is set to 'True'.
1564
+ dev_addons_config (Optional[dict[str, Any]]): Configuration for
1565
+ dev addons. Can be used only if 'is_dev' is set to 'True'.
1519
1566
 
1520
1567
  """
1521
1568
  con = get_server_api_connection()
@@ -1531,17 +1578,55 @@ def update_bundle(*args, **kwargs):
1531
1578
 
1532
1579
  Args:
1533
1580
  bundle_name (str): Name of bundle.
1581
+ addon_versions (Optional[dict[str, str]]): Addon versions,
1582
+ possible only for dev bundles.
1583
+ installer_version (Optional[str]): Installer version, possible
1584
+ only for dev bundles.
1534
1585
  dependency_packages (Optional[dict[str, str]]): Dependency pacakge
1535
1586
  names that should be used with the bundle.
1536
1587
  is_production (Optional[bool]): Bundle will be marked as
1537
1588
  production.
1538
1589
  is_staging (Optional[bool]): Bundle will be marked as staging.
1590
+ is_dev (Optional[bool]): Bundle will be marked as dev.
1591
+ dev_active_user (Optional[str]): Username that will be assigned
1592
+ to dev bundle. Can be used only for dev bundles.
1593
+ dev_addons_config (Optional[dict[str, Any]]): Configuration for
1594
+ dev addons. Can be used only for dev bundles.
1539
1595
 
1540
1596
  """
1541
1597
  con = get_server_api_connection()
1542
1598
  return con.update_bundle(*args, **kwargs)
1543
1599
 
1544
1600
 
1601
+ def check_bundle_compatibility(*args, **kwargs):
1602
+ """Check bundle compatibility.
1603
+
1604
+ Can be used as per-flight validation before creating bundle.
1605
+
1606
+ Args:
1607
+ name (str): Name of bundle.
1608
+ addon_versions (dict[str, str]): Addon versions.
1609
+ installer_version (Union[str, None]): Installer version.
1610
+ dependency_packages (Optional[dict[str, str]]): Dependency
1611
+ package names. Keys are platform names and values are name of
1612
+ packages.
1613
+ is_production (Optional[bool]): Bundle will be marked as
1614
+ production.
1615
+ is_staging (Optional[bool]): Bundle will be marked as staging.
1616
+ is_dev (Optional[bool]): Bundle will be marked as dev.
1617
+ dev_active_user (Optional[str]): Username that will be assigned
1618
+ to dev bundle. Can be used only if 'is_dev' is set to 'True'.
1619
+ dev_addons_config (Optional[dict[str, Any]]): Configuration for
1620
+ dev addons. Can be used only if 'is_dev' is set to 'True'.
1621
+
1622
+ Returns:
1623
+ Dict[str, Any]: Server response, with 'success' and 'issues'.
1624
+
1625
+ """
1626
+ con = get_server_api_connection()
1627
+ return con.check_bundle_compatibility(*args, **kwargs)
1628
+
1629
+
1545
1630
  def delete_bundle(*args, **kwargs):
1546
1631
  """Delete bundle from server.
1547
1632
 
@@ -2367,8 +2452,8 @@ def get_folders(*args, **kwargs):
2367
2452
  """Query folders from server.
2368
2453
 
2369
2454
  Todos:
2370
- Folder name won't be unique identifier, so we should add folder path
2371
- filtering.
2455
+ Folder name won't be unique identifier, so we should add
2456
+ folder path filtering.
2372
2457
 
2373
2458
  Notes:
2374
2459
  Filter 'active' don't have direct filter in GraphQl.
@@ -2,10 +2,17 @@ import re
2
2
  import copy
3
3
  import collections
4
4
  from abc import ABC, abstractmethod
5
+ import typing
5
6
 
6
7
  from ._api import get_server_api_connection
7
8
  from .utils import create_entity_id, convert_entity_id, slugify_string
8
9
 
10
+ if typing.TYPE_CHECKING:
11
+ from typing import Literal
12
+
13
+ StatusState = Literal["not_started", "in_progress", "done", "blocked"]
14
+ EntityType = Literal["project", "folder", "task"]
15
+
9
16
 
10
17
  class _CustomNone(object):
11
18
  def __init__(self, name=None):
@@ -44,7 +51,7 @@ class EntityHub(object):
44
51
  key on entities. This is not recommended as 'data' may be use for
45
52
  secure information and would also slow down server queries. Content
46
53
  of 'data' key can't be received only GraphQl.
47
-
54
+
48
55
  """
49
56
 
50
57
  def __init__(
@@ -129,8 +136,8 @@ class EntityHub(object):
129
136
  Use attribute schema to validate values on entities.
130
137
 
131
138
  Args:
132
- entity_type (Literal["project", "folder", "task"]): Entity type
133
- for which should be attributes received.
139
+ entity_type (EntityType): Entity type for which should
140
+ be attributes received.
134
141
 
135
142
  Returns:
136
143
  Dict[str, Dict[str, Any]]: Attribute schemas that are available
@@ -259,8 +266,8 @@ class EntityHub(object):
259
266
  Args:
260
267
  folder_type (str): Type of folder. Folder type must be available in
261
268
  config of project folder types.
262
- entity_id (Union[str, None]): Id of the entity. New id is created if
263
- not passed.
269
+ entity_id (Union[str, None]): Id of the entity. New id is created
270
+ if not passed.
264
271
  parent_id (Union[str, None]): Id of parent entity.
265
272
  name (str): Name of entity.
266
273
  label (Optional[str]): Folder label.
@@ -289,8 +296,8 @@ class EntityHub(object):
289
296
  Args:
290
297
  task_type (str): Type of task. Task type must be available in
291
298
  config of project folder types.
292
- entity_id (Union[str, None]): Id of the entity. New id is created if
293
- not passed.
299
+ entity_id (Union[str, None]): Id of the entity. New id is created
300
+ if not passed.
294
301
  parent_id (Union[str, None]): Id of parent entity.
295
302
  name (str): Name of entity.
296
303
  label (Optional[str]): Folder label.
@@ -579,10 +586,16 @@ class EntityHub(object):
579
586
  raise ValueError(
580
587
  "Project \"{}\" was not found.".format(project_name)
581
588
  )
582
-
589
+ major, minor, _, _, _ = self._connection.get_server_version_tuple()
590
+ status_scope_supported = True
591
+ if (major, minor) < (1, 5):
592
+ status_scope_supported = False
583
593
  self._project_entity = ProjectEntity.from_entity_data(
584
594
  project, self
585
595
  )
596
+ self._project_entity.set_status_scope_supported(
597
+ status_scope_supported
598
+ )
586
599
 
587
600
  self.add_entity(self._project_entity)
588
601
  return self._project_entity
@@ -734,8 +747,8 @@ class EntityHub(object):
734
747
  Args:
735
748
  project_changes (dict[str, Any]): Project changes.
736
749
  orig_types (list[dict[str, Any]]): Original types.
737
- changes_key (Literal[folderTypes, taskTypes]): Key of type changes
738
- in project changes.
750
+ changes_key (Literal["folderTypes", "taskTypes"]): Key of type
751
+ changes in project changes.
739
752
  post_changes (dict[str, Any]): An object where post changes will
740
753
  be stored.
741
754
 
@@ -1247,7 +1260,7 @@ class BaseEntity(ABC):
1247
1260
  """Entity type coresponding to server.
1248
1261
 
1249
1262
  Returns:
1250
- Literal[project, folder, task]: Entity type.
1263
+ EntityType: Entity type.
1251
1264
 
1252
1265
  """
1253
1266
  pass
@@ -1473,7 +1486,7 @@ class BaseEntity(ABC):
1473
1486
 
1474
1487
  Raises:
1475
1488
  TypeError: If validation of parent does not pass.
1476
-
1489
+
1477
1490
  """
1478
1491
  parent_id = None
1479
1492
  if parent is not None:
@@ -1604,16 +1617,19 @@ class ProjectStatus:
1604
1617
  Args:
1605
1618
  name (str): Name of the status. e.g. 'In progress'
1606
1619
  short_name (Optional[str]): Short name of the status. e.g. 'IP'
1607
- state (Optional[Literal[not_started, in_progress, done, blocked]]): A
1608
- state of the status.
1620
+ state (Optional[StatusState]): A state of the status.
1609
1621
  icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
1610
1622
  color (Optional[str]): Color of the status. e.g. '#eeeeee'.
1623
+ scope (Optional[Iterable[str]]): Scope of the status. e.g. ['folder'].
1611
1624
  index (Optional[int]): Index of the status.
1612
1625
  project_statuses (Optional[_ProjectStatuses]): Project statuses
1613
1626
  wrapper.
1614
1627
 
1615
1628
  """
1616
- valid_states = ("not_started", "in_progress", "done", "blocked")
1629
+ valid_states = {"not_started", "in_progress", "done", "blocked"}
1630
+ valid_scope = {
1631
+ "folder", "task", "product", "version", "representation", "workfile"
1632
+ }
1617
1633
  color_regex = re.compile(r"#([a-f0-9]{6})$")
1618
1634
  default_state = "in_progress"
1619
1635
  default_color = "#eeeeee"
@@ -1625,6 +1641,7 @@ class ProjectStatus:
1625
1641
  state=None,
1626
1642
  icon=None,
1627
1643
  color=None,
1644
+ scope=None,
1628
1645
  index=None,
1629
1646
  project_statuses=None,
1630
1647
  is_new=None,
@@ -1633,12 +1650,16 @@ class ProjectStatus:
1633
1650
  icon = icon or ""
1634
1651
  state = state or self.default_state
1635
1652
  color = color or self.default_color
1653
+ if scope is None:
1654
+ scope = self.valid_scope
1655
+ scope = set(scope)
1636
1656
  self._name = name
1637
1657
  self._short_name = short_name
1638
1658
  self._icon = icon
1639
1659
  self._slugified_name = None
1640
1660
  self._state = None
1641
1661
  self._color = None
1662
+ self._scope = scope
1642
1663
  self.set_state(state)
1643
1664
  self.set_color(color)
1644
1665
 
@@ -1647,6 +1668,7 @@ class ProjectStatus:
1647
1668
  self._original_icon = icon
1648
1669
  self._original_state = state
1649
1670
  self._original_color = color
1671
+ self._original_scope = set(scope)
1650
1672
  self._original_index = index
1651
1673
 
1652
1674
  self._index = index
@@ -1690,8 +1712,14 @@ class ProjectStatus:
1690
1712
  self._original_icon = self.icon
1691
1713
  self._original_state = self.state
1692
1714
  self._original_color = self.color
1715
+ self._original_scope = self.scope
1693
1716
  self._original_index = self.index
1694
1717
 
1718
+ def is_available_for_entity_type(self, entity_type):
1719
+ if self._scope is None:
1720
+ return True
1721
+ return entity_type in self._scope
1722
+
1695
1723
  @staticmethod
1696
1724
  def slugify_name(name):
1697
1725
  """Slugify status name for name comparison.
@@ -1750,6 +1778,7 @@ class ProjectStatus:
1750
1778
  or self._original_state != self._state
1751
1779
  or self._original_icon != self._icon
1752
1780
  or self._original_color != self._color
1781
+ or self._original_scope != self._scope
1753
1782
  )
1754
1783
 
1755
1784
  def delete(self):
@@ -1864,8 +1893,7 @@ class ProjectStatus:
1864
1893
  """Get state of project status.
1865
1894
 
1866
1895
  Return:
1867
- Literal[not_started, in_progress, done, blocked]: General
1868
- state of status.
1896
+ StatusState: General state of status.
1869
1897
 
1870
1898
  """
1871
1899
  return self._state
@@ -1874,8 +1902,7 @@ class ProjectStatus:
1874
1902
  """Set color of project status.
1875
1903
 
1876
1904
  Args:
1877
- state (Literal[not_started, in_progress, done, blocked]): General
1878
- state of status.
1905
+ state (StatusState): General state of status.
1879
1906
 
1880
1907
  """
1881
1908
  if state not in self.valid_states:
@@ -1906,6 +1933,36 @@ class ProjectStatus:
1906
1933
  raise ValueError("Invalid color value '{}'".format(color))
1907
1934
  self._color = color
1908
1935
 
1936
+ def get_scope(self):
1937
+ """Get scope of the status.
1938
+
1939
+ Returns:
1940
+ Set[str]: Scope of the status.
1941
+
1942
+ """
1943
+ return set(self._scope)
1944
+
1945
+ def set_scope(self, scope):
1946
+ """Get scope of the status.
1947
+
1948
+ Returns:
1949
+ scope (Iterable[str]): Scope of the status.
1950
+
1951
+ """
1952
+ if not isinstance(scope, (list, set, tuple)):
1953
+ raise TypeError(
1954
+ f"Scope must be a list, set, tuple. Got '{type(scope)}'."
1955
+ )
1956
+
1957
+ scope = set(scope)
1958
+ invalid_entity_types = scope - self.valid_scope
1959
+ if invalid_entity_types:
1960
+ raise ValueError("Invalid scope values '{}'".format(
1961
+ ", ".join(invalid_entity_types)
1962
+ ))
1963
+
1964
+ self._scope = scope
1965
+
1909
1966
  name = property(get_name, set_name)
1910
1967
  short_name = property(get_short_name, set_short_name)
1911
1968
  project_statuses = property(get_project_statuses, set_project_statuses)
@@ -1913,6 +1970,7 @@ class ProjectStatus:
1913
1970
  state = property(get_state, set_state)
1914
1971
  color = property(get_color, set_color)
1915
1972
  icon = property(get_icon, set_icon)
1973
+ scope = property(get_scope, set_scope)
1916
1974
 
1917
1975
  def _validate_other_p_statuses(self, other):
1918
1976
  """Validate if other status can be used for move.
@@ -1977,6 +2035,7 @@ class ProjectStatus:
1977
2035
  "state": self.state,
1978
2036
  "icon": self.icon,
1979
2037
  "color": self.color,
2038
+ "scope": list(self._scope),
1980
2039
  }
1981
2040
  if (
1982
2041
  not self._is_new
@@ -2003,6 +2062,7 @@ class ProjectStatus:
2003
2062
  data.get("state"),
2004
2063
  data.get("icon"),
2005
2064
  data.get("color"),
2065
+ data.get("scope"),
2006
2066
  index=index,
2007
2067
  project_statuses=project_statuses
2008
2068
  )
@@ -2026,6 +2086,7 @@ class _ProjectStatuses:
2026
2086
  ProjectStatus.from_data(status, idx, self)
2027
2087
  for idx, status in enumerate(statuses)
2028
2088
  ]
2089
+ self._scope_supported = False
2029
2090
  self._orig_status_length = len(self._statuses)
2030
2091
  self._set_called = False
2031
2092
 
@@ -2049,27 +2110,31 @@ class _ProjectStatuses:
2049
2110
  state=None,
2050
2111
  icon=None,
2051
2112
  color=None,
2113
+ scope=None,
2052
2114
  ):
2053
2115
  """Create project status.
2054
2116
 
2055
2117
  Args:
2056
2118
  name (str): Name of the status. e.g. 'In progress'
2057
2119
  short_name (Optional[str]): Short name of the status. e.g. 'IP'
2058
- state (Optional[Literal[not_started, in_progress, done, blocked]]): A
2059
- state of the status.
2120
+ state (Optional[StatusState]): A state of the status.
2060
2121
  icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
2061
2122
  color (Optional[str]): Color of the status. e.g. '#eeeeee'.
2123
+ scope (Optional[List[str]]): Scope of the status. e.g. ['folder'].
2062
2124
 
2063
2125
  Returns:
2064
2126
  ProjectStatus: Created project status.
2065
2127
 
2066
2128
  """
2067
2129
  status = ProjectStatus(
2068
- name, short_name, state, icon, color, is_new=True
2130
+ name, short_name, state, icon, color, scope, is_new=True
2069
2131
  )
2070
2132
  self.append(status)
2071
2133
  return status
2072
2134
 
2135
+ def set_status_scope_supported(self, supported: bool):
2136
+ self._scope_supported = supported
2137
+
2073
2138
  def lock(self):
2074
2139
  """Lock statuses.
2075
2140
 
@@ -2083,10 +2148,15 @@ class _ProjectStatuses:
2083
2148
 
2084
2149
  def to_data(self):
2085
2150
  """Convert to project statuses data."""
2086
- return [
2151
+ output = [
2087
2152
  status.to_data()
2088
2153
  for status in self._statuses
2089
2154
  ]
2155
+ # Remove scope if is not supported
2156
+ if not self._scope_supported:
2157
+ for item in output:
2158
+ item.pop("scope")
2159
+ return output
2090
2160
 
2091
2161
  def set(self, statuses):
2092
2162
  """Explicitly override statuses.
@@ -2348,9 +2418,9 @@ class ProjectEntity(BaseEntity):
2348
2418
  task_types,
2349
2419
  statuses,
2350
2420
  *args,
2351
- **kwargs
2421
+ **kwargs,
2352
2422
  ):
2353
- super(ProjectEntity, self).__init__(*args, **kwargs)
2423
+ super().__init__(*args, **kwargs)
2354
2424
 
2355
2425
  self._project_code = project_code
2356
2426
  self._library_project = library
@@ -2379,6 +2449,9 @@ class ProjectEntity(BaseEntity):
2379
2449
  "Parent of project cannot be set to {}".format(parent)
2380
2450
  )
2381
2451
 
2452
+ def set_status_scope_supported(self, supported: bool):
2453
+ self._statuses_obj.set_status_scope_supported(supported)
2454
+
2382
2455
  parent = property(get_parent, set_parent)
2383
2456
 
2384
2457
  def get_orig_folder_types(self):
@@ -2436,7 +2509,7 @@ class ProjectEntity(BaseEntity):
2436
2509
  return self._statuses_obj.get_status_by_slugified_name(name)
2437
2510
 
2438
2511
  def lock(self):
2439
- super(ProjectEntity, self).lock()
2512
+ super().lock()
2440
2513
  self._orig_folder_types = copy.deepcopy(self._folder_types)
2441
2514
  self._orig_task_types = copy.deepcopy(self._task_types)
2442
2515
  self._statuses_obj.lock()
@@ -2469,7 +2542,7 @@ class ProjectEntity(BaseEntity):
2469
2542
  attribs=project["ownAttrib"],
2470
2543
  data=project["data"],
2471
2544
  active=project["active"],
2472
- entity_hub=entity_hub
2545
+ entity_hub=entity_hub,
2473
2546
  )
2474
2547
 
2475
2548
  def to_create_body_data(self):
@@ -2577,6 +2650,12 @@ class FolderEntity(BaseEntity):
2577
2650
  raise ValueError(
2578
2651
  f"Status {status_name} is not available on project."
2579
2652
  )
2653
+
2654
+ if not status.is_available_for_entity_type("folder"):
2655
+ raise ValueError(
2656
+ f"Status {status_name} is not available for folder."
2657
+ )
2658
+
2580
2659
  self._status = status_name
2581
2660
 
2582
2661
  status = property(get_status, set_status)
@@ -2795,7 +2874,7 @@ class TaskEntity(BaseEntity):
2795
2874
  tags = []
2796
2875
  else:
2797
2876
  tags = list(tags)
2798
-
2877
+
2799
2878
  if assignees is None:
2800
2879
  assignees = []
2801
2880
  else:
@@ -2861,6 +2940,12 @@ class TaskEntity(BaseEntity):
2861
2940
  raise ValueError(
2862
2941
  f"Status {status_name} is not available on project."
2863
2942
  )
2943
+
2944
+ if not status.is_available_for_entity_type("task"):
2945
+ raise ValueError(
2946
+ f"Status {status_name} is not available for task."
2947
+ )
2948
+
2864
2949
  self._status = status_name
2865
2950
 
2866
2951
  status = property(get_status, set_status)
@@ -44,7 +44,6 @@ class ServerEvent(object):
44
44
  "user": self.username,
45
45
  "dependencies": copy.deepcopy(self.dependencies),
46
46
  "description": self.description,
47
- "description": self.description,
48
47
  "summary": copy.deepcopy(self.summary),
49
48
  "payload": self.payload,
50
49
  "finished": self.finished,
@@ -72,7 +72,7 @@ class GraphQlQuery:
72
72
 
73
73
  Args:
74
74
  name (str): Name of query.
75
-
75
+
76
76
  """
77
77
  offset = 2
78
78
 
@@ -389,7 +389,9 @@ def representations_graphql_query(fields):
389
389
  repre_ids_var = query.add_variable("representationIds", "[String!]")
390
390
  repre_names_var = query.add_variable("representationNames", "[String!]")
391
391
  version_ids_var = query.add_variable("versionIds", "[String!]")
392
- has_links_var = query.add_variable("representationHasLinks", "HasLinksFilter")
392
+ has_links_var = query.add_variable(
393
+ "representationHasLinks", "HasLinksFilter"
394
+ )
393
395
  statuses_var = query.add_variable(
394
396
  "representationStatuses", "[String!]"
395
397
  )
@@ -16,6 +16,7 @@ import uuid
16
16
  import warnings
17
17
  import itertools
18
18
  from contextlib import contextmanager
19
+ from typing import Optional
19
20
 
20
21
  try:
21
22
  from http import HTTPStatus
@@ -1276,7 +1277,11 @@ class ServerAPI(object):
1276
1277
  # Server may be restarting
1277
1278
  new_response = RestApiResponse(
1278
1279
  None,
1279
- {"detail": "Unable to connect the server. Connection refused"}
1280
+ {
1281
+ "detail": (
1282
+ "Unable to connect the server. Connection refused"
1283
+ )
1284
+ }
1280
1285
  )
1281
1286
 
1282
1287
  except requests.exceptions.Timeout:
@@ -1295,7 +1300,11 @@ class ServerAPI(object):
1295
1300
 
1296
1301
  new_response = RestApiResponse(
1297
1302
  None,
1298
- {"detail": "Unable to connect the server. Connection error"}
1303
+ {
1304
+ "detail": (
1305
+ "Unable to connect the server. Connection error"
1306
+ )
1307
+ }
1299
1308
  )
1300
1309
 
1301
1310
  time.sleep(0.1)
@@ -1323,9 +1332,8 @@ class ServerAPI(object):
1323
1332
  return new_response
1324
1333
 
1325
1334
  def raw_post(self, entrypoint, **kwargs):
1326
- entrypoint = entrypoint.lstrip("/").rstrip("/")
1327
- self.log.debug("Executing [POST] {}".format(entrypoint))
1328
- url = "{}/{}".format(self._rest_url, entrypoint)
1335
+ url = self._endpoint_to_url(entrypoint)
1336
+ self.log.debug("Executing [POST] {}".format(url))
1329
1337
  return self._do_rest_request(
1330
1338
  RequestTypes.post,
1331
1339
  url,
@@ -1333,9 +1341,8 @@ class ServerAPI(object):
1333
1341
  )
1334
1342
 
1335
1343
  def raw_put(self, entrypoint, **kwargs):
1336
- entrypoint = entrypoint.lstrip("/").rstrip("/")
1337
- self.log.debug("Executing [PUT] {}".format(entrypoint))
1338
- url = "{}/{}".format(self._rest_url, entrypoint)
1344
+ url = self._endpoint_to_url(entrypoint)
1345
+ self.log.debug("Executing [PUT] {}".format(url))
1339
1346
  return self._do_rest_request(
1340
1347
  RequestTypes.put,
1341
1348
  url,
@@ -1343,9 +1350,8 @@ class ServerAPI(object):
1343
1350
  )
1344
1351
 
1345
1352
  def raw_patch(self, entrypoint, **kwargs):
1346
- entrypoint = entrypoint.lstrip("/").rstrip("/")
1347
- self.log.debug("Executing [PATCH] {}".format(entrypoint))
1348
- url = "{}/{}".format(self._rest_url, entrypoint)
1353
+ url = self._endpoint_to_url(entrypoint)
1354
+ self.log.debug("Executing [PATCH] {}".format(url))
1349
1355
  return self._do_rest_request(
1350
1356
  RequestTypes.patch,
1351
1357
  url,
@@ -1353,9 +1359,8 @@ class ServerAPI(object):
1353
1359
  )
1354
1360
 
1355
1361
  def raw_get(self, entrypoint, **kwargs):
1356
- entrypoint = entrypoint.lstrip("/").rstrip("/")
1357
- self.log.debug("Executing [GET] {}".format(entrypoint))
1358
- url = "{}/{}".format(self._rest_url, entrypoint)
1362
+ url = self._endpoint_to_url(entrypoint)
1363
+ self.log.debug("Executing [GET] {}".format(url))
1359
1364
  return self._do_rest_request(
1360
1365
  RequestTypes.get,
1361
1366
  url,
@@ -1363,9 +1368,8 @@ class ServerAPI(object):
1363
1368
  )
1364
1369
 
1365
1370
  def raw_delete(self, entrypoint, **kwargs):
1366
- entrypoint = entrypoint.lstrip("/").rstrip("/")
1367
- self.log.debug("Executing [DELETE] {}".format(entrypoint))
1368
- url = "{}/{}".format(self._rest_url, entrypoint)
1371
+ url = self._endpoint_to_url(entrypoint)
1372
+ self.log.debug("Executing [DELETE] {}".format(url))
1369
1373
  return self._do_rest_request(
1370
1374
  RequestTypes.delete,
1371
1375
  url,
@@ -1569,8 +1573,8 @@ class ServerAPI(object):
1569
1573
  depends_on (Optional[str]): Add dependency to another event.
1570
1574
  username (Optional[str]): Username which triggered event.
1571
1575
  description (Optional[str]): Description of event.
1572
- summary (Optional[dict[str, Any]]): Summary of event that can be used
1573
- for simple filtering on listeners.
1576
+ summary (Optional[dict[str, Any]]): Summary of event that can
1577
+ be used for simple filtering on listeners.
1574
1578
  payload (Optional[dict[str, Any]]): Full payload of event data with
1575
1579
  all details.
1576
1580
  finished (Optional[bool]): Mark event as finished on dispatch.
@@ -1637,7 +1641,7 @@ class ServerAPI(object):
1637
1641
  at least one unfinished event with target topic, when set to 'True'.
1638
1642
  This helps when order of events matter and more than one process using
1639
1643
  the same target is running at the same time.
1640
-
1644
+
1641
1645
  Make sure the new event has updated status to '"finished"' status
1642
1646
  when you're done with logic
1643
1647
 
@@ -1708,6 +1712,30 @@ class ServerAPI(object):
1708
1712
 
1709
1713
  return response.data
1710
1714
 
1715
+ def _endpoint_to_url(
1716
+ self,
1717
+ endpoint: str,
1718
+ use_rest: Optional[bool] = True
1719
+ ):
1720
+ """Cleanup endpoint and return full url to AYON server.
1721
+
1722
+ If endpoint already starts with server url only slashes are removed.
1723
+
1724
+ Args:
1725
+ endpoint (str): Endpoint to be cleaned.
1726
+ use_rest (Optional[bool]): Use only base server url if set to
1727
+ False, otherwise REST endpoint is used.
1728
+
1729
+ Returns:
1730
+ str: Full url to AYON server.
1731
+
1732
+ """
1733
+ endpoint = endpoint.lstrip("/").rstrip("/")
1734
+ if endpoint.startswith(self._base_url):
1735
+ return endpoint
1736
+ base_url = self._rest_url if use_rest else self._graphql_url
1737
+ return f"{base_url}/{endpoint}"
1738
+
1711
1739
  def _download_file_to_stream(self, url, stream, chunk_size, progress):
1712
1740
  kwargs = {"stream": True}
1713
1741
  if self._session is None:
@@ -1740,7 +1768,8 @@ class ServerAPI(object):
1740
1768
 
1741
1769
  Args:
1742
1770
  endpoint (str): Endpoint or URL to file that should be downloaded.
1743
- stream (Union[io.BytesIO, BinaryIO]): Stream where output will be stored.
1771
+ stream (Union[io.BytesIO, BinaryIO]): Stream where output will
1772
+ be stored.
1744
1773
  chunk_size (Optional[int]): Size of chunks that are received
1745
1774
  in single loop.
1746
1775
  progress (Optional[TransferProgress]): Object that gives ability
@@ -1752,9 +1781,8 @@ class ServerAPI(object):
1752
1781
 
1753
1782
  if endpoint.startswith(self._base_url):
1754
1783
  url = endpoint
1755
- else:
1756
- endpoint = endpoint.lstrip("/").rstrip("/")
1757
- url = "{}/{}".format(self._rest_url, endpoint)
1784
+
1785
+ url = self._endpoint_to_url(endpoint)
1758
1786
 
1759
1787
  if progress is None:
1760
1788
  progress = TransferProgress()
@@ -1832,7 +1860,7 @@ class ServerAPI(object):
1832
1860
 
1833
1861
  Yields:
1834
1862
  bytes: Chunk of file.
1835
-
1863
+
1836
1864
  """
1837
1865
  # Get size of file
1838
1866
  file_stream.seek(0, io.SEEK_END)
@@ -1922,11 +1950,7 @@ class ServerAPI(object):
1922
1950
  requests.Response: Response object
1923
1951
 
1924
1952
  """
1925
- if endpoint.startswith(self._base_url):
1926
- url = endpoint
1927
- else:
1928
- endpoint = endpoint.lstrip("/").rstrip("/")
1929
- url = "{}/{}".format(self._rest_url, endpoint)
1953
+ url = self._endpoint_to_url(endpoint)
1930
1954
 
1931
1955
  # Create dummy object so the function does not have to check
1932
1956
  # 'progress' variable everywhere
@@ -1969,7 +1993,7 @@ class ServerAPI(object):
1969
1993
 
1970
1994
  Returns:
1971
1995
  requests.Response: Response object
1972
-
1996
+
1973
1997
  """
1974
1998
  if progress is None:
1975
1999
  progress = TransferProgress()
@@ -2189,7 +2213,7 @@ class ServerAPI(object):
2189
2213
  """Get attribute schemas available for an entity type.
2190
2214
 
2191
2215
  Example::
2192
-
2216
+
2193
2217
  ```
2194
2218
  # Example attribute schema
2195
2219
  {
@@ -2337,15 +2361,15 @@ class ServerAPI(object):
2337
2361
  response.raise_for_status()
2338
2362
  return response.data
2339
2363
 
2340
- def get_addon_url(self, addon_name, addon_version, *subpaths):
2341
- """Calculate url to addon route.
2364
+ def get_addon_endpoint(self, addon_name, addon_version, *subpaths):
2365
+ """Calculate endpoint to addon route.
2342
2366
 
2343
2367
  Examples:
2344
2368
 
2345
2369
  >>> api = ServerAPI("https://your.url.com")
2346
2370
  >>> api.get_addon_url(
2347
2371
  ... "example", "1.0.0", "private", "my.zip")
2348
- 'https://your.url.com/addons/example/1.0.0/private/my.zip'
2372
+ 'addons/example/1.0.0/private/my.zip'
2349
2373
 
2350
2374
  Args:
2351
2375
  addon_name (str): Name of addon.
@@ -2360,13 +2384,41 @@ class ServerAPI(object):
2360
2384
  ending = ""
2361
2385
  if subpaths:
2362
2386
  ending = "/{}".format("/".join(subpaths))
2363
- return "{}/addons/{}/{}{}".format(
2364
- self._base_url,
2387
+ return "addons/{}/{}{}".format(
2365
2388
  addon_name,
2366
2389
  addon_version,
2367
2390
  ending
2368
2391
  )
2369
2392
 
2393
+ def get_addon_url(
2394
+ self, addon_name, addon_version, *subpaths, use_rest=True
2395
+ ):
2396
+ """Calculate url to addon route.
2397
+
2398
+ Examples:
2399
+
2400
+ >>> api = ServerAPI("https://your.url.com")
2401
+ >>> api.get_addon_url(
2402
+ ... "example", "1.0.0", "private", "my.zip")
2403
+ 'https://your.url.com/api/addons/example/1.0.0/private/my.zip'
2404
+
2405
+ Args:
2406
+ addon_name (str): Name of addon.
2407
+ addon_version (str): Version of addon.
2408
+ *subpaths (str): Any amount of subpaths that are added to
2409
+ addon url.
2410
+ use_rest (Optional[bool]): Use rest endpoint.
2411
+
2412
+ Returns:
2413
+ str: Final url.
2414
+
2415
+ """
2416
+ endpoint = self.get_addon_endpoint(
2417
+ addon_name, addon_version, *subpaths
2418
+ )
2419
+ url_base = self._base_url if use_rest else self._rest_url
2420
+ return f"{url_base}/{endpoint}"
2421
+
2370
2422
  def download_addon_private_file(
2371
2423
  self,
2372
2424
  addon_name,
@@ -2404,12 +2456,13 @@ class ServerAPI(object):
2404
2456
  dst_dirpath = os.path.dirname(dst_filepath)
2405
2457
  os.makedirs(dst_dirpath, exist_ok=True)
2406
2458
 
2407
- url = self.get_addon_url(
2459
+ endpoint = self.get_addon_endpoint(
2408
2460
  addon_name,
2409
2461
  addon_version,
2410
2462
  "private",
2411
2463
  filename
2412
2464
  )
2465
+ url = f"{self._base_url}/{endpoint}"
2413
2466
  self.download_file(
2414
2467
  url, dst_filepath, chunk_size=chunk_size, progress=progress
2415
2468
  )
@@ -2811,7 +2864,7 @@ class ServerAPI(object):
2811
2864
  """Server bundles with basic information.
2812
2865
 
2813
2866
  This is example output::
2814
-
2867
+
2815
2868
  {
2816
2869
  "bundles": [
2817
2870
  {
@@ -2849,12 +2902,31 @@ class ServerAPI(object):
2849
2902
  installer_version,
2850
2903
  dependency_packages=None,
2851
2904
  is_production=None,
2852
- is_staging=None
2905
+ is_staging=None,
2906
+ is_dev=None,
2907
+ dev_active_user=None,
2908
+ dev_addons_config=None,
2853
2909
  ):
2854
2910
  """Create bundle on server.
2855
2911
 
2856
2912
  Bundle cannot be changed once is created. Only isProduction, isStaging
2857
- and dependency packages can change after creation.
2913
+ and dependency packages can change after creation. In case dev bundle
2914
+ is created, it is possible to change anything, but it is not possible
2915
+ to mark bundle as dev and production or staging at the same time.
2916
+
2917
+ Development addon config can define custom path to client code. It is
2918
+ used only for dev bundles.
2919
+
2920
+ Example of 'dev_addons_config'::
2921
+
2922
+ ```json
2923
+ {
2924
+ "core": {
2925
+ "enabled": true,
2926
+ "path": "/path/to/ayon-core/client"
2927
+ }
2928
+ }
2929
+ ```
2858
2930
 
2859
2931
  Args:
2860
2932
  name (str): Name of bundle.
@@ -2866,6 +2938,11 @@ class ServerAPI(object):
2866
2938
  is_production (Optional[bool]): Bundle will be marked as
2867
2939
  production.
2868
2940
  is_staging (Optional[bool]): Bundle will be marked as staging.
2941
+ is_dev (Optional[bool]): Bundle will be marked as dev.
2942
+ dev_active_user (Optional[str]): Username that will be assigned
2943
+ to dev bundle. Can be used only if 'is_dev' is set to 'True'.
2944
+ dev_addons_config (Optional[dict[str, Any]]): Configuration for
2945
+ dev addons. Can be used only if 'is_dev' is set to 'True'.
2869
2946
 
2870
2947
  """
2871
2948
  body = {
@@ -2873,10 +2950,14 @@ class ServerAPI(object):
2873
2950
  "installerVersion": installer_version,
2874
2951
  "addons": addon_versions,
2875
2952
  }
2953
+
2876
2954
  for key, value in (
2877
2955
  ("dependencyPackages", dependency_packages),
2878
2956
  ("isProduction", is_production),
2879
2957
  ("isStaging", is_staging),
2958
+ ("isDev", is_dev),
2959
+ ("activeUser", dev_active_user),
2960
+ ("addonDevelopment", dev_addons_config),
2880
2961
  ):
2881
2962
  if value is not None:
2882
2963
  body[key] = value
@@ -2887,9 +2968,14 @@ class ServerAPI(object):
2887
2968
  def update_bundle(
2888
2969
  self,
2889
2970
  bundle_name,
2971
+ addon_versions=None,
2972
+ installer_version=None,
2890
2973
  dependency_packages=None,
2891
2974
  is_production=None,
2892
- is_staging=None
2975
+ is_staging=None,
2976
+ is_dev=None,
2977
+ dev_active_user=None,
2978
+ dev_addons_config=None,
2893
2979
  ):
2894
2980
  """Update bundle on server.
2895
2981
 
@@ -2899,19 +2985,33 @@ class ServerAPI(object):
2899
2985
 
2900
2986
  Args:
2901
2987
  bundle_name (str): Name of bundle.
2988
+ addon_versions (Optional[dict[str, str]]): Addon versions,
2989
+ possible only for dev bundles.
2990
+ installer_version (Optional[str]): Installer version, possible
2991
+ only for dev bundles.
2902
2992
  dependency_packages (Optional[dict[str, str]]): Dependency pacakge
2903
2993
  names that should be used with the bundle.
2904
2994
  is_production (Optional[bool]): Bundle will be marked as
2905
2995
  production.
2906
2996
  is_staging (Optional[bool]): Bundle will be marked as staging.
2997
+ is_dev (Optional[bool]): Bundle will be marked as dev.
2998
+ dev_active_user (Optional[str]): Username that will be assigned
2999
+ to dev bundle. Can be used only for dev bundles.
3000
+ dev_addons_config (Optional[dict[str, Any]]): Configuration for
3001
+ dev addons. Can be used only for dev bundles.
2907
3002
 
2908
3003
  """
2909
3004
  body = {
2910
3005
  key: value
2911
3006
  for key, value in (
3007
+ ("installerVersion", installer_version),
3008
+ ("addons", addon_versions),
2912
3009
  ("dependencyPackages", dependency_packages),
2913
3010
  ("isProduction", is_production),
2914
3011
  ("isStaging", is_staging),
3012
+ ("isDev", is_dev),
3013
+ ("activeUser", dev_active_user),
3014
+ ("addonDevelopment", dev_addons_config),
2915
3015
  )
2916
3016
  if value is not None
2917
3017
  }
@@ -2921,6 +3021,63 @@ class ServerAPI(object):
2921
3021
  )
2922
3022
  response.raise_for_status()
2923
3023
 
3024
+ def check_bundle_compatibility(
3025
+ self,
3026
+ name,
3027
+ addon_versions,
3028
+ installer_version,
3029
+ dependency_packages=None,
3030
+ is_production=None,
3031
+ is_staging=None,
3032
+ is_dev=None,
3033
+ dev_active_user=None,
3034
+ dev_addons_config=None,
3035
+ ):
3036
+ """Check bundle compatibility.
3037
+
3038
+ Can be used as per-flight validation before creating bundle.
3039
+
3040
+ Args:
3041
+ name (str): Name of bundle.
3042
+ addon_versions (dict[str, str]): Addon versions.
3043
+ installer_version (Union[str, None]): Installer version.
3044
+ dependency_packages (Optional[dict[str, str]]): Dependency
3045
+ package names. Keys are platform names and values are name of
3046
+ packages.
3047
+ is_production (Optional[bool]): Bundle will be marked as
3048
+ production.
3049
+ is_staging (Optional[bool]): Bundle will be marked as staging.
3050
+ is_dev (Optional[bool]): Bundle will be marked as dev.
3051
+ dev_active_user (Optional[str]): Username that will be assigned
3052
+ to dev bundle. Can be used only if 'is_dev' is set to 'True'.
3053
+ dev_addons_config (Optional[dict[str, Any]]): Configuration for
3054
+ dev addons. Can be used only if 'is_dev' is set to 'True'.
3055
+
3056
+ Returns:
3057
+ Dict[str, Any]: Server response, with 'success' and 'issues'.
3058
+
3059
+ """
3060
+ body = {
3061
+ "name": name,
3062
+ "installerVersion": installer_version,
3063
+ "addons": addon_versions,
3064
+ }
3065
+
3066
+ for key, value in (
3067
+ ("dependencyPackages", dependency_packages),
3068
+ ("isProduction", is_production),
3069
+ ("isStaging", is_staging),
3070
+ ("isDev", is_dev),
3071
+ ("activeUser", dev_active_user),
3072
+ ("addonDevelopment", dev_addons_config),
3073
+ ):
3074
+ if value is not None:
3075
+ body[key] = value
3076
+
3077
+ response = self.post("bundles/check", **body)
3078
+ response.raise_for_status()
3079
+ return response.data
3080
+
2924
3081
  def delete_bundle(self, bundle_name):
2925
3082
  """Delete bundle from server.
2926
3083
 
@@ -3222,7 +3379,7 @@ class ServerAPI(object):
3222
3379
  if project_name:
3223
3380
  args = (project_name, )
3224
3381
 
3225
- endpoint = self.get_addon_url(
3382
+ endpoint = self.get_addon_endpoint(
3226
3383
  addon_name, addon_version, "schema", *args
3227
3384
  )
3228
3385
  result = self.get(endpoint)
@@ -3726,9 +3883,23 @@ class ServerAPI(object):
3726
3883
  return None
3727
3884
 
3728
3885
  response = self.get("projects/{}".format(project_name))
3729
- if response.status == 200:
3730
- return response.data
3731
- return None
3886
+ # TODO ignore only error about not existing project
3887
+ if response.status != 200:
3888
+ return None
3889
+ project = response.data
3890
+ # Add fake scope to statuses if not available
3891
+ for status in project["statuses"]:
3892
+ scope = status.get("scope")
3893
+ if scope is None:
3894
+ status["scope"] = [
3895
+ "folder",
3896
+ "task",
3897
+ "product",
3898
+ "version",
3899
+ "representation",
3900
+ "workfile"
3901
+ ]
3902
+ return project
3732
3903
 
3733
3904
  def get_rest_projects(self, active=True, library=None):
3734
3905
  """Query available project entities.
@@ -4144,8 +4315,8 @@ class ServerAPI(object):
4144
4315
  """Query folders from server.
4145
4316
 
4146
4317
  Todos:
4147
- Folder name won't be unique identifier, so we should add folder path
4148
- filtering.
4318
+ Folder name won't be unique identifier, so we should add
4319
+ folder path filtering.
4149
4320
 
4150
4321
  Notes:
4151
4322
  Filter 'active' don't have direct filter in GraphQl.
@@ -6504,6 +6675,8 @@ class ServerAPI(object):
6504
6675
  product = version.pop("product", {})
6505
6676
  task = version.pop("task", None)
6506
6677
  folder = product.pop("folder", {})
6678
+ self._convert_entity_data(repre)
6679
+ self._representation_conversion(repre)
6507
6680
  self._convert_entity_data(version)
6508
6681
  self._convert_entity_data(product)
6509
6682
  self._convert_entity_data(folder)
@@ -6609,7 +6782,7 @@ class ServerAPI(object):
6609
6782
 
6610
6783
  def get_representation_parents(
6611
6784
  self,
6612
- project_name,
6785
+ project_name,
6613
6786
  representation_id,
6614
6787
  project_fields=None,
6615
6788
  folder_fields=None,
@@ -6945,7 +7118,7 @@ class ServerAPI(object):
6945
7118
  "attrib.{}".format(attr)
6946
7119
  for attr in self.get_attributes_for_type("workfile")
6947
7120
  }
6948
-
7121
+
6949
7122
  if own_attributes is not _PLACEHOLDER:
6950
7123
  warnings.warn(
6951
7124
  (
@@ -6963,6 +7136,7 @@ class ServerAPI(object):
6963
7136
 
6964
7137
  for parsed_data in query.continuous_query(self):
6965
7138
  for workfile_info in parsed_data["project"]["workfiles"]:
7139
+ self._convert_entity_data(workfile_info)
6966
7140
  yield workfile_info
6967
7141
 
6968
7142
  def get_workfile_info(
@@ -8122,7 +8296,7 @@ class ServerAPI(object):
8122
8296
  body = json.loads(
8123
8297
  json.dumps(operation, default=entity_data_json_default)
8124
8298
  )
8125
- except:
8299
+ except (TypeError, ValueError):
8126
8300
  raise ValueError("Couldn't json parse body: {}".format(
8127
8301
  json.dumps(
8128
8302
  operation, indent=4, default=failed_json_default
@@ -6,7 +6,7 @@ import string
6
6
  import platform
7
7
  import collections
8
8
  from urllib.parse import urlparse, urlencode
9
- from typing import Optional
9
+ from typing import Optional, Dict, Any
10
10
 
11
11
  import requests
12
12
  import unidecode
@@ -837,3 +837,52 @@ def get_media_mime_type(filepath: str) -> Optional[str]:
837
837
  content = stream.read()
838
838
 
839
839
  return get_media_mime_type_for_content(content)
840
+
841
+
842
+ def take_web_action_event(
843
+ server_url: str,
844
+ action_token: str
845
+ ) -> Dict[str, Any]:
846
+ """Take web action event using action token.
847
+
848
+ Action token is generated by AYON server and passed to AYON launcher.
849
+
850
+ Args:
851
+ server_url (str): AYON server url.
852
+ action_token (str): Action token.
853
+
854
+ Returns:
855
+ Dict[str, Any]: Web action event.
856
+
857
+ """
858
+ response = requests.get(
859
+ f"{server_url}/api/actions/take/{action_token}"
860
+ )
861
+ response.raise_for_status()
862
+ return response.json()
863
+
864
+
865
+ def abort_web_action_event(
866
+ server_url: str,
867
+ action_token: str,
868
+ reason: str
869
+ ):
870
+ """Abort web action event using action token.
871
+
872
+ A web action event could not be processed for some reason.
873
+
874
+ Args:
875
+ server_url (str): AYON server url.
876
+ action_token (str): Action token.
877
+ reason (str): Reason why webaction event was aborted.
878
+
879
+ Returns:
880
+ requests.Response: Response from server.
881
+
882
+ """
883
+ response = requests.post(
884
+ f"{server_url}/api/actions/take/{action_token}",
885
+ json={"message": reason},
886
+ )
887
+ response.raise_for_status()
888
+ return response
@@ -1,2 +1,2 @@
1
1
  """Package declaring Python API for AYON server."""
2
- __version__ = "1.0.9"
2
+ __version__ = "1.0.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 1.0.9
3
+ Version: 1.0.10
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ayon-python-api"
3
- version = "1.0.9"
3
+ version = "1.0.10"
4
4
  description = "AYON Python API"
5
5
  license = {file = "LICENSE"}
6
6
  readme = {file = "README.md", content-type = "text/markdown"}
@@ -29,11 +29,14 @@ build-backend = "poetry.core.masonry.api"
29
29
 
30
30
  [tool.poetry]
31
31
  name = "ayon-python-api"
32
- version = "1.0.9"
32
+ version = "1.0.10"
33
33
  description = "AYON Python API"
34
34
  authors = [
35
35
  "ynput.io <info@ynput.io>"
36
36
  ]
37
+ packages = [
38
+ { include = "ayon_api" }
39
+ ]
37
40
 
38
41
  [tool.poetry.dependencies]
39
42
  python = ">=3.6.5"
@@ -34,4 +34,4 @@ setup(
34
34
  "appdirs >=1, <2",
35
35
  ],
36
36
  keywords=["AYON", "ynput", "OpenPype", "vfx"],
37
- )
37
+ )