| 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 |