Update ayon-python-api (#5512)

* query asset only if asset id is available

* updated ayon api

* fix subsets arguments
This commit is contained in:
Jakub Trllo 2023-08-29 12:01:29 +02:00 committed by GitHub
parent fd64e261df
commit 3eb2cc21b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 248 additions and 43 deletions

View file

@ -83,10 +83,10 @@ def _get_subsets(
project_name,
subset_ids,
subset_names,
folder_ids,
names_by_folder_ids,
active,
fields
folder_ids=folder_ids,
names_by_folder_ids=names_by_folder_ids,
active=active,
fields=fields,
):
yield convert_v4_subset_to_v3(subset)

View file

@ -75,7 +75,7 @@ class TasksModel(QtGui.QStandardItemModel):
def set_asset_id(self, asset_id):
asset_doc = None
if self._context_is_valid():
if asset_id and self._context_is_valid():
project_name = self._get_current_project()
asset_doc = get_asset_by_id(
project_name, asset_id, fields=["data.tasks"]

View file

@ -48,6 +48,11 @@ from ._api import (
patch,
delete,
get_timeout,
set_timeout,
get_max_retries,
set_max_retries,
get_event,
get_events,
dispatch_event,
@ -245,6 +250,11 @@ __all__ = (
"patch",
"delete",
"get_timeout",
"set_timeout",
"get_max_retries",
"set_max_retries",
"get_event",
"get_events",
"dispatch_event",

View file

@ -474,6 +474,26 @@ def delete(*args, **kwargs):
return con.delete(*args, **kwargs)
def get_timeout(*args, **kwargs):
con = get_server_api_connection()
return con.get_timeout(*args, **kwargs)
def set_timeout(*args, **kwargs):
con = get_server_api_connection()
return con.set_timeout(*args, **kwargs)
def get_max_retries(*args, **kwargs):
con = get_server_api_connection()
return con.get_max_retries(*args, **kwargs)
def set_max_retries(*args, **kwargs):
con = get_server_api_connection()
return con.set_max_retries(*args, **kwargs)
def get_event(*args, **kwargs):
con = get_server_api_connection()
return con.get_event(*args, **kwargs)

View file

@ -1,18 +1,21 @@
# Environments where server url and api key are stored for global connection
SERVER_URL_ENV_KEY = "AYON_SERVER_URL"
SERVER_API_ENV_KEY = "AYON_API_KEY"
SERVER_TIMEOUT_ENV_KEY = "AYON_SERVER_TIMEOUT"
SERVER_RETRIES_ENV_KEY = "AYON_SERVER_RETRIES"
# Backwards compatibility
SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY
# --- User ---
DEFAULT_USER_FIELDS = {
"roles",
"accessGroups",
"defaultAccessGroups",
"name",
"isService",
"isManager",
"isGuest",
"isAdmin",
"defaultRoles",
"createdAt",
"active",
"hasPassword",

View file

@ -247,9 +247,11 @@ def products_graphql_query(fields):
query = GraphQlQuery("ProductsQuery")
project_name_var = query.add_variable("projectName", "String!")
folder_ids_var = query.add_variable("folderIds", "[String!]")
product_ids_var = query.add_variable("productIds", "[String!]")
product_names_var = query.add_variable("productNames", "[String!]")
folder_ids_var = query.add_variable("folderIds", "[String!]")
product_types_var = query.add_variable("productTypes", "[String!]")
statuses_var = query.add_variable("statuses", "[String!]")
project_field = query.add_field("project")
project_field.set_filter("name", project_name_var)
@ -258,6 +260,8 @@ def products_graphql_query(fields):
products_field.set_filter("ids", product_ids_var)
products_field.set_filter("names", product_names_var)
products_field.set_filter("folderIds", folder_ids_var)
products_field.set_filter("productTypes", product_types_var)
products_field.set_filter("statuses", statuses_var)
nested_fields = fields_to_dict(set(fields))
add_links_fields(products_field, nested_fields)

View file

@ -2,6 +2,7 @@ import os
import re
import io
import json
import time
import logging
import collections
import platform
@ -26,6 +27,8 @@ except ImportError:
from json import JSONDecodeError as RequestsJSONDecodeError
from .constants import (
SERVER_TIMEOUT_ENV_KEY,
SERVER_RETRIES_ENV_KEY,
DEFAULT_PRODUCT_TYPE_FIELDS,
DEFAULT_PROJECT_FIELDS,
DEFAULT_FOLDER_FIELDS,
@ -127,6 +130,8 @@ class RestApiResponse(object):
@property
def text(self):
if self._response is None:
return self.detail
return self._response.text
@property
@ -135,6 +140,8 @@ class RestApiResponse(object):
@property
def headers(self):
if self._response is None:
return {}
return self._response.headers
@property
@ -148,6 +155,8 @@ class RestApiResponse(object):
@property
def content(self):
if self._response is None:
return b""
return self._response.content
@property
@ -339,7 +348,11 @@ class ServerAPI(object):
variable value 'AYON_CERT_FILE' by default.
create_session (Optional[bool]): Create session for connection if
token is available. Default is True.
timeout (Optional[float]): Timeout for requests.
max_retries (Optional[int]): Number of retries for requests.
"""
_default_timeout = 10.0
_default_max_retries = 3
def __init__(
self,
@ -352,6 +365,8 @@ class ServerAPI(object):
ssl_verify=None,
cert=None,
create_session=True,
timeout=None,
max_retries=None,
):
if not base_url:
raise ValueError("Invalid server URL {}".format(str(base_url)))
@ -370,6 +385,13 @@ class ServerAPI(object):
)
self._sender = sender
self._timeout = None
self._max_retries = None
# Set timeout and max retries based on passed values
self.set_timeout(timeout)
self.set_max_retries(max_retries)
if ssl_verify is None:
# Custom AYON env variable for CA file or 'True'
# - that should cover most default behaviors in 'requests'
@ -474,6 +496,87 @@ class ServerAPI(object):
ssl_verify = property(get_ssl_verify, set_ssl_verify)
cert = property(get_cert, set_cert)
@classmethod
def get_default_timeout(cls):
"""Default value for requests timeout.
First looks for environment variable SERVER_TIMEOUT_ENV_KEY which
can affect timeout value. If not available then use class
attribute '_default_timeout'.
Returns:
float: Timeout value in seconds.
"""
try:
return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY))
except (ValueError, TypeError):
pass
return cls._default_timeout
@classmethod
def get_default_max_retries(cls):
"""Default value for requests max retries.
First looks for environment variable SERVER_RETRIES_ENV_KEY, which
can affect max retries value. If not available then use class
attribute '_default_max_retries'.
Returns:
int: Max retries value.
"""
try:
return int(os.environ.get(SERVER_RETRIES_ENV_KEY))
except (ValueError, TypeError):
pass
return cls._default_max_retries
def get_timeout(self):
"""Current value for requests timeout.
Returns:
float: Timeout value in seconds.
"""
return self._timeout
def set_timeout(self, timeout):
"""Change timeout value for requests.
Args:
timeout (Union[float, None]): Timeout value in seconds.
"""
if timeout is None:
timeout = self.get_default_timeout()
self._timeout = float(timeout)
def get_max_retries(self):
"""Current value for requests max retries.
Returns:
int: Max retries value.
"""
return self._max_retries
def set_max_retries(self, max_retries):
"""Change max retries value for requests.
Args:
max_retries (Union[int, None]): Max retries value.
"""
if max_retries is None:
max_retries = self.get_default_max_retries()
self._max_retries = int(max_retries)
timeout = property(get_timeout, set_timeout)
max_retries = property(get_max_retries, set_max_retries)
@property
def access_token(self):
"""Access token used for authorization to server.
@ -890,9 +993,17 @@ class ServerAPI(object):
for attr, filter_value in filters.items():
query.set_variable_value(attr, filter_value)
# Backwards compatibility for server 0.3.x
# - will be removed in future releases
major, minor, _, _, _ = self.server_version_tuple
access_groups_field = "accessGroups"
if major == 0 and minor <= 3:
access_groups_field = "roles"
for parsed_data in query.continuous_query(self):
for user in parsed_data["users"]:
user["roles"] = json.loads(user["roles"])
user[access_groups_field] = json.loads(
user[access_groups_field])
yield user
def get_user(self, username=None):
@ -1004,6 +1115,10 @@ class ServerAPI(object):
logout_from_server(self._base_url, self._access_token)
def _do_rest_request(self, function, url, **kwargs):
kwargs.setdefault("timeout", self.timeout)
max_retries = kwargs.get("max_retries", self.max_retries)
if max_retries < 1:
max_retries = 1
if self._session is None:
# Validate token if was not yet validated
# - ignore validation if we're in middle of
@ -1023,38 +1138,54 @@ class ServerAPI(object):
elif isinstance(function, RequestType):
function = self._session_functions_mapping[function]
try:
response = function(url, **kwargs)
response = None
new_response = None
for _ in range(max_retries):
try:
response = function(url, **kwargs)
break
except ConnectionRefusedError:
# Server may be restarting
new_response = RestApiResponse(
None,
{"detail": "Unable to connect the server. Connection refused"}
)
except requests.exceptions.Timeout:
# Connection timed out
new_response = RestApiResponse(
None,
{"detail": "Connection timed out."}
)
except requests.exceptions.ConnectionError:
# Other connection error (ssl, etc) - does not make sense to
# try call server again
new_response = RestApiResponse(
None,
{"detail": "Unable to connect the server. Connection error"}
)
break
time.sleep(0.1)
if new_response is not None:
return new_response
content_type = response.headers.get("Content-Type")
if content_type == "application/json":
try:
new_response = RestApiResponse(response)
except JSONDecodeError:
new_response = RestApiResponse(
None,
{
"detail": "The response is not a JSON: {}".format(
response.text)
}
)
except ConnectionRefusedError:
new_response = RestApiResponse(
None,
{"detail": "Unable to connect the server. Connection refused"}
)
except requests.exceptions.ConnectionError:
new_response = RestApiResponse(
None,
{"detail": "Unable to connect the server. Connection error"}
)
else:
content_type = response.headers.get("Content-Type")
if content_type == "application/json":
try:
new_response = RestApiResponse(response)
except JSONDecodeError:
new_response = RestApiResponse(
None,
{
"detail": "The response is not a JSON: {}".format(
response.text)
}
)
elif content_type in ("image/jpeg", "image/png"):
new_response = RestApiResponse(response)
else:
new_response = RestApiResponse(response)
new_response = RestApiResponse(response)
self.log.debug("Response {}".format(str(new_response)))
return new_response
@ -1747,7 +1878,15 @@ class ServerAPI(object):
entity_type_defaults = DEFAULT_WORKFILE_INFO_FIELDS
elif entity_type == "user":
entity_type_defaults = DEFAULT_USER_FIELDS
entity_type_defaults = set(DEFAULT_USER_FIELDS)
# Backwards compatibility for server 0.3.x
# - will be removed in future releases
major, minor, _, _, _ = self.server_version_tuple
if major == 0 and minor <= 3:
entity_type_defaults.discard("accessGroups")
entity_type_defaults.discard("defaultAccessGroups")
entity_type_defaults.add("roles")
entity_type_defaults.add("defaultRoles")
else:
raise ValueError("Unknown entity type \"{}\"".format(entity_type))
@ -2124,7 +2263,12 @@ class ServerAPI(object):
server.
"""
result = self.get("desktop/dependency_packages")
endpoint = "desktop/dependencyPackages"
major, minor, _, _, _ = self.server_version_tuple
if major == 0 and minor <= 3:
endpoint = "desktop/dependency_packages"
result = self.get(endpoint)
result.raise_for_status()
return result.data
@ -3810,6 +3954,8 @@ class ServerAPI(object):
product_ids=None,
product_names=None,
folder_ids=None,
product_types=None,
statuses=None,
names_by_folder_ids=None,
active=True,
fields=None,
@ -3828,6 +3974,10 @@ class ServerAPI(object):
filtering.
folder_ids (Optional[Iterable[str]]): Ids of task parents.
Use 'None' if folder is direct child of project.
product_types (Optional[Iterable[str]]): Product types used for
filtering.
statuses (Optional[Iterable[str]]): Product statuses used for
filtering.
names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product
name filtering by folder id.
active (Optional[bool]): Filter active/inactive products.
@ -3862,6 +4012,18 @@ class ServerAPI(object):
if not filter_folder_ids:
return
filter_product_types = None
if product_types is not None:
filter_product_types = set(product_types)
if not filter_product_types:
return
filter_statuses = None
if statuses is not None:
filter_statuses = set(statuses)
if not filter_statuses:
return
# This will disable 'folder_ids' and 'product_names' filters
# - maybe could be enhanced in future?
if names_by_folder_ids is not None:
@ -3881,7 +4043,7 @@ class ServerAPI(object):
fields = set(fields) | {"id"}
if "attrib" in fields:
fields.remove("attrib")
fields |= self.get_attributes_fields_for_type("folder")
fields |= self.get_attributes_fields_for_type("product")
else:
fields = self.get_default_fields_for_type("product")
@ -3908,6 +4070,12 @@ class ServerAPI(object):
if filter_folder_ids:
filters["folderIds"] = list(filter_folder_ids)
if filter_product_types:
filters["productTypes"] = list(filter_product_types)
if filter_statuses:
filters["statuses"] = list(filter_statuses)
if product_ids:
filters["productIds"] = list(product_ids)

View file

@ -1,2 +1,2 @@
"""Package declaring Python API for Ayon server."""
__version__ = "0.3.5"
__version__ = "0.4.1"