Ayon-Python-Api 0.1.6__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.6/ayon_python_api.egg-info → ayon-python-api-0.1.8}/PKG-INFO +1 -1
  2. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/__init__.py +30 -0
  3. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/exceptions.py +4 -0
  4. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/server.py +351 -37
  5. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/server_api.py +127 -0
  6. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/utils.py +158 -0
  7. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/version.py +1 -1
  8. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8/ayon_python_api.egg-info}/PKG-INFO +1 -1
  9. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/pyproject.toml +2 -1
  10. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/LICENSE +0 -0
  11. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/README.md +0 -0
  12. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/constants.py +0 -0
  13. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/entity_hub.py +0 -0
  14. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/events.py +0 -0
  15. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/graphql.py +0 -0
  16. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/graphql_queries.py +0 -0
  17. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/operations.py +0 -0
  18. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/thumbnails.py +0 -0
  19. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/SOURCES.txt +0 -0
  20. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  21. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/requires.txt +0 -0
  22. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/top_level.txt +0 -0
  23. {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/setup.cfg +0 -0
  24. {ayon-python-api-0.1.6 → 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.6
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,22 @@
1
+ from .utils import (
2
+ TransferProgress,
3
+ slugify_string,
4
+ )
1
5
  from .server import (
2
6
  ServerAPIBase,
3
7
  )
4
8
 
5
9
  from .server_api import (
6
10
  ServerAPI,
11
+ ServiceContext,
12
+
13
+ init_service,
7
14
 
8
15
  get_server_api_connection,
9
16
 
17
+ get_base_url,
18
+ get_rest_url,
19
+
10
20
  raw_get,
11
21
  raw_post,
12
22
  raw_put,
@@ -19,12 +29,18 @@ from .server_api import (
19
29
  patch,
20
30
  delete,
21
31
 
32
+ get_event,
33
+ dispatch_event,
34
+ update_event,
35
+ enroll_event_job,
36
+
22
37
  download_file,
23
38
  upload_file,
24
39
 
25
40
  query_graphql,
26
41
 
27
42
  get_addons_info,
43
+ download_addon_private_file,
28
44
 
29
45
  get_dependencies_info,
30
46
  update_dependency_info,
@@ -91,12 +107,20 @@ from .server_api import (
91
107
 
92
108
 
93
109
  __all__ = (
110
+ "TransferProgress",
111
+ "slugify_string",
112
+
94
113
  "ServerAPIBase",
95
114
 
96
115
  "ServerAPI",
116
+ "ServiceContext",
117
+ "init_service",
97
118
 
98
119
  "get_server_api_connection",
99
120
 
121
+ "get_base_url",
122
+ "get_rest_url",
123
+
100
124
  "raw_get",
101
125
  "raw_post",
102
126
  "raw_put",
@@ -109,12 +133,18 @@ __all__ = (
109
133
  "patch",
110
134
  "delete",
111
135
 
136
+ "get_event",
137
+ "dispatch_event",
138
+ "update_event",
139
+ "enroll_event_job",
140
+
112
141
  "download_file",
113
142
  "upload_file",
114
143
 
115
144
  "query_graphql",
116
145
 
117
146
  "get_addons_info",
147
+ "download_addon_private_file",
118
148
 
119
149
  "get_dependencies_info",
120
150
  "update_dependency_info",
@@ -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
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import re
3
+ import io
3
4
  import json
4
5
  import logging
5
6
  import collections
@@ -43,7 +44,8 @@ from .utils import (
43
44
  logout_from_server,
44
45
  create_entity_id,
45
46
  entity_data_json_default,
46
- failed_json_default
47
+ failed_json_default,
48
+ TransferProgress,
47
49
  )
48
50
 
49
51
  JSONDecodeError = getattr(json, "JSONDecodeError", ValueError)
@@ -73,13 +75,32 @@ class RequestTypes:
73
75
  class RestApiResponse(object):
74
76
  """API Response."""
75
77
 
76
- def __init__(self, status_code, data=None, content=None, headers=None):
77
- if data is None:
78
- 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
79
84
  self.status = status_code
80
- self.data = data
81
- self.content = content
82
- 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
83
104
 
84
105
  @property
85
106
  def content_type(self):
@@ -93,6 +114,12 @@ class RestApiResponse(object):
93
114
  def status_code(self):
94
115
  return self.status
95
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
+
96
123
  def __contains__(self, key):
97
124
  return key in self.data
98
125
 
@@ -200,6 +227,15 @@ class ServerAPIBase(object):
200
227
 
201
228
  self._thumbnail_cache = ThumbnailCache(True)
202
229
 
230
+ def get_base_url(self):
231
+ return self._base_url
232
+
233
+ def get_rest_url(self):
234
+ return self._rest_url
235
+
236
+ base_url = property(get_base_url)
237
+ rest_url = property(get_rest_url)
238
+
203
239
  @property
204
240
  def access_token(self):
205
241
  return self._access_token
@@ -437,26 +473,22 @@ class ServerAPIBase(object):
437
473
 
438
474
  except ConnectionRefusedError:
439
475
  new_response = RestApiResponse(
440
- 500,
476
+ None,
441
477
  {"detail": "Unable to connect the server. Connection refused"}
442
478
  )
443
479
  except requests.exceptions.ConnectionError:
444
480
  new_response = RestApiResponse(
445
- 500,
481
+ None,
446
482
  {"detail": "Unable to connect the server. Connection error"}
447
483
  )
448
484
  else:
449
485
  content_type = response.headers.get("Content-Type")
450
486
  if content_type == "application/json":
451
487
  try:
452
- new_response = RestApiResponse(
453
- response.status_code,
454
- response.json(),
455
- headers=response.headers,
456
- )
488
+ new_response = RestApiResponse(response)
457
489
  except JSONDecodeError:
458
490
  new_response = RestApiResponse(
459
- 500,
491
+ None,
460
492
  {
461
493
  "detail": "The response is not a JSON: {}".format(
462
494
  response.text)
@@ -464,17 +496,10 @@ class ServerAPIBase(object):
464
496
  )
465
497
 
466
498
  elif content_type in ("image/jpeg", "image/png"):
467
- new_response = RestApiResponse(
468
- response.status_code,
469
- headers=response.headers,
470
- content=response.content
471
- )
499
+ new_response = RestApiResponse(response)
472
500
 
473
501
  else:
474
- new_response = RestApiResponse(
475
- response.status_code,
476
- headers=response.headers,
477
- )
502
+ new_response = RestApiResponse(response)
478
503
 
479
504
  self.log.debug("Response {}".format(str(new_response)))
480
505
  return new_response
@@ -544,22 +569,238 @@ class ServerAPIBase(object):
544
569
  def delete(self, entrypoint, **kwargs):
545
570
  return self.raw_delete(entrypoint, params=kwargs)
546
571
 
547
- def download_file(self, endpoint, filepath, chunk_size=None):
548
- if not chunk_size:
549
- # 1 MB chunk by default
550
- chunk_size = 1024 * 1024
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
+
685
+ def _download_file(self, url, filepath, chunk_size, progress):
551
686
  dst_directory = os.path.dirname(filepath)
552
687
  if not os.path.exists(dst_directory):
553
688
  os.makedirs(dst_directory)
554
689
 
690
+ kwargs = {"stream": True}
691
+ if self._session is None:
692
+ kwargs["headers"] = self.get_headers()
693
+ get_func = self._base_functions_mapping[RequestTypes.get]
694
+ else:
695
+ get_func = self._session_functions_mapping[RequestTypes.get]
696
+
555
697
  with open(filepath, "wb") as f_stream:
556
- with self.raw_get(endpoint, stream=True) as response:
698
+ with get_func(url, **kwargs) as response:
699
+ response.raise_for_status()
700
+ progress.set_content_size(response.headers["Content-length"])
557
701
  for chunk in response.iter_content(chunk_size=chunk_size):
558
702
  f_stream.write(chunk)
703
+ progress.add_transferred_chunk(len(chunk))
704
+
705
+ def download_file(self, endpoint, filepath, chunk_size=None, progress=None):
706
+ """Download file from AYON server.
707
+
708
+ Endpoint can be full url (must start with 'base_url' of api object).
709
+
710
+ Progress object can be used to track download. Can be used when
711
+ download happens in thread and other thread want to catch changes over
712
+ time.
713
+
714
+ Args:
715
+ endpoint (str): Endpoint or URL to file that should be downloaded.
716
+ filepath (str): Path where file will be downloaded.
717
+ chunk_size (int): Size of chunks that are received in single loop.
718
+ progress (TransferProgress): Object that gives ability to track
719
+ download progress.
720
+ """
721
+
722
+ if not chunk_size:
723
+ # 1 MB chunk by default
724
+ chunk_size = 1024 * 1024
725
+
726
+ if endpoint.startswith(self._base_url):
727
+ url = endpoint
728
+ else:
729
+ endpoint = endpoint.lstrip("/").rstrip("/")
730
+ url = "{}/{}".format(self._rest_url, endpoint)
731
+
732
+ # Create dummy object so the function does not have to check
733
+ # 'progress' variable everywhere
734
+ if progress is None:
735
+ progress = TransferProgress()
736
+
737
+ progress.set_source_url(url)
738
+ progress.set_destination_url(filepath)
739
+ progress.set_started()
740
+ try:
741
+ self._download_file(url, filepath, chunk_size, progress)
742
+
743
+ except Exception as exc:
744
+ progress.set_failed(str(exc))
745
+ raise
746
+
747
+ finally:
748
+ progress.set_transfer_done()
749
+
750
+ def _upload_file(self, url, filepath, progress):
751
+ kwargs = {}
752
+ if self._session is None:
753
+ kwargs["headers"] = self.get_headers()
754
+ post_func = self._base_functions_mapping[RequestTypes.post]
755
+ else:
756
+ post_func = self._session_functions_mapping[RequestTypes.post]
559
757
 
560
- def upload_file(self, endpoint, filepath):
561
758
  with open(filepath, "rb") as stream:
562
- self.raw_post(endpoint, data=stream)
759
+ stream.seek(0, io.SEEK_END)
760
+ size = stream.tell()
761
+ stream.seek(0)
762
+ progress.set_content_size(size)
763
+ response = post_func(url, data=stream, **kwargs)
764
+ response.raise_for_status()
765
+ progress.set_transferred_size(size)
766
+
767
+ def upload_file(self, endpoint, filepath, progress=None):
768
+ """Upload file to server.
769
+
770
+ Todos:
771
+ Uploading with more detailed progress.
772
+
773
+ Args:
774
+ endpoint (str): Endpoint or url where file will be uploaded.
775
+ filepath (str): Source filepath.
776
+ progress (TransferProgress): Object that gives ability to track
777
+ upload progress.
778
+ """
779
+
780
+ if endpoint.startswith(self._base_url):
781
+ url = endpoint
782
+ else:
783
+ endpoint = endpoint.lstrip("/").rstrip("/")
784
+ url = "{}/{}".format(self._rest_url, endpoint)
785
+
786
+ # Create dummy object so the function does not have to check
787
+ # 'progress' variable everywhere
788
+ if progress is None:
789
+ progress = TransferProgress()
790
+
791
+ progress.set_source_url(filepath)
792
+ progress.set_destination_url(url)
793
+ progress.set_started()
794
+
795
+ try:
796
+ self._upload_file(url, filepath, progress)
797
+
798
+ except Exception as exc:
799
+ progress.set_failed(str(exc))
800
+ raise
801
+
802
+ finally:
803
+ progress.set_transfer_done()
563
804
 
564
805
  def trigger_server_restart(self):
565
806
  result = self.post("system/restart")
@@ -779,6 +1020,55 @@ class ServerAPIBase(object):
779
1020
  raise ServerError(response.text)
780
1021
  return response.data
781
1022
 
1023
+ def download_addon_private_file(
1024
+ self,
1025
+ addon_name,
1026
+ addon_version,
1027
+ filename,
1028
+ destination_dir,
1029
+ destination_filename=None,
1030
+ chunk_size=None,
1031
+ progress=None,
1032
+ ):
1033
+ """Download a file from addon private files.
1034
+
1035
+ This method requires to have authorized token available. Private files
1036
+ are not under '/api' restpoint.
1037
+
1038
+ Args:
1039
+ addon_name (str): Addon name.
1040
+ addon_version (str): Addon version.
1041
+ filename (str): Filename in private folder on server.
1042
+ destination_dir (str): Where the file should be downloaded.
1043
+ destination_filename (str): Name of destination filename. Source
1044
+ filename is used if not passed.
1045
+ chunk_size (int): Download chunk size.
1046
+ progress (TransferProgress): Object that gives ability to track
1047
+ download progress.
1048
+
1049
+ Returns:
1050
+ str: Filepath to downloaded file.
1051
+ """
1052
+
1053
+ if not destination_filename:
1054
+ destination_filename = filename
1055
+ dst_filepath = os.path.join(destination_dir, destination_filename)
1056
+ # Filename can contain "subfolders"
1057
+ dst_dirpath = os.path.dirname(dst_filepath)
1058
+ if not os.path.exists(dst_dirpath):
1059
+ os.makedirs(dst_dirpath)
1060
+
1061
+ url = "{}/addons/{}/{}/private/{}".format(
1062
+ self._base_url,
1063
+ addon_name,
1064
+ addon_version,
1065
+ filename
1066
+ )
1067
+ self.download_file(
1068
+ url, dst_filepath, chunk_size=chunk_size, progress=progress
1069
+ )
1070
+ return dst_filepath
1071
+
782
1072
  def get_dependencies_info(self):
783
1073
  result = self.get("dependencies")
784
1074
  return result.data
@@ -839,29 +1129,53 @@ class ServerAPIBase(object):
839
1129
 
840
1130
  def download_dependency_package(
841
1131
  self,
842
- dst_directory,
843
1132
  package_name,
844
- platform_name=None
1133
+ dst_directory,
1134
+ filename,
1135
+ platform_name=None,
1136
+ chunk_size=None,
1137
+ progress=None,
845
1138
  ):
1139
+ """Download dependency package from server.
1140
+
1141
+ This method requires to have authorized token available. The package is
1142
+ only downloaded.
1143
+
1144
+ Args:
1145
+ package_name (str): Name of package to download.
1146
+ dst_directory (str): Where the file should be downloaded.
1147
+ filename (str): Name of destination filename.
1148
+ platform_name (str): Name of platform for which the dependency
1149
+ package is targetter. Default value is current platform.
1150
+ chunk_size (int): Download chunk size.
1151
+ progress (TransferProgress): Object that gives ability to track
1152
+ download progress.
1153
+
1154
+ Returns:
1155
+ str: Filepath to downloaded file.
1156
+ """
846
1157
  if platform_name is None:
847
1158
  platform_name = platform.system().lower()
848
1159
 
849
- package_filepath = os.path.join(dst_directory, package_name + ".zip")
1160
+ package_filepath = os.path.join(dst_directory, filename)
850
1161
  self.download_file(
851
1162
  "dependencies/{}/{}".format(package_name, platform_name),
852
- package_filepath
1163
+ package_filepath,
1164
+ chunk_size=chunk_size,
1165
+ progress=progress
853
1166
  )
854
1167
  return package_filepath
855
1168
 
856
1169
  def upload_dependency_package(
857
- self, filepath, package_name, platform_name=None
1170
+ self, filepath, package_name, platform_name=None, progress=None
858
1171
  ):
859
1172
  if platform_name is None:
860
1173
  platform_name = platform.system().lower()
861
1174
 
862
1175
  self.upload_file(
863
1176
  "dependencies/{}/{}".format(package_name, platform_name),
864
- filepath
1177
+ filepath,
1178
+ progress=progress
865
1179
  )
866
1180
 
867
1181
  def delete_dependency_package(self, package_name, platform_name=None):
@@ -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
 
@@ -85,6 +177,16 @@ def get_server_api_connection():
85
177
  return GlobalContext.get_server_api_connection()
86
178
 
87
179
 
180
+ def get_base_url():
181
+ con = get_server_api_connection()
182
+ return con.get_base_url()
183
+
184
+
185
+ def get_rest_url():
186
+ con = get_server_api_connection()
187
+ return con.get_rest_url()
188
+
189
+
88
190
  def raw_get(*args, **kwargs):
89
191
  con = get_server_api_connection()
90
192
  return con.raw_get(*args, **kwargs)
@@ -135,6 +237,26 @@ def delete(*args, **kwargs):
135
237
  return con.delete(*args, **kwargs)
136
238
 
137
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
+
138
260
  def download_file(*args, **kwargs):
139
261
  con = get_server_api_connection()
140
262
  return con.download_file(*args, **kwargs)
@@ -170,6 +292,11 @@ def get_addons_info(*args, **kwargs):
170
292
  return con.get_addons_info(*args, **kwargs)
171
293
 
172
294
 
295
+ def download_addon_private_file(*args, **kwargs):
296
+ con = get_server_api_connection()
297
+ return con.download_addon_private_file(*args, **kwargs)
298
+
299
+
173
300
  def get_dependencies_info(*args, **kwargs):
174
301
  con = get_server_api_connection()
175
302
  return con.get_dependencies_info(*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
 
@@ -265,3 +324,102 @@ def validate_url(url):
265
324
  title="Couldn't connect to server",
266
325
  hints=hints + universal_hints
267
326
  )
327
+
328
+
329
+ class TransferProgress:
330
+ """Object to store progress of download/upload from/to server."""
331
+
332
+ def __init__(self):
333
+ self._started = False
334
+ self._transfer_done = False
335
+ self._transfered = 0
336
+ self._content_size = None
337
+
338
+ self._failed = False
339
+ self._fail_reason = None
340
+
341
+ self._source_url = "N/A"
342
+ self._destination_url = "N/A"
343
+
344
+ def get_content_size(self):
345
+ return self._content_size
346
+
347
+ def set_content_size(self, content_size):
348
+ if self._content_size is not None:
349
+ raise ValueError("Content size was set more then once")
350
+ self._content_size = content_size
351
+
352
+ def get_started(self):
353
+ return self._started
354
+
355
+ def set_started(self):
356
+ if self._started:
357
+ raise ValueError("Progress already started")
358
+ self._started = True
359
+
360
+ def get_transfer_done(self):
361
+ return self._transfer_done
362
+
363
+ def set_transfer_done(self):
364
+ if self._transfer_done:
365
+ raise ValueError("Progress was already marked as done")
366
+ if not self._started:
367
+ raise ValueError("Progress didn't start yet")
368
+ self._transfer_done = True
369
+
370
+ def get_failed(self):
371
+ return self._failed
372
+
373
+ def get_fail_reason(self):
374
+ return self._fail_reason
375
+
376
+ def set_failed(self, reason):
377
+ self._fail_reason = reason
378
+ self._failed = True
379
+
380
+ def get_transferred_size(self):
381
+ return self._transfered
382
+
383
+ def set_transferred_size(self, transfered):
384
+ self._transfered = transfered
385
+
386
+ def add_transferred_chunk(self, chunk_size):
387
+ self._transfered += chunk_size
388
+
389
+ def get_source_url(self):
390
+ return self._source_url
391
+
392
+ def set_source_url(self, url):
393
+ self._source_url = url
394
+
395
+ def get_destination_url(self):
396
+ return self._destination_url
397
+
398
+ def set_destination_url(self, url):
399
+ self._destination_url = url
400
+
401
+ @property
402
+ def is_running(self):
403
+ if (
404
+ not self.started
405
+ or self.done
406
+ or self.failed
407
+ ):
408
+ return False
409
+ return True
410
+
411
+ @property
412
+ def transfer_progress(self):
413
+ if self._content_size is None:
414
+ return None
415
+ return (self._transfered * 100.0) / float(self._content_size)
416
+
417
+ content_size = property(get_content_size, set_content_size)
418
+ started = property(get_started)
419
+ transfer_done = property(get_transfer_done)
420
+ failed = property(get_failed)
421
+ fail_reason = property(get_fail_reason)
422
+ source_url = property(get_source_url, set_source_url)
423
+ destination_url = property(get_destination_url, set_destination_url)
424
+ content_size = property(get_content_size, set_content_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.6"
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.6
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.6"
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