updated 'ayon_api' to '0.5.1'

This commit is contained in:
Jakub Trllo 2023-10-18 10:16:17 +02:00
parent 2088c7d7e6
commit 68f7826cf6
5 changed files with 194 additions and 87 deletions

View file

@ -602,12 +602,12 @@ def delete_installer(*args, **kwargs):
def download_installer(*args, **kwargs):
con = get_server_api_connection()
con.download_installer(*args, **kwargs)
return con.download_installer(*args, **kwargs)
def upload_installer(*args, **kwargs):
con = get_server_api_connection()
con.upload_installer(*args, **kwargs)
return con.upload_installer(*args, **kwargs)
# Dependency packages
@ -753,12 +753,12 @@ def get_secrets(*args, **kwargs):
def get_secret(*args, **kwargs):
con = get_server_api_connection()
return con.delete_secret(*args, **kwargs)
return con.get_secret(*args, **kwargs)
def save_secret(*args, **kwargs):
con = get_server_api_connection()
return con.delete_secret(*args, **kwargs)
return con.save_secret(*args, **kwargs)
def delete_secret(*args, **kwargs):
@ -978,12 +978,14 @@ def delete_project(project_name):
def get_thumbnail_by_id(project_name, thumbnail_id):
con = get_server_api_connection()
con.get_thumbnail_by_id(project_name, thumbnail_id)
return con.get_thumbnail_by_id(project_name, thumbnail_id)
def get_thumbnail(project_name, entity_type, entity_id, thumbnail_id=None):
con = get_server_api_connection()
con.get_thumbnail(project_name, entity_type, entity_id, thumbnail_id)
return con.get_thumbnail(
project_name, entity_type, entity_id, thumbnail_id
)
def get_folder_thumbnail(project_name, folder_id, thumbnail_id=None):

View file

@ -144,6 +144,7 @@ def product_types_query(fields):
query_queue.append((k, v, field))
return query
def project_product_types_query(fields):
query = GraphQlQuery("ProjectProductTypes")
project_query = query.add_field("project")
@ -175,6 +176,8 @@ def folders_graphql_query(fields):
parent_folder_ids_var = query.add_variable("parentFolderIds", "[String!]")
folder_paths_var = query.add_variable("folderPaths", "[String!]")
folder_names_var = query.add_variable("folderNames", "[String!]")
folder_types_var = query.add_variable("folderTypes", "[String!]")
statuses_var = query.add_variable("folderStatuses", "[String!]")
has_products_var = query.add_variable("folderHasProducts", "Boolean!")
project_field = query.add_field("project")
@ -185,6 +188,8 @@ def folders_graphql_query(fields):
folders_field.set_filter("parentIds", parent_folder_ids_var)
folders_field.set_filter("names", folder_names_var)
folders_field.set_filter("paths", folder_paths_var)
folders_field.set_filter("folderTypes", folder_types_var)
folders_field.set_filter("statuses", statuses_var)
folders_field.set_filter("hasProducts", has_products_var)
nested_fields = fields_to_dict(fields)

View file

