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.
- {ayon-python-api-0.1.6/ayon_python_api.egg-info → ayon-python-api-0.1.8}/PKG-INFO +1 -1
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/__init__.py +30 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/exceptions.py +4 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/server.py +351 -37
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/server_api.py +127 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/utils.py +158 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/version.py +1 -1
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8/ayon_python_api.egg-info}/PKG-INFO +1 -1
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/pyproject.toml +2 -1
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/LICENSE +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/README.md +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/constants.py +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/entity_hub.py +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/events.py +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/graphql.py +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/graphql_queries.py +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/operations.py +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_api/thumbnails.py +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/SOURCES.txt +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/dependency_links.txt +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/requires.txt +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/top_level.txt +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/setup.cfg +0 -0
- {ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/setup.py +0 -0
|
@@ -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",
|
|
@@ -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,
|
|
77
|
-
if
|
|
78
|
-
|
|
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.
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
2
|
+
__version__ = "0.1.8"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ayon_api"
|
|
3
|
-
version = "0.1.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ayon-python-api-0.1.6 → ayon-python-api-0.1.8}/ayon_python_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|