17
17
API_AUTH_PATH_REGISTER )
18
18
19
19
from arcsecond .api .error import ArcsecondConnectionError , ArcsecondError
20
- from arcsecond .api .helpers import transform_payload_for_multipart_encoder_fields
20
+ from arcsecond .api .helpers import extract_multipart_encoder_file_fields
21
21
from arcsecond .config import config_file_read_api_key , config_file_read_organisation_memberships
22
22
from arcsecond .options import State
23
23
30
30
EVENT_METHOD_PROGRESS_PERCENT = 'EVENT_METHOD_PROGRESS_PERCENT'
31
31
32
32
33
+ class AsyncFileUploader (object ):
34
+ """AsyncFileUploader is a helper class used when uploading files to the cloud.
35
+
36
+ Technically speaking, it can handle any http request in a background thread.
37
+ It is however named like this because it is returned in place of a standard
38
+ response payload when a file is to be uploaded.
39
+ """
40
+
41
+ def __init__ (self , url , method , data = None , payload = None , ** headers ):
42
+ self .url = url
43
+ self .method = method
44
+ self .payload = payload
45
+ self .data = data
46
+ self .headers = headers
47
+ self ._storage = {}
48
+ self ._thread = None
49
+
50
+ def start (self ):
51
+ if self ._thread is None :
52
+ args = (self .url , self .method , self .data , self .payload , self .headers )
53
+ self ._thread = threading .Thread (target = self ._target , args = args )
54
+ self ._thread .start ()
55
+
56
+ def _target (self , url , method , data , payload , headers ):
57
+ try :
58
+ self ._storage ['response' ] = method (url , data = data , json = payload , headers = headers )
59
+ except requests .exceptions .ConnectionError :
60
+ self ._storage ['error' ] = ArcsecondConnectionError (url )
61
+ except Exception as e :
62
+ self ._storage ['error' ] = ArcsecondError (str (e ))
63
+
64
+ def finish (self ):
65
+ self .join ()
66
+ return self .get_results ()
67
+
68
+ def join (self ):
69
+ self ._thread .join ()
70
+
71
+ def is_alive (self ):
72
+ return self ._thread .is_alive ()
73
+
74
+ def get_results (self ):
75
+ response = self ._storage .get ('response' )
76
+ if isinstance (response , dict ):
77
+ # Responses of standard JSON payload requests are dict
78
+ return response
79
+ elif response is not None :
80
+ if 200 <= response .status_code < 300 :
81
+ return response .json () if response .text else {}, None
82
+ else :
83
+ return None , response .text
84
+ else :
85
+ return None , self ._storage .get ('error' )
86
+
87
+
33
88
class APIEndPoint (object ):
34
89
name = None
35
90
@@ -72,62 +127,50 @@ def _check_uuid(self, uuid_str):
72
127
except ValueError :
73
128
raise ArcsecondError ('Invalid UUID {}.' .format (uuid_str ))
74
129
75
- def _check_and_set_api_key (self , headers , url ):
76
- if API_AUTH_PATH_REGISTER in url or API_AUTH_PATH_LOGIN in url or 'Authorization' in headers .keys ():
77
- return headers
78
-
79
- if self .state .verbose :
80
- click .echo ('Checking local API key... ' , nl = False )
81
-
82
- api_key = config_file_read_api_key (self .state .config_section ())
83
- if not api_key :
84
- raise ArcsecondError ('Missing API key. You must login first: $ arcsecond login' )
85
-
86
- headers ['X-Arcsecond-API-Authorization' ] = 'Key ' + api_key
130
+ def list (self , name = '' , ** headers ):
131
+ return self ._perform_request (self ._list_url (name ), 'get' , None , None , ** headers )
87
132
88
- if self .state .verbose :
89
- click .echo ('OK' )
90
- return headers
133
+ def create (self , payload , callback = None , ** headers ):
134
+ # If a file is provided as part of the payload, a instance of AsyncFileUploader is returned
135
+ # in place of a standard JSON body response.
136
+ return self ._perform_request (self ._list_url (), 'post' , payload , callback , ** headers )
91
137
92
- def _check_organisation_membership_and_permission (self , method_name , organisation ):
93
- memberships = config_file_read_organisation_memberships (self .state .config_section ())
94
- if self .state .organisation not in memberships .keys ():
95
- raise ArcsecondError ('No membership found for organisation {}' .format (organisation ))
138
+ def read (self , id_name_uuid , ** headers ):
139
+ return self ._perform_request (self ._detail_url (id_name_uuid ), 'get' , None , None , ** headers )
96
140
97
- membership = memberships [self .state .organisation ]
98
- if method_name not in SAFE_METHODS and membership not in WRITABLE_MEMBERSHIPS :
99
- raise ArcsecondError ('Membership for organisation {} has no write permission' .format (organisation ))
141
+ def update (self , id_name_uuid , payload , ** headers ):
142
+ return self ._perform_request (self ._detail_url (id_name_uuid ), 'put' , payload , None , ** headers )
100
143
101
- def _async_perform_request (self , url , method , payload = None , ** headers ):
102
- def _async_perform_request_store_response (storage , method , url , payload , headers ):
103
- try :
104
- storage ['response' ] = method (url , json = payload , headers = headers )
105
- except requests .exceptions .ConnectionError :
106
- storage ['error' ] = ArcsecondConnectionError (self ._get_base_url ())
107
- except Exception as e :
108
- storage ['error' ] = ArcsecondError (str (e ))
144
+ def delete (self , id_name_uuid , ** headers ):
145
+ return self ._perform_request (self ._detail_url (id_name_uuid ), 'delete' , None , None , ** headers )
109
146
110
- storage = {}
111
- thread = threading .Thread (target = _async_perform_request_store_response ,
112
- args = (storage , method , url , payload , headers ))
113
- thread .start ()
147
+ def _perform_request (self , url , method , payload , callback = None , ** headers ):
148
+ method_name , method , payload , headers = self ._prepare_request (url , method , payload , ** headers )
114
149
115
- spinner = Spinner ()
116
- while thread .is_alive ():
117
- if self .state .verbose :
118
- spinner .next ()
119
- thread .join ()
120
- if self .state .verbose :
121
- click .echo ()
150
+ payload , fields = extract_multipart_encoder_file_fields (payload )
151
+ if fields is None :
152
+ # Standard JSON sync request
153
+ return self ._perform_spinner_request (url , method , method_name , None , payload , ** headers )
154
+ else :
155
+ # Process payload synchronously nonetheless
156
+ if payload :
157
+ self ._perform_spinner_request (url , method , method_name , None , payload , ** headers )
122
158
123
- if 'error' in storage .keys ():
124
- raise storage .get ('error' )
159
+ # File upload
160
+ upload_monitor = self ._build_dynamic_upload_data (fields , callback )
161
+ headers .update (** {'Content-Type' : upload_monitor .content_type })
125
162
126
- return storage .get ('response' , None )
163
+ if self .state .is_using_cli :
164
+ return self ._perform_spinner_request (url , method , method_name , upload_monitor , None , ** headers )
165
+ else :
166
+ return AsyncFileUploader (url , method , data = upload_monitor , payload = None , ** headers )
127
167
128
168
def _prepare_request (self , url , method , payload , ** headers ):
129
169
assert (url and method )
130
170
171
+ if self .state .verbose :
172
+ click .echo ('Preparing request...' )
173
+
131
174
if not isinstance (method , str ) or callable (method ):
132
175
raise ArcsecondError ('Invalid HTTP request method {}. ' .format (str (method )))
133
176
@@ -145,62 +188,73 @@ def _prepare_request(self, url, method, payload, **headers):
145
188
# Filtering None values out of payload.
146
189
payload = {k : v for k , v in payload .items () if v is not None }
147
190
148
- return url , method_name , method , payload , headers
191
+ return method_name , method , payload , headers
149
192
150
- def _perform_request (self , url , method , payload , callback = None , ** headers ):
151
- if self . state . verbose :
152
- click . echo ( 'Preparing request...' )
193
+ def _build_dynamic_upload_data (self , fields , callback = None ):
194
+ # The monitor is the data!
195
+ encoded_data = encoder . MultipartEncoder ( fields = fields )
153
196
154
- url , method_name , method , payload , headers = self ._prepare_request (url , method , payload , ** headers )
197
+ if self .state .is_using_cli is True and self .state .verbose :
198
+ bar = Bar ('Uploading ' + fields ['file' ][0 ], suffix = '%(percent)d%%' )
199
+ return encoder .MultipartEncoderMonitor (encoded_data , lambda m : bar .goto (m .bytes_read / m .len * 100 ))
200
+ elif self .state .is_using_cli is False and callback :
201
+ return encoder .MultipartEncoderMonitor (encoded_data , lambda m : callback (EVENT_METHOD_PROGRESS_PERCENT ,
202
+ m .bytes_read / m .len * 100 ))
203
+ else :
204
+ return encoder .MultipartEncoderMonitor (encoded_data , None )
155
205
206
+ def _perform_spinner_request (self , url , method , method_name , data = None , payload = None , ** headers ):
156
207
if self .state .verbose :
157
208
click .echo ('Sending {} request to {}' .format (method_name , url ))
209
+ click .echo ('Payload: {}' .format (payload ))
158
210
159
- payload , fields = transform_payload_for_multipart_encoder_fields (payload )
160
- if fields :
161
- encoded_data = encoder .MultipartEncoder (fields = fields )
162
- bar , upload_callback = None , None
211
+ performer = AsyncFileUploader (url , method , data = data , payload = payload , ** headers )
212
+ performer .start ()
163
213
164
- if self .state .is_using_cli is False and callback :
165
- upload_callback = lambda m : callback (EVENT_METHOD_PROGRESS_PERCENT , m .bytes_read / m .len * 100 )
166
- elif self .state .verbose :
167
- bar = Bar ('Uploading ' + fields ['file' ][0 ], suffix = '%(percent)d%%' )
168
- upload_callback = lambda m : bar .goto (m .bytes_read / m .len * 100 )
169
-
170
- upload_monitor = encoder .MultipartEncoderMonitor (encoded_data , upload_callback )
171
- headers .update (** {'Content-Type' : upload_monitor .content_type })
172
- response = method (url , data = upload_monitor , headers = headers )
173
-
174
- if self .state .verbose :
175
- bar .finish ()
176
- else :
214
+ spinner = Spinner ()
215
+ while performer .is_alive ():
177
216
if self .state .verbose :
178
- click .echo ('Payload: {}' .format (payload ))
217
+ spinner .next ()
218
+
219
+ response , error = performer .finish ()
179
220
180
- response = self ._async_perform_request (url , method , payload , ** headers )
221
+ # If we have an error and it is an ArcsecondError, raise it.
222
+ # As for now, only ArcsecondError could be returned, and there is no
223
+ # real point of returning both response and error below. But
224
+ # methods in main.py expect them both.
181
225
182
- if response is None :
183
- raise ArcsecondConnectionError ( url )
226
+ if error and isinstance ( error , ArcsecondError ) :
227
+ raise error
184
228
185
229
if self .state .verbose :
230
+ click .echo ()
186
231
click .echo ('Request status code ' + str (response .status_code ))
187
232
188
- if 200 <= response .status_code < 300 :
189
- return response .json () if response .text else {}, None
190
- else :
191
- return None , response .text
233
+ return response , error
192
234
193
- def list (self , name = '' , ** headers ):
194
- return self ._perform_request (self ._list_url (name ), 'get' , None , None , ** headers )
235
+ def _check_and_set_api_key (self , headers , url ):
236
+ if API_AUTH_PATH_REGISTER in url or API_AUTH_PATH_LOGIN in url or 'Authorization' in headers .keys ():
237
+ return headers
195
238
196
- def create ( self , payload , callback = None , ** headers ) :
197
- return self . _perform_request ( self . _list_url (), 'post' , payload , callback , ** headers )
239
+ if self . state . verbose :
240
+ click . echo ( 'Checking local API key... ' , nl = False )
198
241
199
- def read (self , id_name_uuid , ** headers ):
200
- return self ._perform_request (self ._detail_url (id_name_uuid ), 'get' , None , None , ** headers )
242
+ api_key = config_file_read_api_key (self .state .config_section ())
243
+ if not api_key :
244
+ raise ArcsecondError ('Missing API key. You must login first: $ arcsecond login' )
201
245
202
- def update (self , id_name_uuid , payload , ** headers ):
203
- return self ._perform_request (self ._detail_url (id_name_uuid ), 'put' , payload , None , ** headers )
246
+ headers ['X-Arcsecond-API-Authorization' ] = 'Key ' + api_key
204
247
205
- def delete (self , id_name_uuid , ** headers ):
206
- return self ._perform_request (self ._detail_url (id_name_uuid ), 'delete' , None , None , ** headers )
248
+ if self .state .verbose :
249
+ click .echo ('OK' )
250
+
251
+ return headers
252
+
253
+ def _check_organisation_membership_and_permission (self , method_name , organisation ):
254
+ memberships = config_file_read_organisation_memberships (self .state .config_section ())
255
+ if self .state .organisation not in memberships .keys ():
256
+ raise ArcsecondError ('No membership found for organisation {}' .format (organisation ))
257
+
258
+ membership = memberships [self .state .organisation ]
259
+ if method_name not in SAFE_METHODS and membership not in WRITABLE_MEMBERSHIPS :
260
+ raise ArcsecondError ('Membership for organisation {} has no write permission' .format (organisation ))
0 commit comments