Ayon-Python-Api 0.1.9__tar.gz → 0.1.11__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 (25) hide show
  1. {ayon-python-api-0.1.9/ayon_python_api.egg-info → ayon-python-api-0.1.11}/PKG-INFO +6 -8
  2. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/README.md +6 -8
  3. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/__init__.py +21 -6
  4. ayon-python-api-0.1.9/ayon_api/server_api.py → ayon-python-api-0.1.11/ayon_api/_api.py +115 -12
  5. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/constants.py +15 -0
  6. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/entity_hub.py +2 -2
  7. ayon-python-api-0.1.11/ayon_api/events.py +52 -0
  8. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/graphql_queries.py +33 -0
  9. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/operations.py +1 -1
  10. ayon-python-api-0.1.9/ayon_api/server.py → ayon-python-api-0.1.11/ayon_api/server_api.py +319 -13
  11. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/version.py +1 -1
  12. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11/ayon_python_api.egg-info}/PKG-INFO +6 -8
  13. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_python_api.egg-info/SOURCES.txt +1 -1
  14. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_python_api.egg-info/requires.txt +1 -1
  15. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/pyproject.toml +2 -2
  16. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/setup.py +1 -1
  17. ayon-python-api-0.1.9/ayon_api/events.py +0 -105
  18. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/LICENSE +0 -0
  19. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/exceptions.py +0 -0
  20. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/graphql.py +0 -0
  21. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/thumbnails.py +0 -0
  22. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_api/utils.py +0 -0
  23. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  24. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/ayon_python_api.egg-info/top_level.txt +0 -0
  25. {ayon-python-api-0.1.9 → ayon-python-api-0.1.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -9,20 +9,18 @@ License: Apache License (2.0)
9
9
  Description: # AYON server API
10
10
  Python client for connection server. Client must be (at least for some time) Python 2 compatible because will be used in DCC that are "older".
11
11
 
12
- Client should support connection to server with raw REST functions and prepared functionality for work with entities. Must not contain only functionality that can be used with core server functionality.
12
+ AYON Python api should support connection to server with raw REST functions and prepared functionality for work with entities. Must not contain only functionality that can be used with core server functionality.
13
13
 
14
+ Module support singleton connection which is using `AYON_SERVER_URL` and `AYON_TOKEN` environment variables as source for connection. The singleton connection is using `ServerAPI` object. There can be created multiple connection to different server at one time, for that purpose use `ServerAPIBase` object.
14
15
 
15
16
  ## TODOs
16
- - Current implementation contains prepared functions for entity queries which is not using full potential of OpenPype server but is MongoDB compatible.
17
- - only functions related to v4 should stay at that place and v3 compatible functions should be moved
18
- - Missing settings getter
19
- - Missing CRUD operations for entities (Only read is possible)
20
- - More clear what is difference in `ServerAPIBase` and `ServerAPI`
17
+ - More clear what is difference in `ServerAPIBase` and `ServerAPI` (`server.py` and `server_api.py`) and better names
21
18
  - `ServerAPI` was added primarily for desktop app which handle login and logout in a different way so the class should be maybe removed and `ServerAPIBase` should be renamed to `ServerAPI`
22
19
  - find more suitable names of classes
23
- - find more suitable name of objects (right now is used `connection` or `conn`)
20
+ - find more suitable name of objects (right now is used `connection` or `con`)
24
21
  - Add folder and task changes to operations
25
22
  - Entity hub should use operations to update folders and tasks
23
+
26
24
  Keywords: AYON,ynput,OpenPype,vfx
27
25
  Platform: UNKNOWN
28
26
  Classifier: Development Status :: 3 - Alpha
@@ -1,17 +1,15 @@
1
1
  # AYON server API
2
2
  Python client for connection server. Client must be (at least for some time) Python 2 compatible because will be used in DCC that are "older".
3
3
 
4
- Client should support connection to server with raw REST functions and prepared functionality for work with entities. Must not contain only functionality that can be used with core server functionality.
4
+ AYON Python api should support connection to server with raw REST functions and prepared functionality for work with entities. Must not contain only functionality that can be used with core server functionality.
5
5
 
6
+ Module support singleton connection which is using `AYON_SERVER_URL` and `AYON_TOKEN` environment variables as source for connection. The singleton connection is using `ServerAPI` object. There can be created multiple connection to different server at one time, for that purpose use `ServerAPIBase` object.
6
7
 
7
8
  ## TODOs
8
- - Current implementation contains prepared functions for entity queries which is not using full potential of OpenPype server but is MongoDB compatible.
9
- - only functions related to v4 should stay at that place and v3 compatible functions should be moved
10
- - Missing settings getter
11
- - Missing CRUD operations for entities (Only read is possible)
12
- - More clear what is difference in `ServerAPIBase` and `ServerAPI`
9
+ - More clear what is difference in `ServerAPIBase` and `ServerAPI` (`server.py` and `server_api.py`) and better names
13
10
  - `ServerAPI` was added primarily for desktop app which handle login and logout in a different way so the class should be maybe removed and `ServerAPIBase` should be renamed to `ServerAPI`
14
11
  - find more suitable names of classes
15
- - find more suitable name of objects (right now is used `connection` or `conn`)
12
+ - find more suitable name of objects (right now is used `connection` or `con`)
16
13
  - Add folder and task changes to operations
17
- - Entity hub should use operations to update folders and tasks
14
+ - Entity hub should use operations to update folders and tasks
15
+
@@ -2,21 +2,27 @@ from .utils import (
2
2
  TransferProgress,
3
3
  slugify_string,
4
4
  )
5
- from .server import (
6
- ServerAPIBase,
7
- )
8
-
9
5
  from .server_api import (
10
6
  ServerAPI,
7
+ )
8
+
9
+ from ._api import (
10
+ GlobalServerAPI,
11
11
  ServiceContext,
12
12
 
13
13
  init_service,
14
+ get_service_addon_name,
15
+ get_service_addon_version,
16
+ get_service_addon_settings,
14
17
 
15
18
  is_connection_created,
19
+ create_connection,
16
20
  close_connection,
17
21
  change_token,
18
22
  set_environments,
19
23
  get_server_api_connection,
24
+ set_site_id,
25
+ set_client_version,
20
26
 
21
27
  get_base_url,
22
28
  get_rest_url,
@@ -34,6 +40,7 @@ from .server_api import (
34
40
  delete,
35
41
 
36
42
  get_event,
43
+ get_events,
37
44
  dispatch_event,
38
45
  update_event,
39
46
  enroll_event_job,
@@ -114,17 +121,24 @@ __all__ = (
114
121
  "TransferProgress",
115
122
  "slugify_string",
116
123
 
117
- "ServerAPIBase",
118
-
119
124
  "ServerAPI",
125
+
126
+ "GlobalServerAPI",
120
127
  "ServiceContext",
128
+
121
129
  "init_service",
130
+ "get_service_addon_name",
131
+ "get_service_addon_version",
132
+ "get_service_addon_settings",
122
133
 
123
134
  "is_connection_created",
135
+ "create_connection",
124
136
  "close_connection",
125
137
  "change_token",
126
138
  "set_environments",
127
139
  "get_server_api_connection",
140
+ "set_site_id",
141
+ "set_client_version",
128
142
 
129
143
  "get_base_url",
130
144
  "get_rest_url",
@@ -142,6 +156,7 @@ __all__ = (
142
156
  "delete",
143
157
 
144
158
  "get_event",
159
+ "get_events",
145
160
  "dispatch_event",
146
161
  "update_event",
147
162
  "enroll_event_job",
@@ -1,3 +1,11 @@
1
+ """Singleton based server api for direct access.
2
+
3
+ This implementation will be probably the most used part of package. Gives
4
+ option to have singleton connection to Server URL based on environment variable
5
+ values. All public functions and classes are imported in '__init__.py' so
6
+ they're available directly in top module import.
7
+ """
8
+
1
9
  import os
2
10
  import socket
3
11
 
@@ -5,11 +13,11 @@ from .constants import (
5
13
  SERVER_URL_ENV_KEY,
6
14
  SERVER_TOKEN_ENV_KEY,
7
15
  )
8
- from .server import ServerAPIBase
16
+ from .server_api import ServerAPI
9
17
  from .exceptions import FailedServiceInit
10
18
 
11
19
 
12
- class ServerAPI(ServerAPIBase):
20
+ class GlobalServerAPI(ServerAPI):
13
21
  """Extended server api which also handles storing tokens and url.
14
22
 
15
23
  Created object expect to have set environment variables
@@ -17,11 +25,11 @@ class ServerAPI(ServerAPIBase):
17
25
  but that can be filled afterwards with calling 'login' method.
18
26
  """
19
27
 
20
- def __init__(self):
28
+ def __init__(self, site_id=None, client_version=None):
21
29
  url = self.get_url()
22
30
  token = self.get_token()
23
31
 
24
- super(ServerAPI, self).__init__(url, token)
32
+ super(GlobalServerAPI, self).__init__(url, token, site_id, client_version)
25
33
 
26
34
  self.validate_server_availability()
27
35
  self.create_session()
@@ -34,7 +42,7 @@ class ServerAPI(ServerAPIBase):
34
42
  """
35
43
 
36
44
  previous_token = self._access_token
37
- super(ServerAPI, self).login(username, password)
45
+ super(GlobalServerAPI, self).login(username, password)
38
46
  if self.has_valid_token and previous_token != self._access_token:
39
47
  os.environ[SERVER_TOKEN_ENV_KEY] = self._access_token
40
48
 
@@ -74,7 +82,7 @@ class GlobalContext:
74
82
 
75
83
  @classmethod
76
84
  def change_token(cls, url, token):
77
- ServerAPI.set_environments(url, token)
85
+ GlobalServerAPI.set_environments(url, token)
78
86
  if cls._connection is None:
79
87
  return
80
88
 
@@ -89,10 +97,17 @@ class GlobalContext:
89
97
  cls._connection.close_session()
90
98
  cls._connection = None
91
99
 
100
+ @classmethod
101
+ def create_connection(cls, *args, **kwargs):
102
+ if cls._connection is not None:
103
+ cls.close_connection()
104
+ cls._connection = GlobalServerAPI(*args, **kwargs)
105
+ return cls._connection
106
+
92
107
  @classmethod
93
108
  def get_server_api_connection(cls):
94
109
  if cls._connection is None:
95
- cls._connection = ServerAPI()
110
+ cls.create_connection()
96
111
  return cls._connection
97
112
 
98
113
 
@@ -170,8 +185,8 @@ class ServiceContext:
170
185
  cls.addon_version = addon_version
171
186
  cls.service_name = service_name or socket.gethostname()
172
187
 
173
- # Make sure required environments for ServerAPI are set
174
- ServerAPI.set_environments(cls.server_url, cls.token)
188
+ # Make sure required environments for GlobalServerAPI are set
189
+ GlobalServerAPI.set_environments(cls.server_url, cls.token)
175
190
 
176
191
  if connect:
177
192
  print("Connecting to server \"{}\"".format(server_url))
@@ -181,9 +196,52 @@ class ServiceContext:
181
196
 
182
197
 
183
198
  def init_service(*args, **kwargs):
199
+ """Initialize current connection from service.
200
+
201
+ The service expect specific environment variables. The variables must all
202
+ be set to make the connection work as a service.
203
+ """
204
+
184
205
  ServiceContext.init_service(*args, **kwargs)
185
206
 
186
207
 
208
+ def get_service_addon_name():
209
+ """Name of addon which initialized service connection.
210
+
211
+ Returns:
212
+ Union[str, None]: Name of addon or None.
213
+ """
214
+
215
+ return ServiceContext.addon_name
216
+
217
+
218
+ def get_service_addon_version():
219
+ """Version of addon which initialized service connection.
220
+
221
+ Returns:
222
+ Union[str, None]: Version of addon or None.
223
+ """
224
+
225
+ return ServiceContext.addon_version
226
+
227
+
228
+ def get_service_addon_settings():
229
+ """Addon settings of service which initialized service.
230
+
231
+ Returns:
232
+ Dict[str, Any]: Addon settings.
233
+
234
+ Raises:
235
+ ValueError: When service was not initialized.
236
+ """
237
+
238
+ addon_name = get_service_addon_name()
239
+ addon_version = get_service_addon_version()
240
+ if addon_name is None or addon_version is None:
241
+ raise ValueError("Service is not initialized")
242
+ return get_addon_settings(addon_name, addon_version)
243
+
244
+
187
245
  def is_connection_created():
188
246
  """Is global connection created.
189
247
 
@@ -194,6 +252,20 @@ def is_connection_created():
194
252
  return GlobalContext.is_connection_created()
195
253
 
196
254
 
255
+ def create_connection(site_id=None, client_version=None):
256
+ """Create global connection.
257
+
258
+ Args:
259
+ site_id (str): Machine site id/name.
260
+ client_version (str): Desktop app version.
261
+
262
+ Returns:
263
+ GlobalServerAPI: Created connection.
264
+ """
265
+
266
+ return GlobalContext.create_connection(site_id, client_version)
267
+
268
+
197
269
  def close_connection():
198
270
  """Close global connection if is connected."""
199
271
 
@@ -221,22 +293,48 @@ def set_environments(url, token):
221
293
  token (Union[str, None]): API key token to be used for connection.
222
294
  """
223
295
 
224
- ServerAPI.set_environments(url, token)
296
+ GlobalServerAPI.set_environments(url, token)
225
297
 
226
298
 
227
299
  def get_server_api_connection():
228
- """Access to global scope object of ServerAPI.
300
+ """Access to global scope object of GlobalServerAPI.
229
301
 
230
302
  This access expect to have set environment variables 'AYON_SERVER_URL'
231
303
  and 'AYON_TOKEN'.
232
304
 
233
305
  Returns:
234
- ServerAPI: Object of connection to server.
306
+ GlobalServerAPI: Object of connection to server.
235
307
  """
236
308
 
237
309
  return GlobalContext.get_server_api_connection()
238
310
 
239
311
 
312
+ def set_site_id(site_id):
313
+ """Set site id of already connected client connection.
314
+
315
+ Site id is human-readable machine id used in AYON desktop application.
316
+
317
+ Args:
318
+ site_id (Union[str, None]): Site id used in connection.
319
+ """
320
+
321
+ con = get_server_api_connection()
322
+ con.set_site_id(site_id)
323
+
324
+
325
+ def set_client_version(client_version):
326
+ """Set version of already connected client connection.
327
+
328
+ Client version is version of AYON desktop application.
329
+
330
+ Args:
331
+ client_version (Union[str, None]): Client version string.
332
+ """
333
+
334
+ con = get_server_api_connection()
335
+ con.set_client_version(client_version)
336
+
337
+
240
338
  def get_base_url():
241
339
  con = get_server_api_connection()
242
340
  return con.get_base_url()
@@ -302,6 +400,11 @@ def get_event(*args, **kwargs):
302
400
  return con.get_event(*args, **kwargs)
303
401
 
304
402
 
403
+ def get_events(*args, **kwargs):
404
+ con = get_server_api_connection()
405
+ return con.get_events(*args, **kwargs)
406
+
407
+
305
408
  def dispatch_event(*args, **kwargs):
306
409
  con = get_server_api_connection()
307
410
  return con.dispatch_event(*args, **kwargs)
@@ -88,3 +88,18 @@ DEFAULT_WORKFILE_INFO_FIELDS = {
88
88
  "updatedAt",
89
89
  "updatedBy",
90
90
  }
91
+
92
+ DEFAULT_EVENT_FIELDS = {
93
+ "id",
94
+ "hash",
95
+ "createdAt",
96
+ "dependsOn",
97
+ "description",
98
+ "project",
99
+ "retries",
100
+ "sender",
101
+ "status",
102
+ "topic",
103
+ "updatedAt",
104
+ "user",
105
+ }
@@ -3,7 +3,7 @@ import collections
3
3
  from abc import ABCMeta, abstractmethod, abstractproperty
4
4
 
5
5
  import six
6
- from .server_api import get_server_api_connection
6
+ from ._api import get_server_api_connection
7
7
  from .utils import create_entity_id, convert_entity_id
8
8
 
9
9
  UNKNOWN_VALUE = object()
@@ -27,7 +27,7 @@ class EntityHub(object):
27
27
 
28
28
  Args:
29
29
  project_name (str): Name of project where changes will happen.
30
- connection (ServerAPIBase): Connection to server with logged user.
30
+ connection (ServerAPI): Connection to server with logged user.
31
31
  allow_data_changes (bool): This option gives ability to change 'data'
32
32
  key on entities. This is not recommended as 'data' may be use for
33
33
  secure information and would also slow down server queries. Content
@@ -0,0 +1,52 @@
1
+ import copy
2
+
3
+
4
+ class ServerEvent(object):
5
+ def __init__(
6
+ self,
7
+ topic,
8
+ sender=None,
9
+ event_hash=None,
10
+ project_name=None,
11
+ username=None,
12
+ dependencies=None,
13
+ description=None,
14
+ summary=None,
15
+ payload=None,
16
+ finished=True,
17
+ store=True,
18
+ ):
19
+ if dependencies is None:
20
+ dependencies = []
21
+ if payload is None:
22
+ payload = {}
23
+ if summary is None:
24
+ summary = {}
25
+
26
+ self.topic = topic
27
+ self.sender = sender
28
+ self.event_hash = event_hash
29
+ self.project_name = project_name
30
+ self.username = username
31
+ self.dependencies = dependencies
32
+ self.description = description
33
+ self.summary = summary
34
+ self.payload = payload
35
+ self.finished = finished
36
+ self.store = store
37
+
38
+ def to_data(self):
39
+ return {
40
+ "topic": self.topic,
41
+ "sender": self.sender,
42
+ "hash": self.event_hash,
43
+ "project": self.project_name,
44
+ "user": self.username,
45
+ "dependencies": copy.deepcopy(self.dependencies),
46
+ "description": self.description,
47
+ "description": self.description,
48
+ "summary": copy.deepcopy(self.summary),
49
+ "payload": self.payload,
50
+ "finished": self.finished,
51
+ "store": self.store
52
+ }
@@ -327,3 +327,36 @@ def workfiles_info_graphql_query(fields):
327
327
  for k, v in value.items():
328
328
  query_queue.append((k, v, field))
329
329
  return query
330
+
331
+
332
+ def events_graphql_query(fields):
333
+ query = GraphQlQuery("WorkfilesInfo")
334
+ topics_var = query.add_variable("eventTopics", "[String!]")
335
+ projects_var = query.add_variable("projectNames", "[String!]")
336
+ states_var = query.add_variable("eventStates", "[String!]")
337
+ users_var = query.add_variable("eventUsers", "[String!]")
338
+ include_logs_var = query.add_variable("includeLogsFilter", "Boolean!")
339
+
340
+ events_field = query.add_field("events", has_edges=True)
341
+ events_field.set_filter("topics", topics_var)
342
+ events_field.set_filter("projects", projects_var)
343
+ events_field.set_filter("states", states_var)
344
+ events_field.set_filter("users", users_var)
345
+ events_field.set_filter("includeLogs", include_logs_var)
346
+
347
+ nested_fields = fields_to_dict(set(fields))
348
+
349
+ query_queue = collections.deque()
350
+ for key, value in nested_fields.items():
351
+ query_queue.append((key, value, events_field))
352
+
353
+ while query_queue:
354
+ item = query_queue.popleft()
355
+ key, value, parent = item
356
+ field = parent.add_field(key)
357
+ if value is FIELD_VALUE:
358
+ continue
359
+
360
+ for k, v in value.items():
361
+ query_queue.append((k, v, field))
362
+ return query
@@ -6,7 +6,7 @@ from abc import ABCMeta, abstractproperty
6
6
 
7
7
  import six
8
8
 
9
- from .server_api import get_server_api_connection
9
+ from ._api import get_server_api_connection
10
10
  from .utils import create_entity_id, REMOVED_VALUE
11
11
 
12
12
 
@@ -6,7 +6,12 @@ import logging
6
6
  import collections
7
7
  import platform
8
8
  import copy
9
- from http import HTTPStatus
9
+ import uuid
10
+ from contextlib import contextmanager
11
+ try:
12
+ from http import HTTPStatus
13
+ except ImportError:
14
+ HTTPStatus = None
10
15
 
11
16
  import requests
12
17
 
@@ -19,6 +24,7 @@ from .constants import (
19
24
  DEFAULT_REPRESENTATION_FIELDS,
20
25
  REPRESENTATION_FILES_FIELDS,
21
26
  DEFAULT_WORKFILE_INFO_FIELDS,
27
+ DEFAULT_EVENT_FIELDS,
22
28
  )
23
29
  from .thumbnails import ThumbnailCache
24
30
  from .graphql import GraphQlQuery, INTROSPECTION_QUERY
@@ -32,6 +38,7 @@ from .graphql_queries import (
32
38
  representations_graphql_query,
33
39
  representations_parents_qraphql_query,
34
40
  workfiles_info_graphql_query,
41
+ events_graphql_query,
35
42
  )
36
43
  from .exceptions import (
37
44
  FailedOperations,
@@ -56,6 +63,12 @@ PROJECT_NAME_REGEX = re.compile(
56
63
  )
57
64
 
58
65
 
66
+ def _get_description(response):
67
+ if HTTPStatus is None:
68
+ return str(response.orig_response)
69
+ return HTTPStatus(response.status).description
70
+
71
+
59
72
  class RequestType:
60
73
  def __init__(self, name):
61
74
  self.name = name
@@ -95,7 +108,10 @@ class RestApiResponse(object):
95
108
  @property
96
109
  def data(self):
97
110
  if self._data is None:
98
- self._data = self._response.json()
111
+ if self.status != 204:
112
+ self._data = self.orig_response.json()
113
+ else:
114
+ self._data = {}
99
115
  return self._data
100
116
 
101
117
  @property
@@ -108,7 +124,7 @@ class RestApiResponse(object):
108
124
 
109
125
  @property
110
126
  def detail(self):
111
- return self.get("detail", HTTPStatus(self.status).description)
127
+ return self.get("detail", _get_description(self))
112
128
 
113
129
  @property
114
130
  def status_code(self):
@@ -172,7 +188,91 @@ def fill_own_attribs(entity):
172
188
  own_attrib[key] = copy.deepcopy(value)
173
189
 
174
190
 
175
- class ServerAPIBase(object):
191
+ class _AsUserStack:
192
+ """Handle stack of users used over server api connection in service mode.
193
+
194
+ ServerAPI can behave as other users if it is using special API key.
195
+
196
+ Examples:
197
+ >>> stack = _AsUserStack()
198
+ >>> stack.set_default_username("DefaultName")
199
+ >>> print(stack.username)
200
+ DefaultName
201
+ >>> with stack.as_user("Other1"):
202
+ ... print(stack.username)
203
+ ... with stack.as_user("Other2"):
204
+ ... print(stack.username)
205
+ ... print(stack.username)
206
+ ... stack.clear()
207
+ ... print(stack.username)
208
+ Other1
209
+ Other2
210
+ Other1
211
+ None
212
+ >>> print(stack.username)
213
+ None
214
+ >>> stack.set_default_username("DefaultName")
215
+ >>> print(stack.username)
216
+ DefaultName
217
+ """
218
+
219
+ def __init__(self):
220
+ self._users_by_id = {}
221
+ self._user_ids = []
222
+ self._last_user = None
223
+ self._default_user = None
224
+
225
+ def clear(self):
226
+ self._users_by_id = {}
227
+ self._user_ids = []
228
+ self._last_user = None
229
+ self._default_user = None
230
+
231
+ @property
232
+ def username(self):
233
+ # Use '_user_ids' for boolean check to have ability "unset"
234
+ # default user
235
+ if self._user_ids:
236
+ return self._last_user
237
+ return self._default_user
238
+
239
+ def get_default_username(self):
240
+ return self._default_user
241
+
242
+ def set_default_username(self, username=None):
243
+ self._default_user = username
244
+
245
+ default_username = property(get_default_username, set_default_username)
246
+
247
+ @contextmanager
248
+ def as_user(self, username):
249
+ self._last_user = username
250
+ user_id = uuid.uuid4().hex
251
+ self._user_ids.append(user_id)
252
+ self._users_by_id[user_id] = username
253
+ try:
254
+ yield
255
+ finally:
256
+ self._users_by_id.pop(user_id, None)
257
+ if not self._user_ids:
258
+ return
259
+
260
+ # First check if is the user id the last one
261
+ was_last = self._user_ids[-1] == user_id
262
+ # Remove id from variables
263
+ if user_id in self._user_ids:
264
+ self._user_ids.remove(user_id)
265
+
266
+ if not was_last:
267
+ return
268
+
269
+ new_last_user = None
270
+ if self._user_ids:
271
+ new_last_user = self._users_by_id.get(self._user_ids[-1])
272
+ self._last_user = new_last_user
273
+
274
+
275
+ class ServerAPI(object):
176
276
  """Base handler of connection to server.
177
277
 
178
278
  Requires url to server which is used as base for api and graphql calls.
@@ -225,8 +325,15 @@ class ServerAPIBase(object):
225
325
  self._attributes_schema = None
226
326
  self._entity_type_attributes_cache = {}
227
327
 
328
+ self._as_user_stack = _AsUserStack()
228
329
  self._thumbnail_cache = ThumbnailCache(True)
229
330
 
331
+ @property
332
+ def log(self):
333
+ if self._log is None:
334
+ self._log = logging.getLogger(self.__class__.__name__)
335
+ return self._log
336
+
230
337
  def get_base_url(self):
231
338
  return self._base_url
232
339
 
@@ -248,12 +355,111 @@ class ServerAPIBase(object):
248
355
  return
249
356
  self._site_id = site_id
250
357
  # Recreate session on machine id change
251
- if self._session is not None:
252
- self.close_session()
253
- self.create_session()
358
+ self._update_session_headers()
254
359
 
255
360
  site_id = property(get_site_id, set_site_id)
256
361
 
362
+ def get_client_version(self):
363
+ """Version of client used to connect to server.
364
+
365
+ Client version is AYON client build desktop application.
366
+
367
+ Returns:
368
+ str: Client version string used in connection.
369
+ """
370
+
371
+ return self._client_version
372
+
373
+ def set_client_version(self, client_version):
374
+ """Set version of client used to connect to server.
375
+
376
+ Client version is AYON client build desktop application.
377
+
378
+ Args:
379
+ client_version (Union[str, None]): Client version string.
380
+ """
381
+
382
+ if self._client_version == client_version:
383
+ return
384
+
385
+ self._client_version = client_version
386
+ self._update_session_headers()
387
+
388
+ client_version = property(get_client_version, set_client_version)
389
+
390
+ def get_default_service_username(self):
391
+ """Default username used for callbacks when used with service API key.
392
+
393
+ Returns:
394
+ Union[str, None]: Username if any was filled.
395
+ """
396
+
397
+ return self._as_user_stack.get_default_username()
398
+
399
+ def set_default_service_username(self, username=None):
400
+ """Service API will work as other user.
401
+
402
+ Service API keys can work as other user. It can be temporary using
403
+ context manager 'as_user' or it is possible to set default username if
404
+ 'as_user' context manager is not entered.
405
+
406
+ Args:
407
+ username (Union[str, None]): Username to work as when service.
408
+
409
+ Raises:
410
+ ValueError: When connection is not yet authenticated or api key
411
+ is not service token.
412
+ """
413
+
414
+ current_username = self._as_user_stack.get_default_username()
415
+ if current_username == username:
416
+ return
417
+
418
+ if not self.has_valid_token:
419
+ raise ValueError(
420
+ "Authentication of connection did not happen yet."
421
+ )
422
+
423
+ if not self._access_token_is_service:
424
+ raise ValueError(
425
+ "Can't set service username. API key is not a service token."
426
+ )
427
+
428
+ self._as_user_stack.set_default_username(username)
429
+ if self._as_user_stack.username == username:
430
+ self._update_session_headers()
431
+
432
+ @contextmanager
433
+ def as_username(self, username):
434
+ """Service API will temporarily work as other user.
435
+
436
+ This method can be used only if service API key is logged in.
437
+
438
+ Args:
439
+ username (Union[str, None]): Username to work as when service.
440
+
441
+ Raises:
442
+ ValueError: When connection is not yet authenticated or api key
443
+ is not service token.
444
+ """
445
+
446
+ if not self.has_valid_token:
447
+ raise ValueError(
448
+ "Authentication of connection did not happen yet."
449
+ )
450
+
451
+ if not self._access_token_is_service:
452
+ raise ValueError(
453
+ "Can't set service username. API key is not a service token."
454
+ )
455
+
456
+ with self._as_user_stack.as_user(username) as o:
457
+ self._update_session_headers()
458
+ try:
459
+ yield o
460
+ finally:
461
+ self._update_session_headers()
462
+
257
463
  @property
258
464
  def is_server_available(self):
259
465
  if self._server_available is None:
@@ -303,6 +509,7 @@ class ServerAPIBase(object):
303
509
  if self._session is not None:
304
510
  raise ValueError("Session is already created.")
305
511
 
512
+ self._as_user_stack.clear()
306
513
  # Validate token before session creation
307
514
  self.validate_token()
308
515
 
@@ -327,6 +534,21 @@ class ServerAPIBase(object):
327
534
  self._session_functions_mapping = {}
328
535
  session.close()
329
536
 
537
+ def _update_session_headers(self):
538
+ if self._session is None:
539
+ return
540
+
541
+ # Header keys that may change over time
542
+ for key, value in (
543
+ ("X-as-user", self._as_user_stack.username),
544
+ ("x-ayon-version", self._client_version),
545
+ ("x-ayon-site-id", self._site_id),
546
+ ):
547
+ if value is not None:
548
+ self._session.headers[key] = value
549
+ elif key in self._session.headers:
550
+ self._session.headers.pop(key)
551
+
330
552
  def get_info(self):
331
553
  """Get information about current used api key.
332
554
 
@@ -383,12 +605,6 @@ class ServerAPIBase(object):
383
605
  raise UnauthorizedError("User is not authorized.")
384
606
  return output
385
607
 
386
- @property
387
- def log(self):
388
- if self._log is None:
389
- self._log = logging.getLogger(self.__class__.__name__)
390
- return self._log
391
-
392
608
  def get_headers(self, content_type=None):
393
609
  if content_type is None:
394
610
  content_type = "application/json"
@@ -407,6 +623,9 @@ class ServerAPIBase(object):
407
623
  if self._access_token:
408
624
  if self._access_token_is_service:
409
625
  headers["X-Api-Key"] = self._access_token
626
+ username = self._as_user_stack.username
627
+ if username:
628
+ headers["X-as-user"] = username
410
629
  else:
411
630
  headers["Authorization"] = "Bearer {}".format(
412
631
  self._access_token)
@@ -586,6 +805,74 @@ class ServerAPIBase(object):
586
805
  response.raise_for_status()
587
806
  return response.data
588
807
 
808
+ def get_events(
809
+ self,
810
+ topics=None,
811
+ project_names=None,
812
+ states=None,
813
+ users=None,
814
+ include_logs=None,
815
+ fields=None
816
+ ):
817
+ """Get events from server with filtering options.
818
+
819
+ Notes:
820
+ Not all event happen on a project.
821
+
822
+ Args:
823
+ topics (Iterable[str]): Name of topics.
824
+ project_names (Iterable[str]): Project on which event happened.
825
+ states (Iterable[str]): Filtering by states.
826
+ users (Iterable[str]): Filtering by users who created/triggered
827
+ an event.
828
+ include_logs (bool): Query also log events.
829
+ fields (Iterable[str]): Fields that should be received for each
830
+ event.
831
+
832
+ Returns:
833
+ Generator[Dict[str, Any]]: Available events matching filters.
834
+ """
835
+
836
+ filters = {}
837
+ if topics is not None:
838
+ topics = set(topics)
839
+ if not topics:
840
+ return
841
+ filters["eventTopics"] = list(topics)
842
+
843
+ if project_names is not None:
844
+ project_names = set(project_names)
845
+ if not project_names:
846
+ return
847
+ filters["projectName"] = list(project_names)
848
+
849
+ if states is not None:
850
+ states = set(states)
851
+ if not states:
852
+ return
853
+ filters["eventStates"] = list(states)
854
+
855
+ if users is not None:
856
+ users = set(users)
857
+ if not users:
858
+ return
859
+ filters["eventUsers"] = list(users)
860
+
861
+ if include_logs is None:
862
+ include_logs = False
863
+ filters["includeLogsFilter"] = include_logs
864
+
865
+ if not fields:
866
+ fields = DEFAULT_EVENT_FIELDS
867
+
868
+ query = events_graphql_query(set(fields))
869
+ for attr, filter_value in filters.items():
870
+ query.set_variable_value(attr, filter_value)
871
+
872
+ for parsed_data in query.continuous_query(self):
873
+ for event in parsed_data["events"]:
874
+ yield event
875
+
589
876
  def update_event(
590
877
  self,
591
878
  event_id,
@@ -627,6 +914,25 @@ class ServerAPIBase(object):
627
914
  finished=True,
628
915
  store=True,
629
916
  ):
917
+ """Dispatch event to server.
918
+
919
+ Arg:
920
+ topic (str): Event topic used for filtering of listeners.
921
+ sender (Optional[str]): Sender of event.
922
+ hash (Optional[str]): Event hash.
923
+ project_name (Optional[str]): Project name.
924
+ username (Optional[str]): Username which triggered event.
925
+ dependencies (Optional[list[str]]): List of event id deprendencies.
926
+ description (Optional[str]): Description of event.
927
+ summary (Optional[dict[str, Any]]): Summary of event that can be used
928
+ for simple filtering on listeners.
929
+ payload (Optional[dict[str, Any]]): Full payload of event data with
930
+ all details.
931
+ finished (bool): Mark event as finished on dispatch.
932
+ store (bool): Store event in event queue for possible future processing
933
+ otherwise is event send only to active listeners.
934
+ """
935
+
630
936
  if summary is None:
631
937
  summary = {}
632
938
  if payload is None:
@@ -1,2 +1,2 @@
1
1
  """Package declaring Python API for Ayon server."""
2
- __version__ = "0.1.9"
2
+ __version__ = "0.1.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -9,20 +9,18 @@ License: Apache License (2.0)
9
9
  Description: # AYON server API
10
10
  Python client for connection server. Client must be (at least for some time) Python 2 compatible because will be used in DCC that are "older".
11
11
 
12
- Client should support connection to server with raw REST functions and prepared functionality for work with entities. Must not contain only functionality that can be used with core server functionality.
12
+ AYON Python api should support connection to server with raw REST functions and prepared functionality for work with entities. Must not contain only functionality that can be used with core server functionality.
13
13
 
14
+ Module support singleton connection which is using `AYON_SERVER_URL` and `AYON_TOKEN` environment variables as source for connection. The singleton connection is using `ServerAPI` object. There can be created multiple connection to different server at one time, for that purpose use `ServerAPIBase` object.
14
15
 
15
16
  ## TODOs
16
- - Current implementation contains prepared functions for entity queries which is not using full potential of OpenPype server but is MongoDB compatible.
17
- - only functions related to v4 should stay at that place and v3 compatible functions should be moved
18
- - Missing settings getter
19
- - Missing CRUD operations for entities (Only read is possible)
20
- - More clear what is difference in `ServerAPIBase` and `ServerAPI`
17
+ - More clear what is difference in `ServerAPIBase` and `ServerAPI` (`server.py` and `server_api.py`) and better names
21
18
  - `ServerAPI` was added primarily for desktop app which handle login and logout in a different way so the class should be maybe removed and `ServerAPIBase` should be renamed to `ServerAPI`
22
19
  - find more suitable names of classes
23
- - find more suitable name of objects (right now is used `connection` or `conn`)
20
+ - find more suitable name of objects (right now is used `connection` or `con`)
24
21
  - Add folder and task changes to operations
25
22
  - Entity hub should use operations to update folders and tasks
23
+
26
24
  Keywords: AYON,ynput,OpenPype,vfx
27
25
  Platform: UNKNOWN
28
26
  Classifier: Development Status :: 3 - Alpha
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  setup.py
5
5
  ayon_api/__init__.py
6
+ ayon_api/_api.py
6
7
  ayon_api/constants.py
7
8
  ayon_api/entity_hub.py
8
9
  ayon_api/events.py
@@ -10,7 +11,6 @@ ayon_api/exceptions.py
10
11
  ayon_api/graphql.py
11
12
  ayon_api/graphql_queries.py
12
13
  ayon_api/operations.py
13
- ayon_api/server.py
14
14
  ayon_api/server_api.py
15
15
  ayon_api/thumbnails.py
16
16
  ayon_api/utils.py
@@ -1,4 +1,4 @@
1
1
  requests>=2.27.1
2
2
  six>=1.15
3
- Unidecode>=1.3.6
3
+ Unidecode>=1.2.0
4
4
  appdirs<2,>=1
@@ -1,13 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "ayon_api"
3
- version = "0.1.9"
3
+ version = "0.1.11"
4
4
  description = "Python api for AYON server"
5
5
  authors = ["ynput.io <info@ynput.io>"]
6
6
 
7
7
  [tool.poetry.dependencies]
8
8
  requests = "^2.28.1"
9
9
  six = "^1.15"
10
- Unidecode = "^1.3"
10
+ Unidecode = "^1.2"
11
11
  appdirs = "^1.4"
12
12
 
13
13
  [tool.poetry.dev-dependencies]
@@ -30,7 +30,7 @@ setup(
30
30
  install_requires=[
31
31
  "requests >= 2.27.1",
32
32
  "six >= 1.15",
33
- "Unidecode >= 1.3.6",
33
+ "Unidecode >= 1.2.0",
34
34
  "appdirs >=1, <2",
35
35
  ],
36
36
  keywords=["AYON", "ynput", "OpenPype", "vfx"],
@@ -1,105 +0,0 @@
1
- import copy
2
-
3
- from .server_api import get_server_api_connection
4
-
5
-
6
- class ServerEvent(object):
7
- def __init__(
8
- self,
9
- topic,
10
- sender=None,
11
- hash=None,
12
- project_name=None,
13
- username=None,
14
- dependencies=None,
15
- description=None,
16
- summary=None,
17
- payload=None,
18
- finished=True,
19
- store=True,
20
- ):
21
- if dependencies is None:
22
- dependencies = []
23
- if payload is None:
24
- payload = {}
25
- if summary is None:
26
- summary = {}
27
-
28
- self.topic = topic
29
- self.sender = sender
30
- self.hash = hash
31
- self.project_name = project_name
32
- self.username = username
33
- self.dependencies = dependencies
34
- self.summary = summary
35
- self.payload = payload
36
- self.finished = finished
37
- self.store = store
38
-
39
- def to_data(self):
40
- return {
41
- "topic": self.topic,
42
- "sender": self.sender,
43
- "hash": self.hash,
44
- "project": self.project_name,
45
- "user": self.username,
46
- "dependencies": copy.deepcopy(self.dependencies),
47
- "description": self.description,
48
- "summary": copy.deepcopy(self.summary),
49
- "payload": self.payload,
50
- "finished": self.finished,
51
- "store": self.store
52
- }
53
-
54
-
55
- def dispatch_event(
56
- topic,
57
- sender=None,
58
- hash=None,
59
- project_name=None,
60
- username=None,
61
- dependencies=None,
62
- description=None,
63
- summary=None,
64
- payload=None,
65
- finished=True,
66
- store=True,
67
- ):
68
- """Dispatch event to server.
69
-
70
- Arg:
71
- topic (str): Event topic used for filtering of listeners.
72
- sender (Optional[str]): Sender of event.
73
- hash (Optional[str]): Event hash.
74
- project_name (Optional[str]): Project name.
75
- username (Optional[str]): Username which triggered event.
76
- dependencies (Optional[list[str]]): List of event id deprendencies.
77
- description (Optional[str]): Description of event.
78
- summary (Optional[dict[str, Any]]): Summary of event that can be used
79
- for simple filtering on listeners.
80
- payload (Optional[dict[str, Any]]): Full payload of event data with
81
- all details.
82
- finished (bool): Mark event as finished on dispatch.
83
- store (bool): Store event in event queue for possible future processing
84
- otherwise is event send only to active listeners.
85
- """
86
-
87
- event = ServerEvent(
88
- topic,
89
- sender,
90
- hash,
91
- project_name,
92
- username,
93
- dependencies,
94
- description,
95
- summary,
96
- payload,
97
- finished,
98
- store
99
- )
100
- con = get_server_api_connection()
101
- response = con.post("events", json=event.to_data())
102
- if response:
103
- print(response, response.data)
104
- return event
105
- return None