Left: | ||
Right: |
OLD | NEW |
---|---|
(Empty) | |
1 # This file is part of the Adblock Plus web scripts, | |
2 # Copyright (C) 2006-present eyeo GmbH | |
3 # | |
4 # Adblock Plus is free software: you can redistribute it and/or modify | |
5 # it under the terms of the GNU General Public License version 3 as | |
6 # published by the Free Software Foundation. | |
7 # | |
8 # Adblock Plus is distributed in the hope that it will be useful, | |
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 # GNU General Public License for more details. | |
12 # | |
13 # You should have received a copy of the GNU General Public License | |
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | |
15 | |
16 from __future__ import unicode_literals | |
17 | |
18 import requests | |
Vasily Kuznetsov
2018/09/26 15:45:24
Imports order should be stdlib, 3rd party, local w
Tudor Avram
2018/10/04 06:48:07
Done.
| |
19 import json | |
20 | |
21 __all__ = [ | |
22 'XTMCloudAPI', 'XTMCloudException', 'get_token', | |
23 ] | |
24 | |
25 _BASE_URL = 'https://wstest2.xtm-intl.com/rest-api/' | |
Vasily Kuznetsov
2018/09/26 15:45:24
Shouldn't this URL be configurable somehow?
Tudor Avram
2018/10/04 06:48:09
Going to discuss the level of configurability with
Vasily Kuznetsov
2018/10/05 10:56:25
Acknowledged.
| |
26 | |
27 | |
28 class XTMCloudException(Exception): | |
29 _BASE_MESSAGE = ('Error: XTM Cloud API failed while {0}, with error ' | |
30 'code {1}: {2}') | |
31 | |
32 def __init__(self, code, msg, action): | |
33 """Constructor. | |
34 | |
35 Parameters | |
36 ---------- | |
37 code: int | |
38 The error code returned by the API. | |
39 msg: str | |
40 The error message returned by the API. | |
41 action: str | |
42 The action that caused the exception. | |
43 | |
44 """ | |
45 full_message = self._BASE_MESSAGE.format(action, code, msg) | |
46 super(XTMCloudException, self).__init__(full_message) | |
Vasily Kuznetsov
2018/09/26 15:45:24
Do you think somebody might be interested in havin
Tudor Avram
2018/10/04 06:48:08
Done.
| |
47 | |
48 | |
49 class XTMCloudAPI(object): | |
50 | |
51 _AUTHORIZATION_TMP = 'XTM-Basic {0}' | |
52 _URL_PATHS = { | |
53 'create': 'projects', | |
Vasily Kuznetsov
2018/09/26 15:45:24
So I think I understand my biggest problem with ha
Tudor Avram
2018/10/04 06:48:08
Done.
| |
54 'download_multiple': 'projects/{0}/files/download', | |
55 'downlload_single': 'projects/{0}/files/{1}/download', | |
56 'upload': 'projects/{0}/files/upload', | |
57 'get_target_lang': 'projects/{0}/metrics', | |
58 'add_target_lang': 'projects/{0}/target-languages', | |
59 } | |
60 _DEFAULT_CONTENT_TYPE = 'multipart/form-data' | |
Vasily Kuznetsov
2018/09/26 15:45:24
Are we using this constant anywhere?
Tudor Avram
2018/10/04 06:48:10
Nope. Was using that initially, forgot to remove i
| |
61 _MATCH_TYPE = { | |
62 'upload': 'NO_MATCH', | |
63 'update': 'MATCH_NAMES', | |
64 } | |
65 _URL_PARAMS = { | |
66 'download': '?fileType={1}', | |
67 } | |
68 _SUCCESS_CODES = { | |
69 'create': 201, | |
70 'upload': 200, | |
71 'download': 200, | |
72 'get_target_lang': 200, | |
73 'add_target_lang': 200, | |
74 } | |
75 | |
76 def __init__(self, token): | |
77 """Constructor. | |
78 | |
79 Parameters | |
80 ---------- | |
81 token: str | |
82 Token used to authenticate with the API. | |
83 | |
84 """ | |
85 self._token = token | |
86 self.base_url = _BASE_URL | |
87 | |
88 def _execute(self, url, data=None, files=None, stream=False, | |
89 params=None, headers=None): | |
90 """Private method executing requests and returning the result. | |
Vasily Kuznetsov
2018/09/26 15:45:24
Nit: I think we don't really need to include the i
Tudor Avram
2018/10/04 06:48:09
Done.
| |
91 | |
92 Parameters | |
93 ---------- | |
94 url: str | |
95 The url we're making the request to. | |
96 data: dict | |
97 The data to be sent to the API. If provided, the request will be | |
98 a POST request, otherwise GET. Default None. | |
99 files: dict | |
100 The files to be uploaded(if any). Default None. | |
101 params: dict | |
102 The URL parameters to be specified. | |
103 stream: bool | |
104 Whether using the stream option when executing the request or not. | |
105 headers: dict | |
106 Default headers to be sent with the request. | |
107 | |
108 Returns | |
109 ------- | |
110 The contents of the API response. | |
111 | |
112 """ | |
113 auth_header = { | |
114 'Authorization': self._AUTHORIZATION_TMP.format(self._token), | |
115 } | |
116 | |
117 if headers: | |
118 headers.update(auth_header) | |
119 else: | |
120 headers = auth_header | |
121 | |
122 if data: | |
123 response = requests.post(url, data=data, files=files, | |
124 headers=headers, params=params, | |
125 stream=stream) | |
126 else: | |
127 response = requests.get(url, headers=headers, stream=True, | |
128 params=params) | |
129 | |
130 return response | |
131 | |
132 def _construct_files_dict(self, files, param_tmp): | |
133 """Private method converting a list of files to a uploadable format. | |
134 | |
135 Parameters | |
136 ---------- | |
137 files: iterable | |
138 Of the files to be uploaded. See create_project() for expected | |
139 format. | |
140 | |
141 Returns | |
142 ------- | |
143 dict | |
144 With the names of the files to be uploaded. | |
145 dict | |
146 With the files in the correct format for upload. | |
147 | |
148 """ | |
149 files_to_upload = {} | |
150 file_names = {} | |
151 | |
152 for idx in range(len(files)): | |
153 if files[idx][0]: | |
154 file_names[(param_tmp + '.name').format(idx)] = files[idx][0] | |
155 files_to_upload[(param_tmp + '.file').format(idx)] = files[idx][1] | |
156 | |
157 return file_names, files_to_upload | |
158 | |
159 def create_project(self, name, description, reference_id, target_languages, | |
160 customer_id, workflow_id, source_language='en_US', | |
161 files=None): | |
162 """Create a new project with XTM Cloud. | |
163 | |
164 Parameters | |
165 ---------- | |
166 name: str | |
167 The name of the project we want to create. | |
168 description: str | |
169 The description of the project. | |
170 reference_id: str | |
171 The referenceID of the project. | |
172 target_languages: iterable | |
173 The target language(s) for the project. | |
174 customer_id: int | |
175 The id of the customer creating the project | |
176 workflow_id: int | |
177 The id of the main workflow | |
178 source_language: str | |
179 The source language for the project. | |
180 files: iterable | |
181 With the files to be uploaded on creation Expects a list of | |
182 tuples of the form: | |
183 (<file_name>, <file_content>). | |
184 | |
185 If <file_name> is None, it will be ignored. | |
186 | |
187 Returns | |
188 ------- | |
189 int | |
190 The id of the created project. | |
191 iterable | |
192 Containing the information for all the jobs that were initiated. | |
193 See docs for upload_files() for the fully documented format. | |
194 | |
195 Raises | |
196 ------ | |
197 XTMCloudException | |
198 If the creation of the project was not successful. | |
199 | |
200 """ | |
201 data = { | |
202 'name': name, | |
203 'description': description, | |
204 'referenceId': reference_id, | |
205 'customerId': customer_id, | |
206 'workflowId': workflow_id, | |
207 'sourceLanguage': source_language, | |
208 'targetLanguages': target_languages, | |
209 } | |
210 | |
211 if files: | |
212 file_names, files_to_upload = self._construct_files_dict( | |
213 files, 'translationFiles[{}]') | |
214 data.update(file_names) | |
215 else: | |
216 # Hacky way to go around 415 error code | |
217 files_to_upload = {'a': 'b'} | |
218 | |
219 url = self.base_url + self._URL_PATHS['create'] | |
220 | |
221 response = self._execute(url, data=data, files=files_to_upload) | |
222 | |
223 if response.status_code != self._SUCCESS_CODES['create']: | |
224 # The creation was not successful | |
225 raise XTMCloudException(response.status_code, | |
226 response.text.decode('utf-8'), | |
227 'creating job') | |
228 | |
229 data = json.loads(response.text.encode('utf-8')) | |
230 | |
231 return data['projectId'], data['jobs'] | |
232 | |
233 def upload_files(self, files, project_id, overwrite=True): | |
234 """Upload a set of files to a project. | |
235 | |
236 Parameters | |
237 ---------- | |
238 files: iterable | |
239 Of the files to be uploaded. | |
240 Expects a list of tuples with the following format: | |
241 (<file_name>, <file_content>) | |
242 If <file_name> is None, it will be ignored. | |
243 | |
244 project_id: int | |
245 The id of the project we're uploading to. | |
246 overwrite: bool | |
247 Whether the files to be uploaded are going to overwrite the files | |
248 that are already in the XTM project or not. Default False. | |
249 | |
250 Returns | |
251 ------- | |
252 iterable | |
253 With the upload results. Each element will have the form: | |
254 | |
255 { | |
256 'fileName': <the name of the uploaded file>, | |
257 'jobId': <ID of the job associated with the file>, | |
258 'sourceLanguage': <source language of the uploaded file>, | |
259 'targetLanguage': <target language of the uploaded file>, | |
260 } | |
261 | |
262 Raises | |
263 ------ | |
264 XTMCloudException | |
265 If the API operation fails. | |
266 | |
267 """ | |
268 file_names, files_to_upload = self._construct_files_dict( | |
269 files, 'files[{}]', | |
270 ) | |
271 | |
272 if len(files_to_upload.keys()) == 0: | |
273 raise Exception('Error: No files provided for upload.') | |
274 | |
275 data = { | |
276 'matchType': self._MATCH_TYPE['update'] if overwrite else | |
Vasily Kuznetsov
2018/09/26 15:45:24
Maybe it would make more sense to have True and Fa
Tudor Avram
2018/10/04 06:48:09
Done.
| |
277 self._MATCH_TYPE['upload'], | |
278 } | |
279 data.update(file_names) | |
280 | |
281 url = self.base_url + self._URL_PATHS['upload'].format(project_id) | |
282 | |
283 response = self._execute(url, data=data, files=files_to_upload) | |
284 | |
285 if response.status_code != self._SUCCESS_CODES['upload']: | |
286 raise XTMCloudException(response.status_code, | |
287 response.text.encode('utf-8'), | |
288 'uploading files') | |
289 | |
290 return json.loads(response.text.encode('utf-8'))['jobs'] | |
291 | |
292 def download_files(self, project_id, files_type='TARGET'): | |
293 """Download files for a specific project. | |
294 | |
295 Parameters | |
296 ---------- | |
297 project_id: int | |
298 The id of the project to download from. | |
299 files_type: str | |
300 The type of the files we want to download. Default TARGET | |
301 | |
302 Returns | |
303 ------- | |
304 bytes | |
305 The contents of the zip file returned by the API | |
306 | |
307 Raises | |
308 ------ | |
309 XTMCloudException | |
310 If the request is flawed in any way. | |
311 | |
312 """ | |
313 url = (self.base_url + self._URL_PATHS['download_multiple']).format( | |
314 project_id, | |
315 ) | |
316 | |
317 exception_msg = { | |
318 400: 'Invalid request', | |
319 401: 'Authentication failed', | |
320 500: 'Internal server error', | |
321 404: 'Project not found.', | |
322 } | |
323 | |
324 response = self._execute(url, params={'fileType': files_type}, | |
325 stream=True) | |
326 | |
327 if response.status_code != self._SUCCESS_CODES['download']: | |
328 raise XTMCloudException( | |
329 response.status_code, | |
330 exception_msg[response.status_code], | |
331 'downloading files from {}'.format(project_id), | |
332 ) | |
333 | |
334 return response.content | |
335 | |
336 def get_target_languages(self, project_id): | |
337 """Get all the target languages for a specific project. | |
338 | |
339 Parameters | |
340 ---------- | |
341 project_id: int | |
342 The id if the project in question. | |
343 | |
344 Returns | |
345 ------- | |
346 iterable | |
347 With the target languages for this project. | |
348 | |
349 Raises | |
350 ------ | |
351 XTMCloudException | |
352 If the request is unsuccessful. | |
353 | |
354 """ | |
355 url = (self.base_url + self._URL_PATHS['get_target_lang']).format( | |
356 project_id, | |
357 ) | |
358 | |
359 response = self._execute(url, stream=True) | |
360 | |
361 if response.status_code != self._SUCCESS_CODES['get_target_lang']: | |
362 raise XTMCloudException(response.status_code, response.content, | |
363 'extracting target languages') | |
364 | |
365 data = json.loads(response.content.encode('utf-8')) | |
366 return {item['targetLanguage'] for item in data} | |
367 | |
368 def add_target_languages(self, project_id, target_languages): | |
369 """Add target languages to a project. | |
370 | |
371 Parameters | |
372 ---------- | |
373 project_id: int | |
374 The id of the project in question. | |
375 target_languages: iterable | |
376 The languages to be added. | |
377 | |
378 Raises | |
379 ------- | |
380 XTMCloudException | |
381 If the request to the API was not successful. | |
382 | |
383 """ | |
384 data = json.dumps({ | |
385 'targetLanguages': target_languages, | |
386 }) | |
387 url = (self.base_url + self._URL_PATHS['add_target_lang']).format( | |
388 project_id, | |
389 ) | |
390 headers = {'content-type': 'application/json'} | |
391 | |
392 response = self._execute(url, data=data, headers=headers) | |
393 | |
394 if response.status_code != self._SUCCESS_CODES['add_target_lang']: | |
395 raise XTMCloudException(response.status_code, response.content, | |
396 'adding target languages to project') | |
397 | |
398 | |
399 def get_token(username, password, user_id): | |
Vasily Kuznetsov
2018/09/26 15:45:25
Maybe this should also be a method of XTMCloudAPI
Tudor Avram
2018/10/04 06:48:08
We have already discussed this in person. As I men
Vasily Kuznetsov
2018/10/05 10:56:25
Allright, makes sense.
| |
400 """Generate an API token from username and password. | |
401 | |
402 Parameters | |
403 ---------- | |
404 username: str | |
405 The username used to generate the token. | |
406 password: str | |
407 The password used to generate the token. | |
408 user_id: int | |
409 The user ID used to generate the token. | |
410 | |
411 Returns | |
412 ------- | |
413 str | |
414 The resulting token generated by the API. | |
415 | |
416 Raises | |
417 ------ | |
418 XTMCloudException | |
419 If the credentials provided were invalid. | |
420 | |
421 """ | |
422 request_body = json.dumps({ | |
423 'client': username, | |
424 'password': password, | |
425 'userId': user_id, | |
426 }) | |
427 | |
428 url = _BASE_URL + 'auth/token' | |
429 | |
430 headers = {'content-type': 'application/json'} | |
431 | |
432 response = requests.post(url, data=request_body, headers=headers) | |
433 | |
434 if response.status_code == 200: | |
435 return json.loads(response.text)['token'].encode() | |
436 | |
437 raise XTMCloudException(response.status_code, | |
438 response.text.encode('utf-8'), | |
439 'generating token') | |
OLD | NEW |