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

Delta Between Two Patch Sets: cms/translations/xtm/xtm_api.py

Issue 29886648: Issue #6942 - Add XTM integration in CMS (Closed)
Left Patch Set: Addressed initial comments Created Sept. 25, 2018, 12:24 p.m.
Right Patch Set: Addressed comments from Patch Set #4 Created Oct. 5, 2018, 4:23 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Right: Side by side diff | Download
LEFTRIGHT
(no file at all)
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 json
19
20 import requests
21
22 __all__ = [
23 'XTMCloudAPI', 'XTMCloudException', 'get_token',
24 ]
25
26 _BASE_URL = 'https://wstest2.xtm-intl.com/rest-api/'
wspee 2018/10/10 15:09:15 This is a hardcoded url to a user specific test sy
27
28
29 class XTMCloudException(Exception):
30 _BASE_MESSAGE = ('Error: XTM Cloud API failed while {0}, with error '
31 'code {1}: {2}')
32
33 def __init__(self, code, msg, action):
34 """Constructor.
35
36 Parameters
37 ----------
38 code: int
39 The error code returned by the API.
40 msg: str
41 The error message returned by the API.
42 action: str
43 The action that caused the exception.
44
45 """
46 full_message = self._BASE_MESSAGE.format(action, code, msg)
47 super(XTMCloudException, self).__init__(full_message)
48
49 self.code = code
50 self.message = msg
51 self.action = action
52
53
54 class XTMCloudAPI(object):
55
56 _AUTHORIZATION_TMP = 'XTM-Basic {0}'
57
58 class _UrlPaths:
59 CREATE = 'projects'
60 DOWNLOAD = 'projects/{0}/files/download'
61 UPLOAD = 'projects/{0}/files/upload'
62 GET_TARGET_LANG = 'projects/{0}/metrics'
63 ADD_TARGET_LANG = 'projects/{0}/target-languages'
64
65 _MATCH_TYPE = {
66 False: 'NO_MATCH',
67 True: 'MATCH_NAMES',
68 }
69
70 class _SuccessCodes:
71 CREATE = 201
72 UPLOAD = 200
73 DOWNLOAD = 200
74 GET_TARGET_LANGS = 200
75 ADD_TARGET_LANGS = 200
76
77 def __init__(self, token):
78 """Constructor.
79
80 Parameters
81 ----------
82 token: str
83 Token used to authenticate with the API.
84
85 """
86 self._token = token
87 self.base_url = _BASE_URL
88
89 def _execute(self, url, data=None, files=None, stream=False,
90 params=None, headers=None):
91 """Send request to the API and return the response.
92
93 Parameters
94 ----------
95 url: str
96 The url we're making the request to.
97 data: dict
98 The data to be sent to the API. If provided, the request will be
99 a POST request, otherwise GET. Default None.
100 files: dict
101 The files to be uploaded(if any). Default None.
102 params: dict
103 The URL parameters to be specified.
104 stream: bool
105 Whether using the stream option when executing the request or not.
106 headers: dict
107 Default headers to be sent with the request.
108
109 Returns
110 -------
111 The contents of the API response.
112
113 """
114 auth_header = {
115 'Authorization': self._AUTHORIZATION_TMP.format(self._token),
116 }
117
118 if headers:
119 headers.update(auth_header)
120 else:
121 headers = auth_header
122
123 if data:
124 response = requests.post(url, data=data, files=files,
125 headers=headers, params=params,
126 stream=stream)
127 else:
128 response = requests.get(url, headers=headers, stream=True,
129 params=params)
130
131 return response
132
133 def _construct_files_dict(self, files, param_tmp):
134 """Convert a list of files to an uploadable format.
135
136 Parameters
137 ----------
138 files: iterable
139 Of the files to be uploaded. See create_project() for expected
140 format.
141
142 Returns
143 -------
144 dict
145 With the names of the files to be uploaded.
146 dict
147 With the files in the correct format for upload.
148
149 """
150 files_data = {}
151 file_names = {}
152 idx = 0
153
154 for name in files:
155 file_names[(param_tmp + '.name').format(idx)] = name
156 files_data[(param_tmp + '.file').format(idx)] = files[name]
157 idx += 1
158
159 return file_names, files_data
160
161 def create_project(self, name, description, reference_id, target_languages,
162 customer_id, workflow_id, source_language='en_US',
163 files=None):
164 """Create a new project with XTM Cloud.
165
166 Parameters
167 ----------
168 name: str
169 The name of the project we want to create.
170 description: str
171 The description of the project.
172 reference_id: str
173 The referenceID of the project.
174 target_languages: iterable
175 The target language(s) for the project.
176 customer_id: int
177 The id of the customer creating the project
178 workflow_id: int
179 The id of the main workflow
180 source_language: str
181 The source language for the project.
182 files: dict
183 With the files to be uploaded on creation.
184 Expects this format:
185 <file_name>: <file_data>
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._UrlPaths.CREATE
220
221 response = self._execute(url, data=data, files=files_to_upload)
222
223 if response.status_code != self._SuccessCodes.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: dict
239 With the files to be uploaded on creation.
240 Expects this format:
241 <file_name>: <file_data>
242 project_id: int
243 The id of the project we're uploading to.
244 overwrite: bool
245 Whether the files to be uploaded are going to overwrite the files
246 that are already in the XTM project or not. Default False.
247
248 Returns
249 -------
250 iterable
251 With the upload results. Each element will have the form:
252
253 {
254 'fileName': <the name of the uploaded file>,
255 'jobId': <ID of the job associated with the file>,
256 'sourceLanguage': <source language of the uploaded file>,
257 'targetLanguage': <target language of the uploaded file>,
258 }
259
260 Raises
261 ------
262 XTMCloudException
263 If the API operation fails.
264
265 """
266 file_names, files_to_upload = self._construct_files_dict(
267 files, 'files[{}]',
268 )
269
270 if len(files_to_upload.keys()) == 0:
271 raise Exception('Error: No files provided for upload.')
272
273 data = {'matchType': self._MATCH_TYPE[overwrite]}
274 data.update(file_names)
275
276 url = self.base_url + self._UrlPaths.UPLOAD.format(project_id)
277
278 response = self._execute(url, data=data, files=files_to_upload)
279
280 if response.status_code != self._SuccessCodes.UPLOAD:
281 raise XTMCloudException(response.status_code,
282 response.text.encode('utf-8'),
283 'uploading files')
284
285 return json.loads(response.text.encode('utf-8'))['jobs']
286
287 def download_files(self, project_id, files_type='TARGET'):
288 """Download files for a specific project.
289
290 Parameters
291 ----------
292 project_id: int
293 The id of the project to download from.
294 files_type: str
295 The type of the files we want to download. Default TARGET
296
297 Returns
298 -------
299 bytes
300 The contents of the zip file returned by the API
301
302 Raises
303 ------
304 XTMCloudException
305 If the request is flawed in any way.
306
307 """
308 url = (self.base_url + self._UrlPaths.DOWNLOAD).format(project_id)
309
310 exception_msg = {
311 400: 'Invalid request',
312 401: 'Authentication failed',
313 500: 'Internal server error',
314 404: 'Project not found.',
315 }
316
317 response = self._execute(url, params={'fileType': files_type},
318 stream=True)
319
320 if response.status_code != self._SuccessCodes.DOWNLOAD:
321 raise XTMCloudException(
322 response.status_code,
323 exception_msg[response.status_code],
324 'downloading files from {}'.format(project_id),
325 )
326
327 return response.content
328
329 def get_target_languages(self, project_id):
330 """Get all the target languages for a specific project.
331
332 Parameters
333 ----------
334 project_id: int
335 The id if the project in question.
336
337 Returns
338 -------
339 iterable
340 With the target languages for this project.
341
342 Raises
343 ------
344 XTMCloudException
345 If the request is unsuccessful.
346
347 """
348 url = (self.base_url + self._UrlPaths.GET_TARGET_LANG).format(
349 project_id,
350 )
351
352 response = self._execute(url, stream=True)
353
354 if response.status_code != self._SuccessCodes.GET_TARGET_LANGS:
355 raise XTMCloudException(response.status_code, response.content,
356 'extracting target languages')
357
358 data = json.loads(response.content.encode('utf-8'))
359 return {item['targetLanguage'] for item in data}
360
361 def add_target_languages(self, project_id, target_languages):
362 """Add target languages to a project.
363
364 Parameters
365 ----------
366 project_id: int
367 The id of the project in question.
368 target_languages: iterable
369 The languages to be added.
370
371 Raises
372 -------
373 XTMCloudException
374 If the request to the API was not successful.
375
376 """
377 data = json.dumps({
378 'targetLanguages': target_languages,
379 })
380 url = (self.base_url + self._UrlPaths.ADD_TARGET_LANG).format(
381 project_id,
382 )
383 headers = {'content-type': 'application/json'}
384
385 response = self._execute(url, data=data, headers=headers)
386
387 if response.status_code != self._SuccessCodes.ADD_TARGET_LANGS:
388 raise XTMCloudException(response.status_code, response.content,
389 'adding target languages to project')
390
391
392 def get_token(username, password, user_id):
393 """Generate an API token from username and password.
394
395 Parameters
396 ----------
397 username: str
398 The username used to generate the token.
399 password: str
400 The password used to generate the token.
401 user_id: int
402 The user ID used to generate the token.
403
404 Returns
405 -------
406 str
407 The resulting token generated by the API.
408
409 Raises
410 ------
411 XTMCloudException
412 If the credentials provided were invalid.
413
414 """
415 request_body = json.dumps({
416 'client': username,
417 'password': password,
418 'userId': user_id,
419 })
420
421 url = _BASE_URL + 'auth/token'
422
423 headers = {'content-type': 'application/json'}
424
425 response = requests.post(url, data=request_body, headers=headers)
426
427 if response.status_code == 200:
428 return json.loads(response.text)['token'].encode()
429
430 raise XTMCloudException(response.status_code,
431 response.text.encode('utf-8'),
432 'generating token')
LEFTRIGHT

Powered by Google App Engine
This is Rietveld