| 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 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') | 
| OLD | NEW | 
|---|