Ayon-Python-Api 0.1.7__tar.gz → 0.1.8__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 (24) hide show
  1. {ayon-python-api-0.1.7/ayon_python_api.egg-info → ayon-python-api-0.1.8}/PKG-INFO +1 -1
  2. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/__init__.py +17 -0
  3. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/exceptions.py +4 -0
  4. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/server.py +154 -27
  5. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/server_api.py +112 -0
  6. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/utils.py +76 -17
  7. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/version.py +1 -1
  8. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8/ayon_python_api.egg-info}/PKG-INFO +1 -1
  9. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/pyproject.toml +2 -1
  10. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/LICENSE +0 -0
  11. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/README.md +0 -0
  12. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/constants.py +0 -0
  13. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/entity_hub.py +0 -0
  14. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/events.py +0 -0
  15. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/graphql.py +0 -0
  16. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/graphql_queries.py +0 -0
  17. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/operations.py +0 -0
  18. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_api/thumbnails.py +0 -0
  19. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/SOURCES.txt +0 -0
  20. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  21. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/requires.txt +0 -0
  22. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/top_level.txt +0 -0
  23. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/setup.cfg +0 -0
  24. {ayon-python-api-0.1.7 → ayon-python-api-0.1.8}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -1,5 +1,6 @@
1
1
  from .utils import (
2
2
  TransferProgress,
3
+ slugify_string,
3
4
  )