@ -75,6 +75,7 @@ from .utils import (
TransferProgress,
create_dependency_package_basename,
ThumbnailContent,
get_default_timeout,
)
PatternType = type(re.compile(""))
@ -351,7 +352,6 @@ class ServerAPI(object):
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__(
@ -500,20 +500,13 @@ class ServerAPI(object):
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'.
Utils function 'get_default_timeout' is used by default.
Returns:
float: Timeout value in seconds.
"""
try:
return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY))
except (ValueError, TypeError):
pass
return cls._default_timeout
return get_default_timeout()
@classmethod
def get_default_max_retries(cls):
@ -662,13 +655,10 @@ class ServerAPI(object):
as default variant.
Args:
variant (Literal['production', 'staging']): Settings variant name.
variant (str): Settings variant name. It is possible to use
'production', 'staging' or name of dev bundle.
"""
if variant not in ("production", "staging"):
raise ValueError((
"Invalid variant name {}. Expected 'production' or 'staging'"
).format(variant))
self._default_settings_variant = variant
default_settings_variant = property(
@ -938,8 +928,8 @@ class ServerAPI(object):
int(re_match.group("major")),
int(re_match.group("minor")),
int(re_match.group("patch")),
re_match.group("prerelease"),
re_match.group("buildmetadata")
re_match.group("prerelease") or "",
re_match.group("buildmetadata") or "",
)
return self._server_version_tuple
@ -1140,31 +1130,41 @@ class ServerAPI(object):
response = None
new_response = None
for _ in range(max_retries):
for retry_idx in reversed(range(max_retries)):
try:
response = function(url, **kwargs)
break
except ConnectionRefusedError:
if retry_idx == 0:
self.log.warning(
"Connection error happened.", exc_info=True
)
# 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
# Log warning only on last attempt
if retry_idx == 0:
self.log.warning(
"Connection error happened.", exc_info=True
)
new_response = RestApiResponse(
None,
{"detail": "Unable to connect the server. Connection error"}
)
break
time.sleep(0.1)
@ -1349,7 +1349,9 @@ class ServerAPI(object):
status=None,
description=None,
summary=None,
payload=None
payload=None,
progress=None,
retries=None
):
kwargs = {
key: value
@ -1360,9 +1362,27 @@ class ServerAPI(object):
("description", description),
("summary", summary),
("payload", payload),
("progress", progress),
("retries", retries),
)
if value is not None
}
# 'progress' and 'retries' are available since 0.5.x server version
major, minor, _, _, _ = self.server_version_tuple
if (major, minor) < (0, 5):
args = []
if progress is not None:
args.append("progress")
if retries is not None:
args.append("retries")
fields = ", ".join("'{}'".format(f) for f in args)
ending = "s" if len(args) > 1 else ""
raise ValueError((
"Your server version '{}' does not support update"
" of {} field{} on event. The fields are supported since"
" server version '0.5'."
).format(self.get_server_version(), fields, ending))
response = self.patch(
"events/{}".format(event_id),
**kwargs
@ -1434,6 +1454,7 @@ class ServerAPI(object):
description=None,
sequential=None,
events_filter=None,
max_retries=None,
):
"""Enroll job based on events.
@ -1475,8 +1496,12 @@ class ServerAPI(object):
in target event.
sequential (Optional[bool]): The source topic must be processed
in sequence.
events_filter (Optional[ayon_server.sqlfilter.Filter]): A dict-like
with conditions to filter the source event.
events_filter (Optional[dict[str, Any]]): Filtering conditions
to filter the source event. For more technical specifications
look to server backed 'ayon_server.sqlfilter.Filter'.
TODO: Add example of filters.
max_retries (Optional[int]): How many times can be event retried.
Default value is based on server (3 at the time of this PR).
Returns:
Union[None, dict[str, Any]]: None if there is no event matching
@ -1487,6 +1512,7 @@ class ServerAPI(object):
"sourceTopic": source_topic,
"targetTopic": target_topic,
"sender": sender,
"maxRetries": max_retries,
}
if sequential is not None:
kwargs["sequential"] = sequential
@ -2236,6 +2262,34 @@ class ServerAPI(object):
response.raise_for_status("Failed to create/update dependency")
return response.data
def _get_dependency_package_route(
self, filename=None, platform_name=None
):
major, minor, patch, _, _ = self.server_version_tuple
if (major, minor, patch) <= (0, 2, 0):
# Backwards compatibility for AYON server 0.2.0 and lower
self.log.warning((
"Using deprecated dependency package route."
" Please update your AYON server to version 0.2.1 or higher."
" Backwards compatibility for this route will be removed"
" in future releases of ayon-python-api."
))
if platform_name is None:
platform_name = platform.system().lower()
base = "dependencies"
if not filename:
return base
return "{}/{}/{}".format(base, filename, platform_name)
if (major, minor) <= (0, 3):
endpoint = "desktop/dependency_packages"
else:
endpoint = "desktop/dependencyPackages"
if filename:
return "{}/{}".format(endpoint, filename)
return endpoint
def get_dependency_packages(self):
"""Information about dependency packages on server.
@ -2263,33 +2317,11 @@ class ServerAPI(object):
server.
"""
endpoint = "desktop/dependencyPackages"
major, minor, _, _, _ = self.server_version_tuple
if major == 0 and minor <= 3:
endpoint = "desktop/dependency_packages"
endpoint = self._get_dependency_package_route()
result = self.get(endpoint)
result.raise_for_status()
return result.data
def _get_dependency_package_route(
self, filename=None, platform_name=None
):
major, minor, patch, _, _ = self.server_version_tuple
if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)):
base = "desktop/dependency_packages"
if not filename:
return base
return "{}/{}".format(base, filename)
# Backwards compatibility for AYON server 0.2.0 and lower
if platform_name is None:
platform_name = platform.system().lower()
base = "dependencies"
if not filename:
return base
return "{}/{}/{}".format(base, filename, platform_name)
def create_dependency_package(
self,
filename,
@ -3515,7 +3547,9 @@ class ServerAPI(object):
folder_ids=None,
folder_paths=None,
folder_names=None,
folder_types=None,
parent_ids=None,
statuses=None,
active=True,
fields=None,
own_attributes=False
@ -3536,8 +3570,12 @@ class ServerAPI(object):
for filtering.
folder_names (Optional[Iterable[str]]): Folder names used
for filtering.
folder_types (Optional[Iterable[str]]): Folder types used
for filtering.
parent_ids (Optional[Iterable[str]]): Ids of folder parents.
Use 'None' if folder is direct child of project.
statuses (Optional[Iterable[str]]): Folder statuses used
for filtering.
active (Optional[bool]): Filter active/inactive folders.
Both are returned if is set to None.
fields (Optional[Iterable[str]]): Fields to be queried for
@ -3574,6 +3612,18 @@ class ServerAPI(object):
return
filters["folderNames"] = list(folder_names)
if folder_types is not None:
folder_types = set(folder_types)
if not folder_types:
return
filters["folderTypes"] = list(folder_types)
if statuses is not None:
statuses = set(statuses)
if not statuses:
return
filters["folderStatuses"] = list(statuses)
if parent_ids is not None:
parent_ids = set(parent_ids)
if not parent_ids:
@ -4312,9 +4362,6 @@ class ServerAPI(object):
fields.remove("attrib")
fields |= self.get_attributes_fields_for_type("version")
if active is not None:
fields.add("active")
# Make sure fields have minimum required fields
fields |= {"id", "version"}
@ -4323,6 +4370,9 @@ class ServerAPI(object):
use_rest = True
fields = {"id"}
if active is not None:
fields.add("active")
if own_attributes:
fields.add("ownAttrib")
@ -5845,19 +5895,22 @@ class ServerAPI(object):
"""Helper method to get links from server for entity types.
Example output:
[
{
"id": "59a212c0d2e211eda0e20242ac120002",
"linkType": "reference",
"description": "reference link between folders",
"projectName": "my_project",
"author": "frantadmin",
"entityId": "b1df109676db11ed8e8c6c9466b19aa8",
"entityType": "folder",
"direction": "out"
},
{
"59a212c0d2e211eda0e20242ac120001": [
{
"id": "59a212c0d2e211eda0e20242ac120002",
"linkType": "reference",
"description": "reference link between folders",
"projectName": "my_project",
"author": "frantadmin",
"entityId": "b1df109676db11ed8e8c6c9466b19aa8",
"entityType": "folder",
"direction": "out"
},
...
],
...
]
}
Args:
project_name (str): Project where links are.

