Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: cms/bin/xtm_translations/xtm_api.py

Issue 29886648: Issue #6942 - Add XTM integration in CMS (Closed)
Patch Set: Addressed initial comments Created Sept. 25, 2018, 12:24 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(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')
OLDNEW

Powered by Google App Engine
This is Rietveld