4
5
  from .server import (
5
6
  ServerAPIBase,
@@ -7,6 +8,9 @@ from .server import (
7
8
 
8
9
  from .server_api import (
9
10
  ServerAPI,
11
+ ServiceContext,
12
+
13
+ init_service,
10
14
 
11
15
  get_server_api_connection,
12
16
 
@@ -25,6 +29,11 @@ from .server_api import (
25
29
  patch,
26
30
  delete,
27
31
 
32
+ get_event,
33
+ dispatch_event,
34
+ update_event,
35
+ enroll_event_job,
36
+
28
37
  download_file,
29
38
  upload_file,
30
39
 
@@ -99,10 +108,13 @@ from .server_api import (
99
108
 
100
109
  __all__ = (
101
110
  "TransferProgress",
111
+ "slugify_string",
102
112
 
103
113
  "ServerAPIBase",
104
114
 
105
115
  "ServerAPI",
116
+ "ServiceContext",
117
+ "init_service",
106
118
 
107
119
  "get_server_api_connection",
108
120
 
@@ -121,6 +133,11 @@ __all__ = (
121
133
  "patch",
122
134
  "delete",
123
135
 
136
+ "get_event",
137
+ "dispatch_event",
138
+ "update_event",
139
+ "enroll_event_job",
140
+
124
141
  "download_file",
125
142
  "upload_file",
126
143
 
@@ -90,4 +90,8 @@ class FolderNotFound(MissingEntityError):
90
90
 
91
91
 
92
92
  class FailedOperations(Exception):
93
+ pass
94
+
95
+
96
+ class FailedServiceInit(Exception):
93
97
  pass
@@ -75,13 +75,32 @@ class RequestTypes:
75
75
  class RestApiResponse(object):
76
76
  """API Response."""
77
77
 
78
- def __init__(self, status_code, data=None, content=None, headers=None):
79
- if data is None:
80
- data = {}
78
+ def __init__(self, response, data=None):
79
+ if response is None:
80
+ status_code = 500
81
+ else:
82
+ status_code = response.status_code
83
+ self._response = response
81
84
  self.status = status_code
82
- self.data = data
83
- self.content = content
84
- self.headers = headers or {}
85
+ self._data = data
86
+
87
+ @property
88
+ def orig_response(self):
89
+ return self._response
90
+
91
+ @property
92
+ def headers(self):
93
+ return self._response.headers
94
+
95
+ @property
96
+ def data(self):
97
+ if self._data is None:
98
+ self._data = self._response.json()
99
+ return self._data
100
+
101
+ @property
102
+ def content(self):
103
+ return self._response.content
85
104
 
86
105
  @property
87
106
  def content_type(self):
@@ -95,6 +114,12 @@ class RestApiResponse(object):
95
114
  def status_code(self):
96
115
  return self.status
97
116
 
117
+ def raise_for_status(self):
118
+ self._response.raise_for_status()
119
+
120
+ def __enter__(self, *args, **kwargs):
121
+ return self._response.__enter__(*args, **kwargs)
122
+
98
123
  def __contains__(self, key):
99
124
  return key in self.data
100
125
 
@@ -448,26 +473,22 @@ class ServerAPIBase(object):
448
473
 
449
474
  except ConnectionRefusedError:
450
475
  new_response = RestApiResponse(
451
- 500,
476
+ None,
452
477
  {"detail": "Unable to connect the server. Connection refused"}
453
478
  )
454
479
  except requests.exceptions.ConnectionError:
455
480
  new_response = RestApiResponse(
456
- 500,
481
+ None,
457
482
  {"detail": "Unable to connect the server. Connection error"}
458
483
  )
459
484
  else:
460
485
  content_type = response.headers.get("Content-Type")
461
486
  if content_type == "application/json":
462
487
  try:
463
- new_response = RestApiResponse(
464
- response.status_code,
465
- response.json(),
466
- headers=response.headers,
467
- )
488
+ new_response = RestApiResponse(response)
468
489
  except JSONDecodeError:
469
490
  new_response = RestApiResponse(
470
- 500,
491
+ None,
471
492
  {
472
493
  "detail": "The response is not a JSON: {}".format(
473
494
  response.text)
@@ -475,17 +496,10 @@ class ServerAPIBase(object):
475
496
  )
476
497
 
477
498
  elif content_type in ("image/jpeg", "image/png"):
478
- new_response = RestApiResponse(
479
- response.status_code,
480
- headers=response.headers,
481
- content=response.content
482
- )
499
+ new_response = RestApiResponse(response)
483
500
 
484
501
  else:
485
- new_response = RestApiResponse(
486
- response.status_code,
487
- headers=response.headers,
488
- )
502
+ new_response = RestApiResponse(response)
489
503
 
490
504
  self.log.debug("Response {}".format(str(new_response)))
491
505
  return new_response
@@ -555,6 +569,119 @@ class ServerAPIBase(object):
555
569
  def delete(self, entrypoint, **kwargs):
556
570
  return self.raw_delete(entrypoint, params=kwargs)
557
571
 
572
+ def get_event(self, event_id):
573
+ """Receive full event data by id.
574
+
575
+ Events received using event server do not contain full information. To
576
+ get the full event information is required to receive it explicitly.
577
+
578
+ Args:
579
+ event_id (str): Id of event.
580
+
581
+ Returns:
582
+ Dict[str, Any]: Full event data.
583
+ """
584
+
585
+ response = self.get("events/{}".format(event_id))
586
+ response.raise_for_status()
587
+ return response.data
588
+
589
+ def update_event(
590
+ self,
591
+ event_id,
592
+ sender=None,
593
+ project_name=None,
594
+ status=None,
595
+ description=None,
596
+ summary=None,
597
+ payload=None
598
+ ):
599
+ kwargs = {}
600
+ for key, value in (
601
+ ("sender", sender),
602
+ ("projectName", project_name),
603
+ ("status", status),
604
+ ("description", description),
605
+ ("summary", summary),
606
+ ("payload", payload),
607
+ ):
608
+ if value is not None:
609
+ kwargs[key] = value
610
+ response = self.put(
611
+ "events/{}".format(event_id),
612
+ **kwargs
613
+ )
614
+ response.raise_for_status()
615
+
616
+ def dispatch_event(
617
+ self,
618
+ topic,
619
+ sender=None,
620
+ event_hash=None,
621
+ project_name=None,
622
+ username=None,
623
+ dependencies=None,
624
+ description=None,
625
+ summary=None,
626
+ payload=None,
627
+ finished=True,
628
+ store=True,
629
+ ):
630
+ if summary is None:
631
+ summary = {}
632
+ if payload is None:
633
+ payload = {}
634
+ event_data = {
635
+ "topic": topic,
636
+ "sender": sender,
637
+ "hash": event_hash,
638
+ "project": project_name,
639
+ "user": username,
640
+ "dependencies": dependencies,
641
+ "description": description,
642
+ "summary": summary,
643
+ "payload": payload,
644
+ "finished": finished,
645
+ "store": store,
646
+ }
647
+ if self.post("events", **event_data):
648
+ self.log.debug("Dispatched event {}".format(topic))
649
+ return True
650
+ self.log.error("Unable to dispatch event {}".format(topic))
651
+ return False
652
+
653
+ def enroll_event_job(
654
+ self,
655
+ source_topic,
656
+ target_topic,
657
+ sender,
658
+ description=None,
659
+ sequential=None
660
+ ):
661
+ """Enroll job based on events.
662
+
663
+ Args:
664
+ source_topic (str): Topic
665
+ """
666
+
667
+ kwargs = {
668
+ "sourceTopic": source_topic,
669
+ "targetTopic": target_topic,
670
+ "sender": sender,
671
+ }
672
+ if sequential is not None:
673
+ kwargs["sequential"] = sequential
674
+ if description is not None:
675
+ kwargs["description"] = description
676
+ response = self.post("enroll", **kwargs)
677
+ if response.status_code == 204:
678
+ return None
679
+ elif response.status_code >= 400:
680
+ self.log.error(response.text)
681
+ return None
682
+
683
+ return response.data
684
+
558
685
  def _download_file(self, url, filepath, chunk_size, progress):
559
686
  dst_directory = os.path.dirname(filepath)
560
687
  if not os.path.exists(dst_directory):
@@ -573,7 +700,7 @@ class ServerAPIBase(object):
573
700
  progress.set_content_size(response.headers["Content-length"])
574
701
  for chunk in response.iter_content(chunk_size=chunk_size):
575
702
  f_stream.write(chunk)
576
- progress.add_downloaded_chunk(len(chunk))
703
+ progress.add_transferred_chunk(len(chunk))
577
704
 
578
705
  def download_file(self, endpoint, filepath, chunk_size=None, progress=None):
579
706
  """Download file from AYON server.
@@ -618,7 +745,7 @@ class ServerAPIBase(object):
618
745
  raise
619
746
 
620
747
  finally:
621
- progress.set_done()
748
+ progress.set_transfer_done()
622
749
 
623
750
  def _upload_file(self, url, filepath, progress):
624
751
  kwargs = {}
@@ -635,7 +762,7 @@ class ServerAPIBase(object):
635
762
  progress.set_content_size(size)
636
763
  response = post_func(url, data=stream, **kwargs)
637
764
  response.raise_for_status()
638
- progress.set_downloaded_size(size)
765
+ progress.set_transferred_size(size)
639
766
 
640
767
  def upload_file(self, endpoint, filepath, progress=None):
641
768
  """Upload file to server.
@@ -673,7 +800,7 @@ class ServerAPIBase(object):
673
800
  raise
674
801
 
675
802
  finally:
676
- progress.set_done()
803
+ progress.set_transfer_done()
677
804
 
678
805
  def trigger_server_restart(self):
679
806
  result = self.post("system/restart")
@@ -1,9 +1,12 @@
1
1
  import os
2
+ import socket
3
+
2
4
  from .constants import (
3
5
  SERVER_URL_ENV_KEY,
4
6
  SERVER_TOKEN_ENV_KEY,
5
7
  )
6
8
  from .server import ServerAPIBase
9
+ from .exceptions import FailedServiceInit
7
10
 
8
11
 
9
12
  class ServerAPI(ServerAPIBase):
@@ -72,6 +75,95 @@ class GlobalContext:
72
75
  return cls._connection
73
76
 
74
77
 
78
+ class ServiceContext:
79
+ """Helper for services running under server.
80
+
81
+ When service is running from server the process receives information about
82
+ connection from environment variables. This class helps to initialize the
83
+ values without knowing environment variables (that may change over time).
84
+
85
+ All what must be done is to call 'init_service' function/method. The
86
+ arguments are for cases when the service is running in specific environment
87
+ and their values are e.g. loaded from private file or for testing purposes.
88
+ """
89
+
90
+ token = None
91
+ server_url = None
92
+ addon_name = None
93
+ addon_version = None
94
+ service_name = None
95
+
96
+ @staticmethod
97
+ def get_value_from_envs(env_keys, value=None):
98
+ if value:
99
+ return value
100
+
101
+ for env_key in env_keys:
102
+ value = os.environ.get(env_key)
103
+ if value:
104
+ break
105
+ return value
106
+
107
+ @classmethod
108
+ def init_service(
109
+ cls,
110
+ token=None,
111
+ server_url=None,
112
+ addon_name=None,
113
+ addon_version=None,
114
+ service_name=None,
115
+ connect=True
116
+ ):
117
+ token = cls.get_value_from_envs(
118
+ ("AY_API_KEY", "AYON_TOKEN"),
119
+ token
120
+ )
121
+ server_url = cls.get_value_from_envs(
122
+ ("AY_SERVER_URL", "AYON_SERVER_URL"),
123
+ server_url
124
+ )
125
+ if not server_url:
126
+ raise FailedServiceInit("URL to server is not set")
127
+
128
+ if not token:
129
+ raise FailedServiceInit(
130
+ "Token to server {} is not set".format(server_url)
131
+ )
132
+
133
+ addon_name = cls.get_value_from_envs(
134
+ ("AY_ADDON_NAME", "AYON_ADDON_NAME"),
135
+ addon_name
136
+ )
137
+ addon_version = cls.get_value_from_envs(
138
+ ("AY_ADDON_VERSION", "AYON_ADDON_VERSION"),
139
+ addon_version
140
+ )
141
+ service_name = cls.get_value_from_envs(
142
+ ("AY_SERVICE_NAME", "AYON_SERVICE_NAME"),
143
+ service_name
144
+ )
145
+
146
+ cls.token = token
147
+ cls.server_url = server_url
148
+ cls.addon_name = addon_name
149
+ cls.addon_version = addon_version
150
+ cls.service_name = service_name or socket.gethostname()
151
+
152
+ # Make sure required environments for ServerAPI are set
153
+ os.environ["AYON_SERVER_URL"] = cls.server_url
154
+ os.environ["AYON_TOKEN"] = cls.token
155
+
156
+ if connect:
157
+ print("Connecting to server \"{}\"".format(server_url))
158
+ con = GlobalContext.get_server_api_connection()
159
+ user = con.get_user()
160
+ print("Logged in as user \"{}\"".format(user["name"]))
161
+
162
+
163
+ def init_service(*args, **kwargs):
164
+ ServiceContext.init_service(*args, **kwargs)
165
+
166
+
75
167
  def get_server_api_connection():
76
168
  """Access to global scope object of ServerAPI.
77
169
 
@@ -145,6 +237,26 @@ def delete(*args, **kwargs):
145
237
  return con.delete(*args, **kwargs)
146
238
 
147
239
 
240
+ def get_event(*args, **kwargs):
241
+ con = get_server_api_connection()
242
+ return con.get_event(*args, **kwargs)
243
+
244
+
245
+ def dispatch_event(*args, **kwargs):
246
+ con = get_server_api_connection()
247
+ return con.get_event(*args, **kwargs)
248
+
249
+
250
+ def update_event(*args, **kwargs):
251
+ con = get_server_api_connection()
252
+ return con.update_event(*args, **kwargs)
253
+
254
+
255
+ def enroll_event_job(*args, **kwargs):
256
+ con = get_server_api_connection()
257
+ return con.enroll_event_job(*args, **kwargs)
258
+
259
+
148
260
  def download_file(*args, **kwargs):
149
261
  con = get_server_api_connection()
150
262
  return con.download_file(*args, **kwargs)
@@ -1,15 +1,21 @@
1
+ import re
1
2
  import datetime
2
3
  import uuid
4
+ import string
3
5
  try:
4
6
  from urlparse import urlparse
5
7
  except ImportError:
6
8
  from urllib.parse import urlparse
7
9
 
8
10
  import requests
11
+ import unidecode
12
+
9
13
 
10
14
  from .exceptions import UrlError
11
15
 
12
16
  REMOVED_VALUE = object()
17
+ SLUGIFY_WHITELIST = string.ascii_letters + string.digits
18
+ SLUGIFY_SEP_WHITELIST = " ,./\\;:!|*^#@~+-_="
13
19
 
14
20
 
15
21
  def create_entity_id():
@@ -47,6 +53,59 @@ def entity_data_json_default(value):
47
53
  )
48
54
 
49
55
 
56
+ def slugify_string(
57
+ input_string,
58
+ separator="_",
59
+ slug_whitelist=SLUGIFY_WHITELIST,
60
+ split_chars=SLUGIFY_SEP_WHITELIST,
61
+ min_length=1,
62
+ lower=False,
63
+ make_set=False,
64
+ ):
65
+ """Slugify a text string.
66
+
67
+ This function removes transliterates input string to ASCII, removes
68
+ special characters and use join resulting elements using
69
+ specified separator.
70
+
71
+ Args:
72
+ input_string (str): Input string to slugify
73
+ separator (str): A string used to separate returned elements
74
+ (default: "_")
75
+ slug_whitelist (str): Characters allowed in the output
76
+ (default: ascii letters, digits and the separator)
77
+ split_chars (str): Set of characters used for word splitting
78
+ (there is a sane default)
79
+ lower (bool): Convert to lower-case (default: False)
80
+ make_set (bool): Return "set" object instead of string.
81
+ min_length (int): Minimal length of an element (word).
82
+
83
+ Returns:
84
+ Union[str, Set[str]]: Based on 'make_set' value returns slugified
85
+ string.
86
+ """
87
+
88
+ tmp_string = unidecode.unidecode(input_string)
89
+ if lower:
90
+ tmp_string = tmp_string.lower()
91
+
92
+ parts = [
93
+ # Remove all characters that are not in whitelist
94
+ re.sub("[^{}]".format(re.escape(slug_whitelist)), "", part)
95
+ # Split text into part by split characters
96
+ for part in re.split("[{}]".format(re.escape(split_chars)), tmp_string)
97
+ ]
98
+ # Filter text parts by length
99
+ filtered_parts = [
100
+ part
101
+ for part in parts
102
+ if len(part) >= min_length
103
+ ]
104
+ if make_set:
105
+ return set(filtered_parts)
106
+ return separator.join(filtered_parts)
107
+
108
+
50
109
  def failed_json_default(value):
51
110
  return "< Failed value {} > {}".format(type(value), str(value))
52
111
 
@@ -272,8 +331,8 @@ class TransferProgress:
272
331
 
273
332
  def __init__(self):
274
333
  self._started = False
275
- self._done = False
276
- self._downloaded = 0
334
+ self._transfer_done = False
335
+ self._transfered = 0
277
336
  self._content_size = None
278
337
 
279
338
  self._failed = False
@@ -298,15 +357,15 @@ class TransferProgress:
298
357
  raise ValueError("Progress already started")
299
358
  self._started = True
300
359
 
301
- def get_done(self):
302
- return self._done
360
+ def get_transfer_done(self):
361
+ return self._transfer_done
303
362
 
304
- def set_done(self):
305
- if self._done:
363
+ def set_transfer_done(self):
364
+ if self._transfer_done:
306
365
  raise ValueError("Progress was already marked as done")
307
366
  if not self._started:
308
367
  raise ValueError("Progress didn't start yet")
309
- self._done = True
368
+ self._transfer_done = True
310
369
 
311
370
  def get_failed(self):
312
371
  return self._failed
@@ -318,14 +377,14 @@ class TransferProgress:
318
377
  self._fail_reason = reason
319
378
  self._failed = True
320
379
 
321
- def get_downloaded_size(self):
322
- return self._downloaded
380
+ def get_transferred_size(self):
381
+ return self._transfered
323
382
 
324
- def set_downloaded_size(self, downloaded_size):
325
- self._downloaded = downloaded_size
383
+ def set_transferred_size(self, transfered):
384
+ self._transfered = transfered
326
385
 
327
- def add_downloaded_chunk(self, chunk_size):
328
- self._downloaded += chunk_size
386
+ def add_transferred_chunk(self, chunk_size):
387
+ self._transfered += chunk_size
329
388
 
330
389
  def get_source_url(self):
331
390
  return self._source_url
@@ -350,17 +409,17 @@ class TransferProgress:
350
409
  return True
351
410
 
352
411
  @property
353
- def progress(self):
412
+ def transfer_progress(self):
354
413
  if self._content_size is None:
355
414
  return None
356
- return (self._downloaded * 100.0) / float(self._content_size)
415
+ return (self._transfered * 100.0) / float(self._content_size)
357
416
 
358
417
  content_size = property(get_content_size, set_content_size)
359
418
  started = property(get_started)
360
- done = property(get_done)
419
+ transfer_done = property(get_transfer_done)
361
420
  failed = property(get_failed)
362
421
  fail_reason = property(get_fail_reason)
363
422
  source_url = property(get_source_url, set_source_url)
364
423
  destination_url = property(get_destination_url, set_destination_url)
365
424
  content_size = property(get_content_size, set_content_size)
366
- downloaded_size = property(get_downloaded_size, set_downloaded_size)
425
+ transferred_size = property(get_transferred_size, set_transferred_size)
@@ -1,2 +1,2 @@
1
1
  """Package declaring Python API for Ayon server."""
2
- __version__ = "0.1.7"
2
+ __version__ = "0.1.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -1,12 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "ayon_api"
3
- version = "0.1.7"
3
+ version = "0.1.8"
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
11
  appdirs = { git = "https://github.com/ActiveState/appdirs.git", branch = "master" }
11
12
 
12
13
  [tool.poetry.dev-dependencies]
File without changes