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 |
| 19 import json |
| 20 |
| 21 __all__ = [ |
| 22 'XTMCloudAPI', 'XTMCloudException', 'get_token', |
| 23 ] |
| 24 |
| 25 _BASE_URL = 'https://wstest2.xtm-intl.com/rest-api/' |
| 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) |
| 47 |
| 48 |
| 49 class XTMCloudAPI(object): |
| 50 |
| 51 _AUTHORIZATION_TMP = 'XTM-Basic {0}' |
| 52 _URL_PATHS = { |
| 53 'create': 'projects', |
| 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' |
| 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. |
| 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 |
| 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): |
| 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 |