ayon-python-api 1.2.22__tar.gz → 1.2.24__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.
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/PKG-INFO +1 -1
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/__init__.py +2 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api.py +61 -9
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/activities.py +7 -3
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/base.py +1 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/dependency_packages.py +6 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/links.py +101 -3
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/lists.py +4 -4
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/graphql.py +38 -34
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/operations.py +27 -4
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/server_api.py +54 -16
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/typing.py +16 -1
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/utils.py +2 -2
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/version.py +1 -1
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/PKG-INFO +1 -1
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/SOURCES.txt +6 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/pyproject.toml +2 -2
- ayon_python_api-1.2.24/tests/test_download_resume.py +53 -0
- ayon_python_api-1.2.24/tests/test_graphql_descending_pagination.py +21 -0
- ayon_python_api-1.2.24/tests/test_graphql_infinite_loop.py +46 -0
- ayon_python_api-1.2.24/tests/test_operations_result.py +26 -0
- ayon_python_api-1.2.24/tests/test_request_max_retries_kwarg.py +26 -0
- ayon_python_api-1.2.24/tests/test_request_retries.py +35 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/LICENSE +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/README.md +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/__init__.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/actions.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/attributes.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/bundles_addons.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/events.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/folders.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/installers.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/products.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/projects.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/representations.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/secrets.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/tasks.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/thumbnails.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/versions.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/workfiles.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/constants.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/entity_hub.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/events.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/exceptions.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/graphql_queries.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/dependency_links.txt +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/requires.txt +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/top_level.txt +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/setup.cfg +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/setup.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/tests/test_entity_hub.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/tests/test_folder_hierarchy.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/tests/test_get_events.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/tests/test_graphql_queries.py +0 -0
- {ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/tests/test_server.py +0 -0
|
@@ -275,6 +275,7 @@ from ._api import (
|
|
|
275
275
|
delete_link_type,
|
|
276
276
|
make_sure_link_type_exists,
|
|
277
277
|
create_link,
|
|
278
|
+
create_links,
|
|
278
279
|
delete_link,
|
|
279
280
|
get_entities_links,
|
|
280
281
|
get_folders_links,
|
|
@@ -594,6 +595,7 @@ __all__ = (
|
|
|
594
595
|
"delete_link_type",
|
|
595
596
|
"make_sure_link_type_exists",
|
|
596
597
|
"create_link",
|
|
598
|
+
"create_links",
|
|
597
599
|
"delete_link",
|
|
598
600
|
"get_entities_links",
|
|
599
601
|
"get_folders_links",
|
|
@@ -53,6 +53,8 @@ if typing.TYPE_CHECKING:
|
|
|
53
53
|
BackgroundOperationTask,
|
|
54
54
|
LinkDirection,
|
|
55
55
|
CreateLinkData,
|
|
56
|
+
CreateLinkResponseData,
|
|
57
|
+
CreateLinksResponseItem,
|
|
56
58
|
EventFilter,
|
|
57
59
|
EventStatus,
|
|
58
60
|
EnrollEventData,
|
|
@@ -1620,7 +1622,9 @@ def get_rest_entity_by_id(
|
|
|
1620
1622
|
def send_batch_operations(
|
|
1621
1623
|
project_name: str,
|
|
1622
1624
|
operations: list[dict[str, Any]],
|
|
1625
|
+
*,
|
|
1623
1626
|
can_fail: bool = False,
|
|
1627
|
+
wait_for_events: bool = False,
|
|
1624
1628
|
raise_on_fail: bool = True,
|
|
1625
1629
|
) -> list[dict[str, Any]]:
|
|
1626
1630
|
"""Post multiple CRUD operations to server.
|
|
@@ -1635,6 +1639,7 @@ def send_batch_operations(
|
|
|
1635
1639
|
operations (list[dict[str, Any]]): Operations to be processed.
|
|
1636
1640
|
can_fail (bool): Server will try to process all
|
|
1637
1641
|
operations even if one of them fails.
|
|
1642
|
+
wait_for_events (bool): Wait for events to be processed on server.
|
|
1638
1643
|
raise_on_fail (bool): Raise exception if an operation
|
|
1639
1644
|
fails. You can handle failed operations on your own
|
|
1640
1645
|
when set to 'False'.
|
|
@@ -1653,6 +1658,7 @@ def send_batch_operations(
|
|
|
1653
1658
|
project_name=project_name,
|
|
1654
1659
|
operations=operations,
|
|
1655
1660
|
can_fail=can_fail,
|
|
1661
|
+
wait_for_events=wait_for_events,
|
|
1656
1662
|
raise_on_fail=raise_on_fail,
|
|
1657
1663
|
)
|
|
1658
1664
|
|
|
@@ -1662,8 +1668,9 @@ def send_background_batch_operations(
|
|
|
1662
1668
|
operations: list[dict[str, Any]],
|
|
1663
1669
|
*,
|
|
1664
1670
|
can_fail: bool = False,
|
|
1665
|
-
|
|
1671
|
+
wait_for_events: bool = False,
|
|
1666
1672
|
raise_on_fail: bool = True,
|
|
1673
|
+
wait: bool = False,
|
|
1667
1674
|
) -> BackgroundOperationTask:
|
|
1668
1675
|
"""Post multiple CRUD operations to server.
|
|
1669
1676
|
|
|
@@ -1686,10 +1693,11 @@ def send_background_batch_operations(
|
|
|
1686
1693
|
operations (list[dict[str, Any]]): Operations to be processed.
|
|
1687
1694
|
can_fail (bool): Server will try to process all
|
|
1688
1695
|
operations even if one of them fails.
|
|
1689
|
-
|
|
1696
|
+
wait_for_events (bool): Wait for events to be processed on server.
|
|
1690
1697
|
raise_on_fail (bool): Raise exception if an operation
|
|
1691
1698
|
fails. You can handle failed operations on your own
|
|
1692
1699
|
when set to 'False'. Used when 'wait' is enabled.
|
|
1700
|
+
wait (bool): Wait for operations to end.
|
|
1693
1701
|
|
|
1694
1702
|
Raises:
|
|
1695
1703
|
ValueError: Operations can't be converted to json string.
|
|
@@ -1705,8 +1713,9 @@ def send_background_batch_operations(
|
|
|
1705
1713
|
project_name=project_name,
|
|
1706
1714
|
operations=operations,
|
|
1707
1715
|
can_fail=can_fail,
|
|
1708
|
-
|
|
1716
|
+
wait_for_events=wait_for_events,
|
|
1709
1717
|
raise_on_fail=raise_on_fail,
|
|
1718
|
+
wait=wait,
|
|
1710
1719
|
)
|
|
1711
1720
|
|
|
1712
1721
|
|
|
@@ -1934,6 +1943,7 @@ def create_dependency_package(
|
|
|
1934
1943
|
checksum: str,
|
|
1935
1944
|
checksum_algorithm: str,
|
|
1936
1945
|
file_size: int,
|
|
1946
|
+
python_version: str | None = None,
|
|
1937
1947
|
sources: Optional[list[dict[str, Any]]] = None,
|
|
1938
1948
|
platform_name: Optional[str] = None,
|
|
1939
1949
|
) -> None:
|
|
@@ -1959,6 +1969,8 @@ def create_dependency_package(
|
|
|
1959
1969
|
checksum (str): Checksum of archive file where dependencies are.
|
|
1960
1970
|
checksum_algorithm (str): Algorithm used to calculate checksum.
|
|
1961
1971
|
file_size (Optional[int]): Size of file.
|
|
1972
|
+
python_version (str | None): Python version for which
|
|
1973
|
+
is dependency package created.
|
|
1962
1974
|
sources (Optional[list[dict[str, Any]]]): Information about
|
|
1963
1975
|
sources from where it is possible to get file.
|
|
1964
1976
|
platform_name (Optional[str]): Name of platform for which is
|
|
@@ -1975,6 +1987,7 @@ def create_dependency_package(
|
|
|
1975
1987
|
checksum=checksum,
|
|
1976
1988
|
checksum_algorithm=checksum_algorithm,
|
|
1977
1989
|
file_size=file_size,
|
|
1990
|
+
python_version=python_version,
|
|
1978
1991
|
sources=sources,
|
|
1979
1992
|
platform_name=platform_name,
|
|
1980
1993
|
)
|
|
@@ -2708,7 +2721,9 @@ def set_entity_watchers(
|
|
|
2708
2721
|
def send_activities_batch_operations(
|
|
2709
2722
|
project_name: str,
|
|
2710
2723
|
operations: list[dict[str, Any]],
|
|
2724
|
+
*,
|
|
2711
2725
|
can_fail: bool = False,
|
|
2726
|
+
wait_for_events: bool = False,
|
|
2712
2727
|
raise_on_fail: bool = True,
|
|
2713
2728
|
) -> list[dict[str, Any]]:
|
|
2714
2729
|
"""Post multiple CRUD activities operations to server.
|
|
@@ -2723,6 +2738,7 @@ def send_activities_batch_operations(
|
|
|
2723
2738
|
operations (list[dict[str, Any]]): Operations to be processed.
|
|
2724
2739
|
can_fail (Optional[bool]): Server will try to process all
|
|
2725
2740
|
operations even if one of them fails.
|
|
2741
|
+
wait_for_events (bool): Wait for events to be processed on server.
|
|
2726
2742
|
raise_on_fail (Optional[bool]): Raise exception if an operation
|
|
2727
2743
|
fails. You can handle failed operations on your own
|
|
2728
2744
|
when set to 'False'.
|
|
@@ -2741,6 +2757,7 @@ def send_activities_batch_operations(
|
|
|
2741
2757
|
project_name=project_name,
|
|
2742
2758
|
operations=operations,
|
|
2743
2759
|
can_fail=can_fail,
|
|
2760
|
+
wait_for_events=wait_for_events,
|
|
2744
2761
|
raise_on_fail=raise_on_fail,
|
|
2745
2762
|
)
|
|
2746
2763
|
|
|
@@ -7504,7 +7521,7 @@ def create_link(
|
|
|
7504
7521
|
output_type: str,
|
|
7505
7522
|
link_name: Optional[str] = None,
|
|
7506
7523
|
data: Optional[dict[str, Any]] = None,
|
|
7507
|
-
) ->
|
|
7524
|
+
) -> CreateLinkResponseData:
|
|
7508
7525
|
"""Create link between 2 entities.
|
|
7509
7526
|
|
|
7510
7527
|
Link has a type which must already exists on a project.
|
|
@@ -7527,7 +7544,7 @@ def create_link(
|
|
|
7527
7544
|
with the link.
|
|
7528
7545
|
|
|
7529
7546
|
Returns:
|
|
7530
|
-
|
|
7547
|
+
CreateLinkResponseData: Information about link.
|
|
7531
7548
|
|
|
7532
7549
|
Raises:
|
|
7533
7550
|
HTTPRequestError: Server error happened.
|
|
@@ -7546,6 +7563,41 @@ def create_link(
|
|
|
7546
7563
|
)
|
|
7547
7564
|
|
|
7548
7565
|
|
|
7566
|
+
def create_links(
|
|
7567
|
+
project_name: str,
|
|
7568
|
+
links: list[CreateLinkData],
|
|
7569
|
+
) -> list[CreateLinksResponseItem]:
|
|
7570
|
+
"""Create multiple links in a single request.
|
|
7571
|
+
|
|
7572
|
+
Example of link data::
|
|
7573
|
+
[
|
|
7574
|
+
{
|
|
7575
|
+
"input": "59a212c0d2e211eda0e20242ac120001",
|
|
7576
|
+
"output": "59a212c0d2e211eda0e20242ac120002",
|
|
7577
|
+
"linkType": "reference|folder|folder",
|
|
7578
|
+
"name": "my_link",
|
|
7579
|
+
"data": {"key": "value"}
|
|
7580
|
+
}
|
|
7581
|
+
]
|
|
7582
|
+
|
|
7583
|
+
Args:
|
|
7584
|
+
project_name (str): Project where links are created.
|
|
7585
|
+
links (list[CreateLinkData]): List of link data.
|
|
7586
|
+
|
|
7587
|
+
Returns:
|
|
7588
|
+
list[CreateLinksResponseItem]: Information about created links.
|
|
7589
|
+
|
|
7590
|
+
Raises:
|
|
7591
|
+
ValueError: Link data is invalid.
|
|
7592
|
+
|
|
7593
|
+
"""
|
|
7594
|
+
con = get_server_api_connection()
|
|
7595
|
+
return con.create_links(
|
|
7596
|
+
project_name=project_name,
|
|
7597
|
+
links=links,
|
|
7598
|
+
)
|
|
7599
|
+
|
|
7600
|
+
|
|
7549
7601
|
def delete_link(
|
|
7550
7602
|
project_name: str,
|
|
7551
7603
|
link_id: str,
|
|
@@ -8003,8 +8055,8 @@ def create_entity_list(
|
|
|
8003
8055
|
*,
|
|
8004
8056
|
list_type: Optional[str] = None,
|
|
8005
8057
|
access: Optional[dict[str, Any]] = None,
|
|
8006
|
-
attrib: Optional[
|
|
8007
|
-
data: Optional[
|
|
8058
|
+
attrib: Optional[dict[str, Any]] = None,
|
|
8059
|
+
data: Optional[dict[str, Any]] = None,
|
|
8008
8060
|
tags: Optional[list[str]] = None,
|
|
8009
8061
|
template: Optional[dict[str, Any]] = None,
|
|
8010
8062
|
entity_list_folder_id: Optional[str] = None,
|
|
@@ -8060,8 +8112,8 @@ def update_entity_list(
|
|
|
8060
8112
|
*,
|
|
8061
8113
|
label: Optional[str] = None,
|
|
8062
8114
|
access: Optional[dict[str, Any]] = None,
|
|
8063
|
-
attrib: Optional[
|
|
8064
|
-
data: Optional[
|
|
8115
|
+
attrib: Optional[dict[str, Any]] = None,
|
|
8116
|
+
data: Optional[dict[str, Any]] = None,
|
|
8065
8117
|
tags: Optional[list[str]] = None,
|
|
8066
8118
|
entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET,
|
|
8067
8119
|
owner: Optional[str] = None,
|
|
@@ -370,8 +370,10 @@ class ActivitiesAPI(BaseServerAPI):
|
|
|
370
370
|
self,
|
|
371
371
|
project_name: str,
|
|
372
372
|
operations: list[dict[str, Any]],
|
|
373
|
+
*,
|
|
373
374
|
can_fail: bool = False,
|
|
374
|
-
|
|
375
|
+
wait_for_events: bool = False,
|
|
376
|
+
raise_on_fail: bool = True,
|
|
375
377
|
) -> list[dict[str, Any]]:
|
|
376
378
|
"""Post multiple CRUD activities operations to server.
|
|
377
379
|
|
|
@@ -385,6 +387,7 @@ class ActivitiesAPI(BaseServerAPI):
|
|
|
385
387
|
operations (list[dict[str, Any]]): Operations to be processed.
|
|
386
388
|
can_fail (Optional[bool]): Server will try to process all
|
|
387
389
|
operations even if one of them fails.
|
|
390
|
+
wait_for_events (bool): Wait for events to be processed on server.
|
|
388
391
|
raise_on_fail (Optional[bool]): Raise exception if an operation
|
|
389
392
|
fails. You can handle failed operations on your own
|
|
390
393
|
when set to 'False'.
|
|
@@ -401,6 +404,7 @@ class ActivitiesAPI(BaseServerAPI):
|
|
|
401
404
|
return self._send_batch_operations(
|
|
402
405
|
f"projects/{project_name}/operations/activities",
|
|
403
406
|
operations,
|
|
404
|
-
can_fail,
|
|
405
|
-
|
|
407
|
+
can_fail=can_fail,
|
|
408
|
+
wait_for_events=wait_for_events,
|
|
409
|
+
raise_on_fail=raise_on_fail,
|
|
406
410
|
)
|
{ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/dependency_packages.py
RENAMED
|
@@ -57,6 +57,7 @@ class DependencyPackagesAPI(BaseServerAPI):
|
|
|
57
57
|
checksum: str,
|
|
58
58
|
checksum_algorithm: str,
|
|
59
59
|
file_size: int,
|
|
60
|
+
python_version: str | None = None,
|
|
60
61
|
sources: Optional[list[dict[str, Any]]] = None,
|
|
61
62
|
platform_name: Optional[str] = None,
|
|
62
63
|
) -> None:
|
|
@@ -82,6 +83,8 @@ class DependencyPackagesAPI(BaseServerAPI):
|
|
|
82
83
|
checksum (str): Checksum of archive file where dependencies are.
|
|
83
84
|
checksum_algorithm (str): Algorithm used to calculate checksum.
|
|
84
85
|
file_size (Optional[int]): Size of file.
|
|
86
|
+
python_version (str | None): Python version for which
|
|
87
|
+
is dependency package created.
|
|
85
88
|
sources (Optional[list[dict[str, Any]]]): Information about
|
|
86
89
|
sources from where it is possible to get file.
|
|
87
90
|
platform_name (Optional[str]): Name of platform for which is
|
|
@@ -99,6 +102,9 @@ class DependencyPackagesAPI(BaseServerAPI):
|
|
|
99
102
|
"size": file_size,
|
|
100
103
|
"platform": platform_name or platform.system().lower(),
|
|
101
104
|
}
|
|
105
|
+
if python_version:
|
|
106
|
+
post_body["pythonVersion"] = python_version
|
|
107
|
+
|
|
102
108
|
if sources:
|
|
103
109
|
post_body["sources"] = sources
|
|
104
110
|
|
|
@@ -15,7 +15,12 @@ from ayon_api.graphql_queries import (
|
|
|
15
15
|
from .base import BaseServerAPI
|
|
16
16
|
|
|
17
17
|
if typing.TYPE_CHECKING:
|
|
18
|
-
from ayon_api.typing import
|
|
18
|
+
from ayon_api.typing import (
|
|
19
|
+
LinkDirection,
|
|
20
|
+
CreateLinkData,
|
|
21
|
+
CreateLinkResponseData,
|
|
22
|
+
CreateLinksResponseItem,
|
|
23
|
+
)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
class LinksAPI(BaseServerAPI):
|
|
@@ -200,7 +205,7 @@ class LinksAPI(BaseServerAPI):
|
|
|
200
205
|
output_type: str,
|
|
201
206
|
link_name: Optional[str] = None,
|
|
202
207
|
data: Optional[dict[str, Any]] = None,
|
|
203
|
-
) ->
|
|
208
|
+
) -> CreateLinkResponseData:
|
|
204
209
|
"""Create link between 2 entities.
|
|
205
210
|
|
|
206
211
|
Link has a type which must already exists on a project.
|
|
@@ -223,7 +228,7 @@ class LinksAPI(BaseServerAPI):
|
|
|
223
228
|
with the link.
|
|
224
229
|
|
|
225
230
|
Returns:
|
|
226
|
-
|
|
231
|
+
CreateLinkResponseData: Information about link.
|
|
227
232
|
|
|
228
233
|
Raises:
|
|
229
234
|
HTTPRequestError: Server error happened.
|
|
@@ -249,6 +254,70 @@ class LinksAPI(BaseServerAPI):
|
|
|
249
254
|
response.raise_for_status()
|
|
250
255
|
return response.data
|
|
251
256
|
|
|
257
|
+
def create_links(
|
|
258
|
+
self,
|
|
259
|
+
project_name: str,
|
|
260
|
+
links: list[CreateLinkData],
|
|
261
|
+
) -> list[CreateLinksResponseItem]:
|
|
262
|
+
"""Create multiple links in a single request.
|
|
263
|
+
|
|
264
|
+
Example of link data::
|
|
265
|
+
[
|
|
266
|
+
{
|
|
267
|
+
"input": "59a212c0d2e211eda0e20242ac120001",
|
|
268
|
+
"output": "59a212c0d2e211eda0e20242ac120002",
|
|
269
|
+
"linkType": "reference|folder|folder",
|
|
270
|
+
"name": "my_link",
|
|
271
|
+
"data": {"key": "value"}
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
project_name (str): Project where links are created.
|
|
277
|
+
links (list[CreateLinkData]): List of link data.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
list[CreateLinksResponseItem]: Information about created links.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
ValueError: Link data is invalid.
|
|
284
|
+
|
|
285
|
+
"""
|
|
286
|
+
if not links:
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
for link in links:
|
|
290
|
+
self._validate_link_data(link)
|
|
291
|
+
|
|
292
|
+
if self.get_server_version_tuple() < (1, 15, 8):
|
|
293
|
+
created_links = []
|
|
294
|
+
for link in links:
|
|
295
|
+
link_type, in_type, out_type = link["linkType"].split("|")
|
|
296
|
+
data = self.create_link(
|
|
297
|
+
project_name,
|
|
298
|
+
link_type,
|
|
299
|
+
link["input"],
|
|
300
|
+
in_type,
|
|
301
|
+
link["output"],
|
|
302
|
+
out_type,
|
|
303
|
+
link_name=link.get("name") or None,
|
|
304
|
+
data=link.get("data") or None,
|
|
305
|
+
)
|
|
306
|
+
created_links.append({
|
|
307
|
+
"id": data["id"],
|
|
308
|
+
"input": link["input"],
|
|
309
|
+
"output": link["output"],
|
|
310
|
+
"linkType": link["linkType"]
|
|
311
|
+
})
|
|
312
|
+
return created_links
|
|
313
|
+
|
|
314
|
+
response = self.post(
|
|
315
|
+
f"projects/{project_name}/links/bulk",
|
|
316
|
+
links=links
|
|
317
|
+
)
|
|
318
|
+
response.raise_for_status()
|
|
319
|
+
return response.data["created"]
|
|
320
|
+
|
|
252
321
|
def delete_link(self, project_name: str, link_id: str) -> None:
|
|
253
322
|
"""Remove link by id.
|
|
254
323
|
|
|
@@ -619,6 +688,35 @@ class LinksAPI(BaseServerAPI):
|
|
|
619
688
|
project_name, [representation_id], link_types, link_direction
|
|
620
689
|
)[representation_id]
|
|
621
690
|
|
|
691
|
+
def _validate_link_data(self, link_data: dict[str, Any]) -> None:
|
|
692
|
+
"""Validate link data before sending to server.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
link_data (dict[str, Any]): Link data to validate.
|
|
696
|
+
|
|
697
|
+
Raises:
|
|
698
|
+
ValueError: Link data is invalid.
|
|
699
|
+
|
|
700
|
+
"""
|
|
701
|
+
required_keys = {"input", "output", "linkType"}
|
|
702
|
+
missing_keys = required_keys - link_data.keys()
|
|
703
|
+
if missing_keys:
|
|
704
|
+
mk = ", ".join(f"'{key}'" for key in sorted(missing_keys))
|
|
705
|
+
raise ValueError(f"Missing required keys in link data: {mk}")
|
|
706
|
+
|
|
707
|
+
link_type = link_data["linkType"]
|
|
708
|
+
if not isinstance(link_type, str):
|
|
709
|
+
raise ValueError(
|
|
710
|
+
f"Invalid linkType type: {type(link_type)}. Expected 'str'"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
link_type_parts = link_type.split("|")
|
|
714
|
+
if len(link_type_parts) != 3:
|
|
715
|
+
raise ValueError(
|
|
716
|
+
f"Invalid linkType format: {link_type}. Expected format:"
|
|
717
|
+
" 'link_type|input_type|output_type'"
|
|
718
|
+
)
|
|
719
|
+
|
|
622
720
|
def _prepare_link_filters(
|
|
623
721
|
self,
|
|
624
722
|
filters: dict[str, Any],
|
|
@@ -157,8 +157,8 @@ class ListsAPI(BaseServerAPI):
|
|
|
157
157
|
*,
|
|
158
158
|
list_type: Optional[str] = None,
|
|
159
159
|
access: Optional[dict[str, Any]] = None,
|
|
160
|
-
attrib: Optional[
|
|
161
|
-
data: Optional[
|
|
160
|
+
attrib: Optional[dict[str, Any]] = None,
|
|
161
|
+
data: Optional[dict[str, Any]] = None,
|
|
162
162
|
tags: Optional[list[str]] = None,
|
|
163
163
|
template: Optional[dict[str, Any]] = None,
|
|
164
164
|
entity_list_folder_id: Optional[str] = None,
|
|
@@ -225,8 +225,8 @@ class ListsAPI(BaseServerAPI):
|
|
|
225
225
|
*,
|
|
226
226
|
label: Optional[str] = None,
|
|
227
227
|
access: Optional[dict[str, Any]] = None,
|
|
228
|
-
attrib: Optional[
|
|
229
|
-
data: Optional[
|
|
228
|
+
attrib: Optional[dict[str, Any]] = None,
|
|
229
|
+
data: Optional[dict[str, Any]] = None,
|
|
230
230
|
tags: Optional[list[str]] = None,
|
|
231
231
|
entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET,
|
|
232
232
|
owner: Optional[str] = None,
|
|
@@ -353,6 +353,25 @@ class GraphQlQuery:
|
|
|
353
353
|
for child in self._children:
|
|
354
354
|
child.parse_result(data, output, progress_data)
|
|
355
355
|
|
|
356
|
+
def _query_data(self, con: ServerAPI) -> dict[str, Any]:
|
|
357
|
+
"""Send single query to server and return 'data' of the response."""
|
|
358
|
+
query_str = self.calculate_query()
|
|
359
|
+
variables = self.get_variables_values()
|
|
360
|
+
response = con.query_graphql(query_str, variables)
|
|
361
|
+
if response.errors:
|
|
362
|
+
raise GraphQlQueryFailed(response.errors, query_str, variables)
|
|
363
|
+
|
|
364
|
+
data = response.data.get("data")
|
|
365
|
+
if data is None:
|
|
366
|
+
# Parsing 'None' would not change pagination state and the same
|
|
367
|
+
# query would be sent again in an infinite loop.
|
|
368
|
+
raise GraphQlQueryError(
|
|
369
|
+
f"GraphQl query '{self._name}' response does not contain"
|
|
370
|
+
f" 'data'. Response: {str(response.data)[:1000]}"
|
|
371
|
+
f"\nQuery:\n{query_str}\nVariables: {variables}"
|
|
372
|
+
)
|
|
373
|
+
return data
|
|
374
|
+
|
|
356
375
|
def query(self, con: ServerAPI) -> dict[str, Any]:
|
|
357
376
|
"""Do a query from server.
|
|
358
377
|
|
|
@@ -366,15 +385,8 @@ class GraphQlQuery:
|
|
|
366
385
|
progress_data = {}
|
|
367
386
|
output = {}
|
|
368
387
|
while self.need_query:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
response = con.query_graphql(
|
|
372
|
-
query_str,
|
|
373
|
-
variables
|
|
374
|
-
)
|
|
375
|
-
if response.errors:
|
|
376
|
-
raise GraphQlQueryFailed(response.errors, query_str, variables)
|
|
377
|
-
self.parse_result(response.data["data"], output, progress_data)
|
|
388
|
+
data = self._query_data(con)
|
|
389
|
+
self.parse_result(data, output, progress_data)
|
|
378
390
|
|
|
379
391
|
return output
|
|
380
392
|
|
|
@@ -394,30 +406,16 @@ class GraphQlQuery:
|
|
|
394
406
|
if self.has_multiple_edge_fields:
|
|
395
407
|
output = {}
|
|
396
408
|
while self.need_query:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
response = con.query_graphql(query_str, variables)
|
|
401
|
-
if response.errors:
|
|
402
|
-
raise GraphQlQueryFailed(
|
|
403
|
-
response.errors, query_str, variables
|
|
404
|
-
)
|
|
405
|
-
self.parse_result(response.data["data"], output, progress_data)
|
|
409
|
+
data = self._query_data(con)
|
|
410
|
+
self.parse_result(data, output, progress_data)
|
|
406
411
|
|
|
407
412
|
yield output
|
|
408
413
|
|
|
409
414
|
else:
|
|
410
415
|
while self.need_query:
|
|
411
416
|
output = {}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
response = con.query_graphql(query_str, variables)
|
|
415
|
-
if response.errors:
|
|
416
|
-
raise GraphQlQueryFailed(
|
|
417
|
-
response.errors, query_str, variables
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
self.parse_result(response.data["data"], output, progress_data)
|
|
417
|
+
data = self._query_data(con)
|
|
418
|
+
self.parse_result(data, output, progress_data)
|
|
421
419
|
|
|
422
420
|
yield output
|
|
423
421
|
|
|
@@ -903,11 +901,13 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField):
|
|
|
903
901
|
progress_data[cursor_key] = nodes_by_cursor
|
|
904
902
|
|
|
905
903
|
page_info = value["pageInfo"]
|
|
904
|
+
# Continue from the last received edge for both orders. Server
|
|
905
|
+
# returns edges of 'last' page from the newest item, so
|
|
906
|
+
# 'startCursor' would move the cursor back only by one item.
|
|
907
|
+
new_cursor = page_info["endCursor"]
|
|
906
908
|
if self._order == SortOrder.ascending:
|
|
907
|
-
new_cursor = page_info["endCursor"]
|
|
908
909
|
self._need_query = page_info["hasNextPage"]
|
|
909
910
|
else:
|
|
910
|
-
new_cursor = page_info["startCursor"]
|
|
911
911
|
self._need_query = page_info["hasPreviousPage"]
|
|
912
912
|
|
|
913
913
|
edges = value["edges"]
|
|
@@ -943,6 +943,14 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField):
|
|
|
943
943
|
change_cursor = False
|
|
944
944
|
|
|
945
945
|
if change_cursor and self._need_query:
|
|
946
|
+
if new_cursor is None:
|
|
947
|
+
# Without cursor the pagination would start from beginning
|
|
948
|
+
raise GraphQlQueryError(
|
|
949
|
+
f"Field '{self.path}' reported another page without"
|
|
950
|
+
" a cursor. Stopped pagination after"
|
|
951
|
+
f" {self._fetched_counter} items."
|
|
952
|
+
)
|
|
953
|
+
|
|
946
954
|
if new_cursor == self._cursor:
|
|
947
955
|
raise GraphQlQueryError(
|
|
948
956
|
"Cursor didn't change during pagination."
|
|
@@ -1016,11 +1024,7 @@ class GraphQlQueryEdgeField(BaseGraphQlQueryField):
|
|
|
1016
1024
|
# Add page information
|
|
1017
1025
|
output.append(edges_offset + "pageInfo {")
|
|
1018
1026
|
for page_key in (
|
|
1019
|
-
|
|
1020
|
-
"endCursor"
|
|
1021
|
-
if self._order == SortOrder.ascending
|
|
1022
|
-
else "startCursor"
|
|
1023
|
-
),
|
|
1027
|
+
"endCursor",
|
|
1024
1028
|
(
|
|
1025
1029
|
"hasNextPage"
|
|
1026
1030
|
if self._order == SortOrder.ascending
|
|
@@ -741,12 +741,21 @@ class OperationsSession(object):
|
|
|
741
741
|
Args:
|
|
742
742
|
con (Optional[ServerAPI]): Connection to server. Global connection
|
|
743
743
|
is used if not passed.
|
|
744
|
+
wait_for_events (bool): Wait for events to be processed on server.
|
|
745
|
+
Default value of the argument. Can be changed when 'commit'
|
|
746
|
+
is called.
|
|
744
747
|
|
|
745
748
|
"""
|
|
746
|
-
def __init__(
|
|
749
|
+
def __init__(
|
|
750
|
+
self,
|
|
751
|
+
con: Optional[ServerAPI] = None,
|
|
752
|
+
*,
|
|
753
|
+
wait_for_events: bool = False,
|
|
754
|
+
) -> None:
|
|
747
755
|
if con is None:
|
|
748
756
|
con = get_server_api_connection()
|
|
749
757
|
self._con = con
|
|
758
|
+
self._wait_for_events = wait_for_events
|
|
750
759
|
self._project_cache = {}
|
|
751
760
|
self._operations = []
|
|
752
761
|
self._nested_operations = collections.defaultdict(list)
|
|
@@ -817,8 +826,15 @@ class OperationsSession(object):
|
|
|
817
826
|
for operation in self._operations
|
|
818
827
|
]
|
|
819
828
|
|
|
820
|
-
def commit(self) -> None:
|
|
821
|
-
"""Commit session operations.
|
|
829
|
+
def commit(self, *, wait_for_events: bool | None = None) -> None:
|
|
830
|
+
"""Commit session operations.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
wait_for_events (bool | None): Wait for events to be processed
|
|
834
|
+
on server. Use 'None' to use the default value defined on
|
|
835
|
+
OperationsSession object.
|
|
836
|
+
|
|
837
|
+
"""
|
|
822
838
|
operations, self._operations = self._operations, []
|
|
823
839
|
if not operations:
|
|
824
840
|
return
|
|
@@ -827,6 +843,9 @@ class OperationsSession(object):
|
|
|
827
843
|
for operation in operations:
|
|
828
844
|
operations_by_project[operation.project_name].append(operation)
|
|
829
845
|
|
|
846
|
+
if wait_for_events is None:
|
|
847
|
+
wait_for_events = self._wait_for_events
|
|
848
|
+
|
|
830
849
|
for project_name, operations in operations_by_project.items():
|
|
831
850
|
operations_body = []
|
|
832
851
|
for operation in operations:
|
|
@@ -835,7 +854,11 @@ class OperationsSession(object):
|
|
|
835
854
|
operations_body.append(body)
|
|
836
855
|
|
|
837
856
|
self._con.send_background_batch_operations(
|
|
838
|
-
project_name,
|
|
857
|
+
project_name,
|
|
858
|
+
operations_body,
|
|
859
|
+
can_fail=False,
|
|
860
|
+
wait_for_events=wait_for_events,
|
|
861
|
+
wait=True,
|
|
839
862
|
)
|
|
840
863
|
|
|
841
864
|
def create_entity(
|
|
@@ -1489,6 +1489,8 @@ class ServerAPI(
|
|
|
1489
1489
|
response = self.raw_get(
|
|
1490
1490
|
"info",
|
|
1491
1491
|
handle_invalid_token=False,
|
|
1492
|
+
verify=self.ssl_verify,
|
|
1493
|
+
cert=self.cert
|
|
1492
1494
|
)
|
|
1493
1495
|
response.raise_for_status()
|
|
1494
1496
|
return response.data
|
|
@@ -1519,7 +1521,7 @@ class ServerAPI(
|
|
|
1519
1521
|
**kwargs
|
|
1520
1522
|
) -> RestApiResponse:
|
|
1521
1523
|
kwargs.setdefault("timeout", self.timeout)
|
|
1522
|
-
max_retries = kwargs.
|
|
1524
|
+
max_retries = kwargs.pop("max_retries", self.max_retries)
|
|
1523
1525
|
if max_retries < 1:
|
|
1524
1526
|
max_retries = 1
|
|
1525
1527
|
|
|
@@ -1558,6 +1560,7 @@ class ServerAPI(
|
|
|
1558
1560
|
response = None
|
|
1559
1561
|
new_response = None
|
|
1560
1562
|
for retry_idx in reversed(range(max_retries)):
|
|
1563
|
+
new_response = None
|
|
1561
1564
|
try:
|
|
1562
1565
|
response = function(url, **kwargs)
|
|
1563
1566
|
|
|
@@ -1658,7 +1661,7 @@ class ServerAPI(
|
|
|
1658
1661
|
url = self._endpoint_to_url(endpoint, use_rest=False)
|
|
1659
1662
|
progress.set_source_url(url)
|
|
1660
1663
|
|
|
1661
|
-
retries = self.
|
|
1664
|
+
retries = max(self.max_retries, 1)
|
|
1662
1665
|
api_prepended = False
|
|
1663
1666
|
for attempt in range(retries):
|
|
1664
1667
|
# Continue in download
|
|
@@ -1684,6 +1687,14 @@ class ServerAPI(
|
|
|
1684
1687
|
progress.set_destination_url(url)
|
|
1685
1688
|
continue
|
|
1686
1689
|
response.raise_for_status()
|
|
1690
|
+
if offset > 0 and response.status_code != 206:
|
|
1691
|
+
# Server ignored 'Range' and sends whole file again,
|
|
1692
|
+
# already downloaded content must be discarded
|
|
1693
|
+
stream.seek(0)
|
|
1694
|
+
stream.truncate()
|
|
1695
|
+
progress.reset_transferred()
|
|
1696
|
+
headers.pop("Range", None)
|
|
1697
|
+
|
|
1687
1698
|
if progress.get_content_size() is None:
|
|
1688
1699
|
progress.set_content_size(
|
|
1689
1700
|
response.headers["Content-length"]
|
|
@@ -1692,11 +1703,21 @@ class ServerAPI(
|
|
|
1692
1703
|
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
1693
1704
|
stream.write(chunk)
|
|
1694
1705
|
progress.add_transferred_chunk(len(chunk))
|
|
1706
|
+
|
|
1707
|
+
content_size = progress.get_content_size()
|
|
1708
|
+
transferred = progress.get_transferred_size()
|
|
1709
|
+
if content_size is not None and transferred != content_size:
|
|
1710
|
+
# Connection was closed before all content was received
|
|
1711
|
+
raise requests.exceptions.ConnectionError(
|
|
1712
|
+
f"Downloaded {transferred} out of {content_size}"
|
|
1713
|
+
f" bytes from '{url}'."
|
|
1714
|
+
)
|
|
1695
1715
|
break
|
|
1696
1716
|
|
|
1697
1717
|
except (
|
|
1698
1718
|
requests.exceptions.Timeout,
|
|
1699
1719
|
requests.exceptions.ConnectionError,
|
|
1720
|
+
requests.exceptions.ChunkedEncodingError,
|
|
1700
1721
|
):
|
|
1701
1722
|
if attempt == retries - 1:
|
|
1702
1723
|
raise
|
|
@@ -2093,7 +2114,7 @@ class ServerAPI(
|
|
|
2093
2114
|
headers.pop(orig_key)
|
|
2094
2115
|
headers[key] = value
|
|
2095
2116
|
|
|
2096
|
-
retries = self.
|
|
2117
|
+
retries = max(self.max_retries, 1)
|
|
2097
2118
|
response = None
|
|
2098
2119
|
|
|
2099
2120
|
# Get size of file
|
|
@@ -2475,7 +2496,9 @@ class ServerAPI(
|
|
|
2475
2496
|
self,
|
|
2476
2497
|
project_name: str,
|
|
2477
2498
|
operations: list[dict[str, Any]],
|
|
2499
|
+
*,
|
|
2478
2500
|
can_fail: bool = False,
|
|
2501
|
+
wait_for_events: bool = False,
|
|
2479
2502
|
raise_on_fail: bool = True,
|
|
2480
2503
|
) -> list[dict[str, Any]]:
|
|
2481
2504
|
"""Post multiple CRUD operations to server.
|
|
@@ -2490,6 +2513,7 @@ class ServerAPI(
|
|
|
2490
2513
|
operations (list[dict[str, Any]]): Operations to be processed.
|
|
2491
2514
|
can_fail (bool): Server will try to process all
|
|
2492
2515
|
operations even if one of them fails.
|
|
2516
|
+
wait_for_events (bool): Wait for events to be processed on server.
|
|
2493
2517
|
raise_on_fail (bool): Raise exception if an operation
|
|
2494
2518
|
fails. You can handle failed operations on your own
|
|
2495
2519
|
when set to 'False'.
|
|
@@ -2506,8 +2530,9 @@ class ServerAPI(
|
|
|
2506
2530
|
return self._send_batch_operations(
|
|
2507
2531
|
f"projects/{project_name}/operations",
|
|
2508
2532
|
operations,
|
|
2509
|
-
can_fail,
|
|
2510
|
-
|
|
2533
|
+
can_fail=can_fail,
|
|
2534
|
+
wait_for_events=wait_for_events,
|
|
2535
|
+
raise_on_fail=raise_on_fail,
|
|
2511
2536
|
)
|
|
2512
2537
|
|
|
2513
2538
|
def send_background_batch_operations(
|
|
@@ -2516,8 +2541,9 @@ class ServerAPI(
|
|
|
2516
2541
|
operations: list[dict[str, Any]],
|
|
2517
2542
|
*,
|
|
2518
2543
|
can_fail: bool = False,
|
|
2519
|
-
|
|
2544
|
+
wait_for_events: bool = False,
|
|
2520
2545
|
raise_on_fail: bool = True,
|
|
2546
|
+
wait: bool = False,
|
|
2521
2547
|
) -> BackgroundOperationTask:
|
|
2522
2548
|
"""Post multiple CRUD operations to server.
|
|
2523
2549
|
|
|
@@ -2540,10 +2566,11 @@ class ServerAPI(
|
|
|
2540
2566
|
operations (list[dict[str, Any]]): Operations to be processed.
|
|
2541
2567
|
can_fail (bool): Server will try to process all
|
|
2542
2568
|
operations even if one of them fails.
|
|
2543
|
-
|
|
2569
|
+
wait_for_events (bool): Wait for events to be processed on server.
|
|
2544
2570
|
raise_on_fail (bool): Raise exception if an operation
|
|
2545
2571
|
fails. You can handle failed operations on your own
|
|
2546
2572
|
when set to 'False'. Used when 'wait' is enabled.
|
|
2573
|
+
wait (bool): Wait for operations to end.
|
|
2547
2574
|
|
|
2548
2575
|
Raises:
|
|
2549
2576
|
ValueError: Operations can't be converted to json string.
|
|
@@ -2558,7 +2585,8 @@ class ServerAPI(
|
|
|
2558
2585
|
response = self.post(
|
|
2559
2586
|
f"projects/{project_name}/operations/background",
|
|
2560
2587
|
operations=operations_body,
|
|
2561
|
-
canFail=can_fail
|
|
2588
|
+
canFail=can_fail,
|
|
2589
|
+
waitForEvents=wait_for_events,
|
|
2562
2590
|
)
|
|
2563
2591
|
response.raise_for_status()
|
|
2564
2592
|
if not wait:
|
|
@@ -2631,7 +2659,8 @@ class ServerAPI(
|
|
|
2631
2659
|
uri: str,
|
|
2632
2660
|
operations: list[dict[str, Any]],
|
|
2633
2661
|
can_fail: bool,
|
|
2634
|
-
|
|
2662
|
+
wait_for_events: bool,
|
|
2663
|
+
raise_on_fail: bool,
|
|
2635
2664
|
) -> list[dict[str, Any]]:
|
|
2636
2665
|
if not operations:
|
|
2637
2666
|
return []
|
|
@@ -2643,7 +2672,8 @@ class ServerAPI(
|
|
|
2643
2672
|
response = self.post(
|
|
2644
2673
|
uri,
|
|
2645
2674
|
operations=operations_body,
|
|
2646
|
-
canFail=can_fail
|
|
2675
|
+
canFail=can_fail,
|
|
2676
|
+
waitForEvents=wait_for_events,
|
|
2647
2677
|
)
|
|
2648
2678
|
|
|
2649
2679
|
op_results = response.get("operations")
|
|
@@ -2667,24 +2697,32 @@ class ServerAPI(
|
|
|
2667
2697
|
if result.get("success"):
|
|
2668
2698
|
return None
|
|
2669
2699
|
|
|
2670
|
-
|
|
2671
|
-
|
|
2700
|
+
self.log.warning(
|
|
2701
|
+
"Operations failed. Server response:\n%s",
|
|
2702
|
+
json.dumps(result, indent=4, default=str),
|
|
2703
|
+
)
|
|
2704
|
+
for op_result in result.get("operations") or []:
|
|
2672
2705
|
if op_result["success"]:
|
|
2673
2706
|
continue
|
|
2674
2707
|
|
|
2675
2708
|
operation_id = op_result["id"]
|
|
2676
2709
|
operation = next(
|
|
2677
|
-
op
|
|
2678
|
-
|
|
2679
|
-
if op["id"] == operation_id
|
|
2710
|
+
(op for op in operations_body if op["id"] == operation_id),
|
|
2711
|
+
op_result,
|
|
2680
2712
|
)
|
|
2681
2713
|
detail = op_result["detail"]
|
|
2682
2714
|
raise FailedOperations(
|
|
2683
2715
|
f"Operation \"{operation_id}\" failed with data:"
|
|
2684
|
-
f"\n{json.dumps(operation, indent=4)}"
|
|
2716
|
+
f"\n{json.dumps(operation, indent=4, default=str)}"
|
|
2685
2717
|
f"\nDetail: {detail}."
|
|
2686
2718
|
)
|
|
2687
2719
|
|
|
2720
|
+
# Server did not report which operation failed
|
|
2721
|
+
raise FailedOperations(
|
|
2722
|
+
"Operations failed. Server response:"
|
|
2723
|
+
f"\n{json.dumps(result, indent=4, default=str)}"
|
|
2724
|
+
)
|
|
2725
|
+
|
|
2688
2726
|
def _prepare_fields(
|
|
2689
2727
|
self,
|
|
2690
2728
|
entity_type: str,
|
|
@@ -127,8 +127,23 @@ AttributeType = Literal[
|
|
|
127
127
|
LinkDirection = Literal["in", "out"]
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
class
|
|
130
|
+
class CreateLinkResponseData(TypedDict):
|
|
131
|
+
id: str
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class CreateLinksResponseItem(TypedDict):
|
|
131
135
|
id: str
|
|
136
|
+
input: str
|
|
137
|
+
output: str
|
|
138
|
+
linkType: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CreateLinkData(TypedDict):
|
|
142
|
+
input: str
|
|
143
|
+
output: str
|
|
144
|
+
linkType: str
|
|
145
|
+
data: NotRequired[dict[str, Any] | None]
|
|
146
|
+
name: NotRequired[str | None]
|
|
132
147
|
|
|
133
148
|
|
|
134
149
|
class AttributeEnumItemDict(TypedDict):
|
|
@@ -955,7 +955,7 @@ class TransferProgress:
|
|
|
955
955
|
"""
|
|
956
956
|
return self._content_size
|
|
957
957
|
|
|
958
|
-
def set_content_size(self, content_size: int) -> None:
|
|
958
|
+
def set_content_size(self, content_size: int | str) -> None:
|
|
959
959
|
"""Set content size in bytes.
|
|
960
960
|
|
|
961
961
|
Args:
|
|
@@ -967,7 +967,7 @@ class TransferProgress:
|
|
|
967
967
|
"""
|
|
968
968
|
if self._content_size is not None:
|
|
969
969
|
raise ValueError("Content size was set more then once")
|
|
970
|
-
self._content_size = content_size
|
|
970
|
+
self._content_size = int(content_size)
|
|
971
971
|
|
|
972
972
|
def get_started(self) -> bool:
|
|
973
973
|
"""Transfer was started.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Package declaring Python API for AYON server."""
|
|
2
|
-
__version__ = "1.2.
|
|
2
|
+
__version__ = "1.2.24"
|
|
@@ -40,8 +40,14 @@ ayon_python_api.egg-info/SOURCES.txt
|
|
|
40
40
|
ayon_python_api.egg-info/dependency_links.txt
|
|
41
41
|
ayon_python_api.egg-info/requires.txt
|
|
42
42
|
ayon_python_api.egg-info/top_level.txt
|
|
43
|
+
tests/test_download_resume.py
|
|
43
44
|
tests/test_entity_hub.py
|
|
44
45
|
tests/test_folder_hierarchy.py
|
|
45
46
|
tests/test_get_events.py
|
|
47
|
+
tests/test_graphql_descending_pagination.py
|
|
48
|
+
tests/test_graphql_infinite_loop.py
|
|
46
49
|
tests/test_graphql_queries.py
|
|
50
|
+
tests/test_operations_result.py
|
|
51
|
+
tests/test_request_max_retries_kwarg.py
|
|
52
|
+
tests/test_request_retries.py
|
|
47
53
|
tests/test_server.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ayon_python_api"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.24"
|
|
4
4
|
description = "AYON Python API"
|
|
5
5
|
license = {file = "LICENSE"}
|
|
6
6
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
@@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
28
28
|
|
|
29
29
|
[tool.poetry]
|
|
30
30
|
name = "ayon_python_api"
|
|
31
|
-
version = "1.2.
|
|
31
|
+
version = "1.2.24"
|
|
32
32
|
description = "AYON Python API"
|
|
33
33
|
authors = [
|
|
34
34
|
"ynput.io <info@ynput.io>"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Interrupted downloads. Does not require running AYON server."""
|
|
2
|
+
import io
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from ayon_api.server_api import ServerAPI
|
|
7
|
+
from ayon_api.utils import RequestTypes
|
|
8
|
+
|
|
9
|
+
from .fake_transfer import FakeResponse
|
|
10
|
+
|
|
11
|
+
CONTENT = b"0123456789"
|
|
12
|
+
SIZE_HEADER = {"Content-Length": str(len(CONTENT))}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def con(monkeypatch):
|
|
17
|
+
monkeypatch.setattr("time.sleep", lambda *args, **kwargs: None)
|
|
18
|
+
return ServerAPI("http://localhost:0", create_session=False, max_retries=3)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _download(con, get_func):
|
|
22
|
+
con._base_functions_mapping[RequestTypes.get] = get_func
|
|
23
|
+
stream = io.BytesIO()
|
|
24
|
+
progress = con.download_file_to_stream("api/file", stream)
|
|
25
|
+
return stream.getvalue(), progress
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_incomplete_download_is_continued(con):
|
|
29
|
+
def get_func(url, **kwargs):
|
|
30
|
+
if "Range" not in kwargs["headers"]:
|
|
31
|
+
# Connection closed after 4 bytes without an exception
|
|
32
|
+
return FakeResponse(200, CONTENT[:4], SIZE_HEADER)
|
|
33
|
+
assert kwargs["headers"]["Range"] == "bytes=4-"
|
|
34
|
+
return FakeResponse(206, CONTENT[4:], {"Content-Length": "6"})
|
|
35
|
+
|
|
36
|
+
content, progress = _download(con, get_func)
|
|
37
|
+
assert content == CONTENT
|
|
38
|
+
assert progress.transferred_size == len(CONTENT)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_resume_without_range_support_does_not_duplicate(con):
|
|
42
|
+
calls = []
|
|
43
|
+
|
|
44
|
+
def get_func(url, **kwargs):
|
|
45
|
+
calls.append(url)
|
|
46
|
+
if len(calls) == 1:
|
|
47
|
+
return FakeResponse(200, CONTENT[:4], SIZE_HEADER)
|
|
48
|
+
# Server ignores 'Range' header and sends whole file again
|
|
49
|
+
return FakeResponse(200, CONTENT, SIZE_HEADER)
|
|
50
|
+
|
|
51
|
+
content, progress = _download(con, get_func)
|
|
52
|
+
assert content == CONTENT
|
|
53
|
+
assert progress.transferred_size == len(CONTENT)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Descending GraphQl pagination must not return duplicates.
|
|
2
|
+
|
|
3
|
+
Does not require running AYON server.
|
|
4
|
+
"""
|
|
5
|
+
from ayon_api.graphql_queries import events_graphql_query
|
|
6
|
+
from ayon_api.utils import SortOrder
|
|
7
|
+
|
|
8
|
+
from .graphql_fake_server import FakeServer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_descending_pagination_has_no_duplicates():
|
|
12
|
+
data = {"events": [{"id": f"e{idx}"} for idx in range(700)]}
|
|
13
|
+
query = events_graphql_query({"id"}, SortOrder.descending)
|
|
14
|
+
# AYON server returns edges of a page queried with 'last' from newest
|
|
15
|
+
server = FakeServer(data, max_page_size=300, reverse_last_pages=True)
|
|
16
|
+
|
|
17
|
+
output = query.query(server)
|
|
18
|
+
|
|
19
|
+
ids = [event["id"] for event in output["events"]]
|
|
20
|
+
assert ids == [f"e{idx}" for idx in reversed(range(700))]
|
|
21
|
+
assert len(server.queries) == 3
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Unexpected GraphQl responses must not cause infinite loop.
|
|
2
|
+
|
|
3
|
+
Does not require running AYON server.
|
|
4
|
+
"""
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from ayon_api.exceptions import GraphQlQueryError
|
|
8
|
+
from ayon_api.graphql_queries import events_graphql_query
|
|
9
|
+
from ayon_api.utils import SortOrder
|
|
10
|
+
|
|
11
|
+
from .graphql_fake_server import FakeResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _ScriptedServer:
|
|
15
|
+
"""Return prepared responses, fail if queried too many times."""
|
|
16
|
+
def __init__(self, responses):
|
|
17
|
+
self._responses = responses
|
|
18
|
+
self.queries = []
|
|
19
|
+
|
|
20
|
+
def query_graphql(self, query_str, variables):
|
|
21
|
+
self.queries.append(query_str)
|
|
22
|
+
if len(self.queries) > 10:
|
|
23
|
+
raise RuntimeError("Infinite query loop")
|
|
24
|
+
idx = min(len(self.queries), len(self._responses)) - 1
|
|
25
|
+
return FakeResponse(self._responses[idx])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_null_data_raises_instead_of_infinite_loop():
|
|
29
|
+
server = _ScriptedServer([{"data": None}])
|
|
30
|
+
query = events_graphql_query({"id"}, SortOrder.ascending)
|
|
31
|
+
with pytest.raises(GraphQlQueryError, match="does not contain 'data'"):
|
|
32
|
+
query.query(server)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_missing_cursor_stops_pagination():
|
|
36
|
+
def page(ids, end_cursor):
|
|
37
|
+
return {"data": {"events": {
|
|
38
|
+
"edges": [{"node": {"id": id_}} for id_ in ids],
|
|
39
|
+
"pageInfo": {"endCursor": end_cursor, "hasNextPage": True},
|
|
40
|
+
}}}
|
|
41
|
+
|
|
42
|
+
server = _ScriptedServer([page(["e0"], "c0"), page([], None)])
|
|
43
|
+
query = events_graphql_query({"id"}, SortOrder.ascending)
|
|
44
|
+
|
|
45
|
+
with pytest.raises(GraphQlQueryError, match="page without a cursor"):
|
|
46
|
+
query.query(server)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Validation of operations result. Does not require running AYON server."""
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from ayon_api.exceptions import FailedOperations
|
|
5
|
+
from ayon_api.server_api import ServerAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def con():
|
|
10
|
+
return ServerAPI("http://localhost:0", create_session=False)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_failed_result_without_failed_operation_raises(con, capsys):
|
|
14
|
+
result = {"success": False, "operations": [{"id": "a", "success": True}]}
|
|
15
|
+
with pytest.raises(FailedOperations, match="Server response"):
|
|
16
|
+
con._validate_operations_result(result, [{"id": "a"}])
|
|
17
|
+
assert capsys.readouterr().out == ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_failed_operation_raises_with_detail(con):
|
|
21
|
+
result = {
|
|
22
|
+
"success": False,
|
|
23
|
+
"operations": [{"id": "a", "success": False, "detail": "Boom"}],
|
|
24
|
+
}
|
|
25
|
+
with pytest.raises(FailedOperations, match="Boom"):
|
|
26
|
+
con._validate_operations_result(result, [{"id": "a", "type": "x"}])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""'max_retries' request argument. Does not require running AYON server."""
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
from ayon_api.server_api import ServerAPI
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_max_retries_kwarg_is_not_passed_to_request(monkeypatch):
|
|
8
|
+
monkeypatch.setattr("time.sleep", lambda *args, **kwargs: None)
|
|
9
|
+
con = ServerAPI("http://localhost:0", create_session=False, max_retries=5)
|
|
10
|
+
calls = []
|
|
11
|
+
|
|
12
|
+
def request_func(url, **kwargs):
|
|
13
|
+
# Real request functions raise TypeError on unknown arguments
|
|
14
|
+
if "max_retries" in kwargs:
|
|
15
|
+
raise TypeError("unexpected keyword argument 'max_retries'")
|
|
16
|
+
calls.append(kwargs)
|
|
17
|
+
raise requests.exceptions.ConnectionError("down")
|
|
18
|
+
|
|
19
|
+
response = con._do_rest_request(
|
|
20
|
+
request_func,
|
|
21
|
+
"http://localhost:0/api/x",
|
|
22
|
+
handle_invalid_token=False,
|
|
23
|
+
max_retries=2,
|
|
24
|
+
)
|
|
25
|
+
assert len(calls) == 2
|
|
26
|
+
assert response.status_code == 500
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Retries of REST requests. Does not require running AYON server."""
|
|
2
|
+
import pytest
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from ayon_api.server_api import ServerAPI
|
|
6
|
+
|
|
7
|
+
from .fake_transfer import FakeResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def con(monkeypatch):
|
|
12
|
+
monkeypatch.setattr("time.sleep", lambda *args, **kwargs: None)
|
|
13
|
+
return ServerAPI("http://localhost:0", create_session=False, max_retries=3)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize(
|
|
17
|
+
"failure",
|
|
18
|
+
[requests.exceptions.ConnectionError("down"), FakeResponse(503)],
|
|
19
|
+
)
|
|
20
|
+
def test_successful_retry_returns_successful_response(con, failure):
|
|
21
|
+
responses = [failure, FakeResponse(200, json_data={"ok": True})]
|
|
22
|
+
|
|
23
|
+
# `_do_rest_request` expects a callable that takes a URL and returns a
|
|
24
|
+
# response. So we just return fake responses to simulate it.
|
|
25
|
+
def request_func(url, **kwargs):
|
|
26
|
+
item = responses.pop(0)
|
|
27
|
+
if isinstance(item, Exception):
|
|
28
|
+
raise item
|
|
29
|
+
return item
|
|
30
|
+
|
|
31
|
+
response = con._do_rest_request(
|
|
32
|
+
request_func, "http://localhost:0/api/x", handle_invalid_token=False
|
|
33
|
+
)
|
|
34
|
+
assert response.status_code == 200
|
|
35
|
+
assert response.data == {"ok": True}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ayon_python_api-1.2.22 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|