LEFT | RIGHT |
(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/' |
| 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') |
LEFT | RIGHT |