ayon-python-api 1.2.23__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.23 → ayon_python_api-1.2.24}/PKG-INFO +1 -1
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api.py +4 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/dependency_packages.py +6 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/graphql.py +38 -34
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/server_api.py +38 -9
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/utils.py +2 -2
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/version.py +1 -1
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/PKG-INFO +1 -1
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/SOURCES.txt +6 -0
- {ayon_python_api-1.2.23 → 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.23 → ayon_python_api-1.2.24}/LICENSE +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/README.md +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/__init__.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/__init__.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/actions.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/activities.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/attributes.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/base.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/bundles_addons.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/events.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/folders.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/installers.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/links.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/lists.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/products.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/projects.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/representations.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/secrets.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/tasks.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/thumbnails.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/versions.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/_api_helpers/workfiles.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/constants.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/entity_hub.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/events.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/exceptions.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/graphql_queries.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/operations.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_api/typing.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/dependency_links.txt +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/requires.txt +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/ayon_python_api.egg-info/top_level.txt +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/setup.cfg +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/setup.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/tests/test_entity_hub.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/tests/test_folder_hierarchy.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/tests/test_get_events.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/tests/test_graphql_queries.py +0 -0
- {ayon_python_api-1.2.23 → ayon_python_api-1.2.24}/tests/test_server.py +0 -0
|
@@ -1943,6 +1943,7 @@ def create_dependency_package(
|
|
|
1943
1943
|
checksum: str,
|
|
1944
1944
|
checksum_algorithm: str,
|
|
1945
1945
|
file_size: int,
|
|
1946
|
+
python_version: str | None = None,
|
|
1946
1947
|
sources: Optional[list[dict[str, Any]]] = None,
|
|
1947
1948
|
platform_name: Optional[str] = None,
|
|
1948
1949
|
) -> None:
|
|
@@ -1968,6 +1969,8 @@ def create_dependency_package(
|
|
|
1968
1969
|
checksum (str): Checksum of archive file where dependencies are.
|
|
1969
1970
|
checksum_algorithm (str): Algorithm used to calculate checksum.
|
|
1970
1971
|
file_size (Optional[int]): Size of file.
|
|
1972
|
+
python_version (str | None): Python version for which
|
|
1973
|
+
is dependency package created.
|
|
1971
1974
|
sources (Optional[list[dict[str, Any]]]): Information about
|
|
1972
1975
|
sources from where it is possible to get file.
|
|
1973
1976
|
platform_name (Optional[str]): Name of platform for which is
|
|
@@ -1984,6 +1987,7 @@ def create_dependency_package(
|
|
|
1984
1987
|
checksum=checksum,
|
|
1985
1988
|
checksum_algorithm=checksum_algorithm,
|
|
1986
1989
|
file_size=file_size,
|
|
1990
|
+
python_version=python_version,
|
|
1987
1991
|
sources=sources,
|
|
1988
1992
|
platform_name=platform_name,
|
|
1989
1993
|
)
|
{ayon_python_api-1.2.23 → 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
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -2676,24 +2697,32 @@ class ServerAPI(
|
|
|
2676
2697
|
if result.get("success"):
|
|
2677
2698
|
return None
|
|
2678
2699
|
|
|
2679
|
-
|
|
2680
|
-
|
|
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 []:
|
|
2681
2705
|
if op_result["success"]:
|
|
2682
2706
|
continue
|
|
2683
2707
|
|
|
2684
2708
|
operation_id = op_result["id"]
|
|
2685
2709
|
operation = next(
|
|
2686
|
-
op
|
|
2687
|
-
|
|
2688
|
-
if op["id"] == operation_id
|
|
2710
|
+
(op for op in operations_body if op["id"] == operation_id),
|
|
2711
|
+
op_result,
|
|
2689
2712
|
)
|
|
2690
2713
|
detail = op_result["detail"]
|
|
2691
2714
|
raise FailedOperations(
|
|
2692
2715
|
f"Operation \"{operation_id}\" failed with data:"
|
|
2693
|
-
f"\n{json.dumps(operation, indent=4)}"
|
|
2716
|
+
f"\n{json.dumps(operation, indent=4, default=str)}"
|
|
2694
2717
|
f"\nDetail: {detail}."
|
|
2695
2718
|
)
|
|
2696
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
|
+
|
|
2697
2726
|
def _prepare_fields(
|
|
2698
2727
|
self,
|
|
2699
2728
|
entity_type: str,
|
|
@@ -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
|
|
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.23 → 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
|