View file

@ -1,3 +1,4 @@
import os
import re
import datetime
import uuid
@ -15,6 +16,7 @@ except ImportError:
import requests
import unidecode
from .constants import SERVER_TIMEOUT_ENV_KEY
from .exceptions import UrlError
REMOVED_VALUE = object()
@ -27,6 +29,23 @@ RepresentationParents = collections.namedtuple(
)
def get_default_timeout():
"""Default value for requests timeout.
First looks for environment variable SERVER_TIMEOUT_ENV_KEY which
can affect timeout value. If not available then use 10.0 s.
Returns:
float: Timeout value in seconds.
"""
try:
return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY))
except (ValueError, TypeError):
pass
return 10.0
class ThumbnailContent:
"""Wrapper for thumbnail content.
@ -231,30 +250,36 @@ def _try_parse_url(url):
return None
def _try_connect_to_server(url):
def _try_connect_to_server(url, timeout=None):
if timeout is None:
timeout = get_default_timeout()
try:
# TODO add validation if the url lead to Ayon server
# - thiw won't validate if the url lead to 'google.com'
requests.get(url)
# - this won't validate if the url lead to 'google.com'
requests.get(url, timeout=timeout)
except BaseException:
return False
return True
def login_to_server(url, username, password):
def login_to_server(url, username, password, timeout=None):
"""Use login to the server to receive token.
Args:
url (str): Server url.
username (str): User's username.
password (str): User's password.
timeout (Optional[float]): Timeout for request. Value from
'get_default_timeout' is used if not specified.
Returns:
Union[str, None]: User's token if login was successfull.
Otherwise 'None'.
"""
if timeout is None:
timeout = get_default_timeout()
headers = {"Content-Type": "application/json"}
response = requests.post(
"{}/api/auth/login".format(url),
@ -262,7 +287,8 @@ def login_to_server(url, username, password):
json={
"name": username,
"password": password
}
},
timeout=timeout,
)
token = None
# 200 - success
@ -273,47 +299,67 @@ def login_to_server(url, username, password):
return token
def logout_from_server(url, token):
def logout_from_server(url, token, timeout=None):
"""Logout from server and throw token away.
Args:
url (str): Url from which should be logged out.
token (str): Token which should be used to log out.
timeout (Optional[float]): Timeout for request. Value from
'get_default_timeout' is used if not specified.
"""
if timeout is None:
timeout = get_default_timeout()
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {}".format(token)
}
requests.post(
url + "/api/auth/logout",
headers=headers
headers=headers,
timeout=timeout,
)
def is_token_valid(url, token):
def is_token_valid(url, token, timeout=None):
"""Check if token is valid.
Token can be a user token or service api key.
Args:
url (str): Server url.
token (str): User's token.
timeout (Optional[float]): Timeout for request. Value from
'get_default_timeout' is used if not specified.
Returns:
bool: True if token is valid.
"""
headers = {
if timeout is None:
timeout = get_default_timeout()
base_headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {}".format(token)
}
response = requests.get(
"{}/api/users/me".format(url),
headers=headers
)
return response.status_code == 200
for header_value in (
{"Authorization": "Bearer {}".format(token)},
{"X-Api-Key": token},
):
headers = base_headers.copy()
headers.update(header_value)
response = requests.get(
"{}/api/users/me".format(url),
headers=headers,
timeout=timeout,
)
if response.status_code == 200:
return True
return False
def validate_url(url):
def validate_url(url, timeout=None):
"""Validate url if is valid and server is available.
Validation checks if can be parsed as url and contains scheme.
@ -334,6 +380,7 @@ def validate_url(url):
Args:
url (str): Server url.
timeout (Optional[int]): Timeout in seconds for connection to server.
Returns:
Url which was used to connect to server.
@ -369,10 +416,10 @@ def validate_url(url):
# - this will trigger UrlError if both will crash
if not parsed_url.scheme:
new_url = "https://" + modified_url
if _try_connect_to_server(new_url):
if _try_connect_to_server(new_url, timeout=timeout):
return new_url
if _try_connect_to_server(modified_url):
if _try_connect_to_server(modified_url, timeout=timeout):
return modified_url
hints = []

View file

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