Ayon-Python-Api 0.3.5__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/PKG-INFO +1 -1
  2. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/__init__.py +10 -0
  3. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/_api.py +20 -0
  4. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/constants.py +5 -2
  5. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/graphql_queries.py +5 -1
  6. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/server_api.py +196 -32
  7. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/version.py +1 -1
  8. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_python_api.egg-info/PKG-INFO +1 -1
  9. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/pyproject.toml +1 -1
  10. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/LICENSE +0 -0
  11. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/README.md +0 -0
  12. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/entity_hub.py +0 -0
  13. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/events.py +0 -0
  14. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/exceptions.py +0 -0
  15. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/graphql.py +0 -0
  16. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/operations.py +0 -0
  17. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_api/utils.py +0 -0
  18. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_python_api.egg-info/SOURCES.txt +0 -0
  19. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  20. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_python_api.egg-info/requires.txt +0 -0
  21. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/ayon_python_api.egg-info/top_level.txt +0 -0
  22. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/setup.cfg +0 -0
  23. {ayon-python-api-0.3.5 → ayon-python-api-0.4.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.3.5
3
+ Version: 0.4.0
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -48,6 +48,11 @@ from ._api import (
48
48
  patch,
49
49
  delete,
50
50
 
51
+ get_timeout,
52
+ set_timeout,
53
+ get_max_retries,
54
+ set_max_retries,
55
+
51
56
  get_event,
52
57
  get_events,
53
58
  dispatch_event,
@@ -245,6 +250,11 @@ __all__ = (
245
250
  "patch",
246
251
  "delete",
247
252
 
253
+ "get_timeout",
254
+ "set_timeout",
255
+ "get_max_retries",
256
+ "set_max_retries",
257
+
248
258
  "get_event",
249
259
  "get_events",
250
260
  "dispatch_event",
@@ -474,6 +474,26 @@ def delete(*args, **kwargs):
474
474
  return con.delete(*args, **kwargs)
475
475
 
476
476
 
477
+ def get_timeout(*args, **kwargs):
478
+ con = get_server_api_connection()
479
+ return con.get_timeout(*args, **kwargs)
480
+
481
+
482
+ def set_timeout(*args, **kwargs):
483
+ con = get_server_api_connection()
484
+ return con.set_timeout(*args, **kwargs)
485
+
486
+
487
+ def get_max_retries(*args, **kwargs):
488
+ con = get_server_api_connection()
489
+ return con.get_max_retries(*args, **kwargs)
490
+
491
+
492
+ def set_max_retries(*args, **kwargs):
493
+ con = get_server_api_connection()
494
+ return con.set_max_retries(*args, **kwargs)
495
+
496
+
477
497
  def get_event(*args, **kwargs):
478
498
  con = get_server_api_connection()
479
499
  return con.get_event(*args, **kwargs)
@@ -1,18 +1,21 @@
1
1
  # Environments where server url and api key are stored for global connection
2
2
  SERVER_URL_ENV_KEY = "AYON_SERVER_URL"
3
3
  SERVER_API_ENV_KEY = "AYON_API_KEY"
4
+ SERVER_TIMEOUT_ENV_KEY = "AYON_SERVER_TIMEOUT"
5
+ SERVER_RETRIES_ENV_KEY = "AYON_SERVER_RETRIES"
6
+
4
7
  # Backwards compatibility
5
8
  SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY
6
9
 
7
10
  # --- User ---
8
11
  DEFAULT_USER_FIELDS = {
9
- "roles",
12
+ "accessGroups",
13
+ "defaultAccessGroups",
10
14
  "name",
11
15
  "isService",
12
16
  "isManager",
13
17
  "isGuest",
14
18
  "isAdmin",
15
- "defaultRoles",
16
19
  "createdAt",
17
20
  "active",
18
21
  "hasPassword",
@@ -247,9 +247,11 @@ def products_graphql_query(fields):
247
247
  query = GraphQlQuery("ProductsQuery")
248
248
 
249
249
  project_name_var = query.add_variable("projectName", "String!")
250
- folder_ids_var = query.add_variable("folderIds", "[String!]")
251
250
  product_ids_var = query.add_variable("productIds", "[String!]")
252
251
  product_names_var = query.add_variable("productNames", "[String!]")
252
+ folder_ids_var = query.add_variable("folderIds", "[String!]")
253
+ product_types_var = query.add_variable("productTypes", "[String!]")
254
+ statuses_var = query.add_variable("statuses", "[String!]")
253
255
 
254
256
  project_field = query.add_field("project")
255
257
  project_field.set_filter("name", project_name_var)
@@ -258,6 +260,8 @@ def products_graphql_query(fields):
258
260
  products_field.set_filter("ids", product_ids_var)
259
261
  products_field.set_filter("names", product_names_var)
260
262
  products_field.set_filter("folderIds", folder_ids_var)
263
+ products_field.set_filter("productTypes", product_types_var)
264
+ products_field.set_filter("statuses", statuses_var)
261
265
 
262
266
  nested_fields = fields_to_dict(set(fields))
263
267
  add_links_fields(products_field, nested_fields)
@@ -2,6 +2,7 @@ import os
2
2
  import re
3
3
  import io
4
4
  import json
5
+ import time
5
6
  import logging
6
7
  import collections
7
8
  import platform
@@ -26,6 +27,8 @@ except ImportError:
26
27
  from json import JSONDecodeError as RequestsJSONDecodeError
27
28
 
28
29
  from .constants import (
30
+ SERVER_TIMEOUT_ENV_KEY,
31
+ SERVER_RETRIES_ENV_KEY,
29
32
  DEFAULT_PRODUCT_TYPE_FIELDS,
30
33
  DEFAULT_PROJECT_FIELDS,
31
34
  DEFAULT_FOLDER_FIELDS,
@@ -127,6 +130,8 @@ class RestApiResponse(object):
127
130
 
128
131
  @property
129
132
  def text(self):
133
+ if self._response is None:
134
+ return self.detail
130
135
  return self._response.text
131
136
 
132
137
  @property
@@ -135,6 +140,8 @@ class RestApiResponse(object):
135
140
 
136
141
  @property
137
142
  def headers(self):
143
+ if self._response is None:
144
+ return {}
138
145
  return self._response.headers
139
146
 
140
147
  @property
@@ -148,6 +155,8 @@ class RestApiResponse(object):
148
155
 
149
156
  @property
150
157
  def content(self):
158
+ if self._response is None:
159
+ return b""
151
160
  return self._response.content
152
161
 
153
162
  @property
@@ -339,7 +348,11 @@ class ServerAPI(object):
339
348
  variable value 'AYON_CERT_FILE' by default.
340
349
  create_session (Optional[bool]): Create session for connection if
341
350
  token is available. Default is True.
351
+ timeout (Optional[float]): Timeout for requests.
352
+ max_retries (Optional[int]): Number of retries for requests.
342
353
  """
354
+ _default_timeout = 10.0
355
+ _default_max_retries = 3
343
356
 
344
357
  def __init__(
345
358
  self,
@@ -352,6 +365,8 @@ class ServerAPI(object):
352
365
  ssl_verify=None,
353
366
  cert=None,
354
367
  create_session=True,
368
+ timeout=None,
369
+ max_retries=None,
355
370
  ):
356
371
  if not base_url:
357
372
  raise ValueError("Invalid server URL {}".format(str(base_url)))
@@ -370,6 +385,9 @@ class ServerAPI(object):
370
385
  )
371
386
  self._sender = sender
372
387
 
388
+ self._timeout = timeout
389
+ self._max_retries = max_retries
390
+
373
391
  if ssl_verify is None:
374
392
  # Custom AYON env variable for CA file or 'True'
375
393
  # - that should cover most default behaviors in 'requests'
@@ -474,6 +492,87 @@ class ServerAPI(object):
474
492
  ssl_verify = property(get_ssl_verify, set_ssl_verify)
475
493
  cert = property(get_cert, set_cert)
476
494
 
495
+ @classmethod
496
+ def get_default_timeout(cls):
497
+ """Default value for requests timeout.
498
+
499
+ First looks for environment variable SERVER_TIMEOUT_ENV_KEY which
500
+ can affect timeout value. If not available then use class
501
+ attribute '_default_timeout'.
502
+
503
+ Returns:
504
+ float: Timeout value in seconds.
505
+ """
506
+
507
+ try:
508
+ return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY))
509
+ except (ValueError, TypeError):
510
+ pass
511
+
512
+ return cls._default_timeout
513
+
514
+ @classmethod
515
+ def get_default_max_retries(cls):
516
+ """Default value for requests max retries.
517
+
518
+ First looks for environment variable SERVER_RETRIES_ENV_KEY, which
519
+ can affect max retries value. If not available then use class
520
+ attribute '_default_max_retries'.
521
+
522
+ Returns:
523
+ int: Max retries value.
524
+ """
525
+
526
+ try:
527
+ return int(os.environ.get(SERVER_RETRIES_ENV_KEY))
528
+ except (ValueError, TypeError):
529
+ pass
530
+
531
+ return cls._default_max_retries
532
+
533
+ def get_timeout(self):
534
+ """Current value for requests timeout.
535
+
536
+ Returns:
537
+ float: Timeout value in seconds.
538
+ """
539
+
540
+ return self._timeout
541
+
542
+ def set_timeout(self, timeout):
543
+ """Change timeout value for requests.
544
+
545
+ Args:
546
+ timeout (Union[float, None]): Timeout value in seconds.
547
+ """
548
+
549
+ if timeout is None:
550
+ timeout = self.get_default_timeout()
551
+ self._timeout = float(timeout)
552
+
553
+ def get_max_retries(self):
554
+ """Current value for requests max retries.
555
+
556
+ Returns:
557
+ int: Max retries value.
558
+ """
559
+
560
+ return self._max_retries
561
+
562
+ def set_max_retries(self, max_retries):
563
+ """Change max retries value for requests.
564
+
565
+ Args:
566
+ max_retries (Union[int, None]): Max retries value.
567
+ """
568
+
569
+ if max_retries is None:
570
+ max_retries = self.get_default_max_retries()
571
+ self._max_retries = int(max_retries)
572
+
573
+ timeout = property(get_timeout, set_timeout)
574
+ max_retries = property(get_max_retries, set_max_retries)
575
+
477
576
  @property
478
577
  def access_token(self):
479
578
  """Access token used for authorization to server.
@@ -890,9 +989,17 @@ class ServerAPI(object):
890
989
  for attr, filter_value in filters.items():
891
990
  query.set_variable_value(attr, filter_value)
892
991
 
992
+ # Backwards compatibility for server 0.3.x
993
+ # - will be removed in future releases
994
+ major, minor, _, _, _ = self.server_version_tuple
995
+ access_groups_field = "accessGroups"
996
+ if major == 0 and minor <= 3:
997
+ access_groups_field = "roles"
998
+
893
999
  for parsed_data in query.continuous_query(self):
894
1000
  for user in parsed_data["users"]:
895
- user["roles"] = json.loads(user["roles"])
1001
+ user[access_groups_field] = json.loads(
1002
+ user[access_groups_field])
896
1003
  yield user
897
1004
 
898
1005
  def get_user(self, username=None):
@@ -1004,6 +1111,10 @@ class ServerAPI(object):
1004
1111
  logout_from_server(self._base_url, self._access_token)
1005
1112
 
1006
1113
  def _do_rest_request(self, function, url, **kwargs):
1114
+ kwargs.setdefault("timeout", self.timeout)
1115
+ max_retries = kwargs.get("max_retries", self.max_retries)
1116
+ if max_retries < 1:
1117
+ max_retries = 1
1007
1118
  if self._session is None:
1008
1119
  # Validate token if was not yet validated
1009
1120
  # - ignore validation if we're in middle of
@@ -1023,38 +1134,54 @@ class ServerAPI(object):
1023
1134
  elif isinstance(function, RequestType):
1024
1135
  function = self._session_functions_mapping[function]
1025
1136
 
1026
- try:
1027
- response = function(url, **kwargs)
1137
+ response = None
1138
+ new_response = None
1139
+ for _ in range(max_retries):
1140
+ try:
1141
+ response = function(url, **kwargs)
1142
+ break
1143
+
1144
+ except ConnectionRefusedError:
1145
+ # Server may be restarting
1146
+ new_response = RestApiResponse(
1147
+ None,
1148
+ {"detail": "Unable to connect the server. Connection refused"}
1149
+ )
1150
+ except requests.exceptions.Timeout:
1151
+ # Connection timed out
1152
+ new_response = RestApiResponse(
1153
+ None,
1154
+ {"detail": "Connection timed out."}
1155
+ )
1156
+ except requests.exceptions.ConnectionError:
1157
+ # Other connection error (ssl, etc) - does not make sense to
1158
+ # try call server again
1159
+ new_response = RestApiResponse(
1160
+ None,
1161
+ {"detail": "Unable to connect the server. Connection error"}
1162
+ )
1163
+ break
1028
1164
 
1029
- except ConnectionRefusedError:
1030
- new_response = RestApiResponse(
1031
- None,
1032
- {"detail": "Unable to connect the server. Connection refused"}
1033
- )
1034
- except requests.exceptions.ConnectionError:
1035
- new_response = RestApiResponse(
1036
- None,
1037
- {"detail": "Unable to connect the server. Connection error"}
1038
- )
1039
- else:
1040
- content_type = response.headers.get("Content-Type")
1041
- if content_type == "application/json":
1042
- try:
1043
- new_response = RestApiResponse(response)
1044
- except JSONDecodeError:
1045
- new_response = RestApiResponse(
1046
- None,
1047
- {
1048
- "detail": "The response is not a JSON: {}".format(
1049
- response.text)
1050
- }
1051
- )
1165
+ time.sleep(0.1)
1052
1166
 
1053
- elif content_type in ("image/jpeg", "image/png"):
1054
- new_response = RestApiResponse(response)
1167
+ if new_response is not None:
1168
+ return new_response
1055
1169
 
1056
- else:
1170
+ content_type = response.headers.get("Content-Type")
1171
+ if content_type == "application/json":
1172
+ try:
1057
1173
  new_response = RestApiResponse(response)
1174
+ except JSONDecodeError:
1175
+ new_response = RestApiResponse(
1176
+ None,
1177
+ {
1178
+ "detail": "The response is not a JSON: {}".format(
1179
+ response.text)
1180
+ }
1181
+ )
1182
+
1183
+ else:
1184
+ new_response = RestApiResponse(response)
1058
1185
 
1059
1186
  self.log.debug("Response {}".format(str(new_response)))
1060
1187
  return new_response
@@ -1747,7 +1874,15 @@ class ServerAPI(object):
1747
1874
  entity_type_defaults = DEFAULT_WORKFILE_INFO_FIELDS
1748
1875
 
1749
1876
  elif entity_type == "user":
1750
- entity_type_defaults = DEFAULT_USER_FIELDS
1877
+ entity_type_defaults = set(DEFAULT_USER_FIELDS)
1878
+ # Backwards compatibility for server 0.3.x
1879
+ # - will be removed in future releases
1880
+ major, minor, _, _, _ = self.server_version_tuple
1881
+ if major == 0 and minor <= 3:
1882
+ entity_type_defaults.discard("accessGroups")
1883
+ entity_type_defaults.discard("defaultAccessGroups")
1884
+ entity_type_defaults.add("roles")
1885
+ entity_type_defaults.add("defaultRoles")
1751
1886
 
1752
1887
  else:
1753
1888
  raise ValueError("Unknown entity type \"{}\"".format(entity_type))
@@ -2124,7 +2259,12 @@ class ServerAPI(object):
2124
2259
  server.
2125
2260
  """
2126
2261
 
2127
- result = self.get("desktop/dependency_packages")
2262
+ endpoint = "desktop/dependencyPackages"
2263
+ major, minor, _, _, _ = self.server_version_tuple
2264
+ if major == 0 and minor <= 3:
2265
+ endpoint = "desktop/dependency_packages"
2266
+
2267
+ result = self.get(endpoint)
2128
2268
  result.raise_for_status()
2129
2269
  return result.data
2130
2270
 
@@ -3810,6 +3950,8 @@ class ServerAPI(object):
3810
3950
  product_ids=None,
3811
3951
  product_names=None,
3812
3952
  folder_ids=None,
3953
+ product_types=None,
3954
+ statuses=None,
3813
3955
  names_by_folder_ids=None,
3814
3956
  active=True,
3815
3957
  fields=None,
@@ -3828,6 +3970,10 @@ class ServerAPI(object):
3828
3970
  filtering.
3829
3971
  folder_ids (Optional[Iterable[str]]): Ids of task parents.
3830
3972
  Use 'None' if folder is direct child of project.
3973
+ product_types (Optional[Iterable[str]]): Product types used for
3974
+ filtering.
3975
+ statuses (Optional[Iterable[str]]): Product statuses used for
3976
+ filtering.
3831
3977
  names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product
3832
3978
  name filtering by folder id.
3833
3979
  active (Optional[bool]): Filter active/inactive products.
@@ -3862,6 +4008,18 @@ class ServerAPI(object):
3862
4008
  if not filter_folder_ids:
3863
4009
  return
3864
4010
 
4011
+ filter_product_types = None
4012
+ if product_types is not None:
4013
+ filter_product_types = set(product_types)
4014
+ if not filter_product_types:
4015
+ return
4016
+
4017
+ filter_statuses = None
4018
+ if statuses is not None:
4019
+ filter_statuses = set(statuses)
4020
+ if not filter_statuses:
4021
+ return
4022
+
3865
4023
  # This will disable 'folder_ids' and 'product_names' filters
3866
4024
  # - maybe could be enhanced in future?
3867
4025
  if names_by_folder_ids is not None:
@@ -3881,7 +4039,7 @@ class ServerAPI(object):
3881
4039
  fields = set(fields) | {"id"}
3882
4040
  if "attrib" in fields:
3883
4041
  fields.remove("attrib")
3884
- fields |= self.get_attributes_fields_for_type("folder")
4042
+ fields |= self.get_attributes_fields_for_type("product")
3885
4043
  else:
3886
4044
  fields = self.get_default_fields_for_type("product")
3887
4045
 
@@ -3908,6 +4066,12 @@ class ServerAPI(object):
3908
4066
  if filter_folder_ids:
3909
4067
  filters["folderIds"] = list(filter_folder_ids)
3910
4068
 
4069
+ if filter_product_types:
4070
+ filters["productTypes"] = list(filter_product_types)
4071
+
4072
+ if filter_statuses:
4073
+ filters["statuses"] = list(filter_statuses)
4074
+
3911
4075
  if product_ids:
3912
4076
  filters["productIds"] = list(product_ids)
3913
4077
 
@@ -1,2 +1,2 @@
1
1
  """Package declaring Python API for Ayon server."""
2
- __version__ = "0.3.5"
2
+ __version__ = "0.4.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.3.5
3
+ Version: 0.4.0
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ayon-python-api"
3
- version = "0.3.5"
3
+ version = "0.4.0"
4
4
  description = "AYON Python API"
5
5
  license = {file = "LICENSE"}
6
6
  readme = {file = "README.md", content-type = "text/markdown"}
File without changes