diff --git a/.gitignore b/.gitignore index 05a73a6..b7e674e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.py[cod] /SmartlingApiSdk.egg-info /dist +*.log +openapi3.json \ No newline at end of file diff --git a/INSTALL.txt b/INSTALL.txt index fce8234..fc5c2f4 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -6,31 +6,35 @@ Example. Example script is placed in separate directory named 'example'. It should give you a examples of initializing API and using api calls. -To run example just do: -cd example -python SmartlingApiExample.py - -Test. - -Test uses nose python framework. -Information about installing nose is here http://readthedocs.org/docs/nose/en/latest. -Nose framework is used for test only and is not necessary for file API SDK to be used. - - -To run test do following: -obtain api key and project id from smartling -export api key, project id, locale: - -export SL_API_KEY=********-****-****-****-************ -export SL_PROJECT_ID=********* -export SL_LOCALE=**-** - -cd test -nosetests testFAPI.py - -Correct tests output looks like this: -....... ----------------------------------------------------------------------- -Ran 7 tests in 8.071s - -OK +To run example you should set up smartling credentials for your project like this: + +use environment variables: + export SL_LOCALE=**-** + export SL_USER_IDENTIFIER=****************************** + export SL_USER_SECRET=******************************************************* + + #optional + export SL_ACCOUNT_UID=******* #required only to list projects api call + export SL_PROJECT_ID=******* #required for api calls `projects` and `project_details` + +or for Windows users set credential explicitly in class smartlingApiSdk/Credentials.py + +After credentials are set try our examples, each API has own example covering almost every API call. + +You can start with Strings API example: + +from smartlingApiSdk.example.StringsExample import example +example() + +or try other examples: + +from smartlingApiSdk.example.FilesSimpleExample import example +from smartlingApiSdk.example.FilesExample import example +from smartlingApiSdk.example.AccountProjectsExample import example +from smartlingApiSdk.example.ContextExample import example +from smartlingApiSdk.example.EstimatesExample import example +from smartlingApiSdk.example.JobsExample import example +from smartlingApiSdk.example.JobBatchesV2Example import example +from smartlingApiSdk.example.TagsExample import example +from smartlingApiSdk.example.MultipleAccountsExample import example +from smartlingApiSdk.example.MultipleProjectsExample import example diff --git a/MANIFEST.in b/MANIFEST.in index 64a8503..1847c80 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ -graft resources \ No newline at end of file +graft resources +global-exclude *.py[cod] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d02fac --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +[Smartling File Translation API](https://api-reference.smartling.com/) +================= + +This repository contains the Python SDK for accessing the Smartling Translation API. + +Smartling is cloud-based software and translation services solution prioritizes process automation and intelligent collaboration. +The Smartling File Translation API allows developers to seamlessly internationalize their website +or application by automating the translation and integration of content. +Developers can upload resource files and download the translated file(s) in a locale of their choosing. +There are options allowing to use professional translation, community translation or machine translation. + +For a full description of the Smartling File Translation API, please read File API section of the docs at: https://api-reference.smartling.com/ +This SDK covers several of Smartling APIs: +Jobs +Job Batches V2 +Strings +Context +Estimates +Account & Projects +Files +Tags + +Quick start +----------- + +Clone the repo: `git clone git@github.com:Smartling/api-sdk-python.git`. +You may start with examples in smartlingApiSdk/example directory. +Set your credentials as described in smartlingApiSdk/Credentials.py file. + +There are quite extensive examples, each SDK has own examples covering almost every API call. + +You can start with Strings API example: + +from smartlingApiSdk.example.StringsExample import example +example() + +or try other examples: + +from smartlingApiSdk.example.FilesSimpleExample import example +from smartlingApiSdk.example.FilesExample import example +from smartlingApiSdk.example.AccountProjectsExample import example +from smartlingApiSdk.example.ContextExample import example +from smartlingApiSdk.example.EstimatesExample import example +from smartlingApiSdk.example.JobsExample import example +from smartlingApiSdk.example.JobBatchesV2Example import example +from smartlingApiSdk.example.TagsExample import example +from smartlingApiSdk.example.MultipleAccountsExample import example +from smartlingApiSdk.example.MultipleProjectsExample import example + +Tests +----- + +pip install nose +cd tests +nosetests + +Versioning +---------- + +For transparency and insight into our release cycle, and for striving to maintain backward compatibility, the File Translation API SDK will be maintained under the Semantic Versioning guidelines as much as possible. + +Releases will be numbered with the follow format: + +`..` + +And constructed with the following guidelines: + +* Breaking backward compatibility bumps the major +* New additions without breaking backward compatibility bumps the minor +* Bug fixes and misc changes bump the patch + +For more information on SemVer, please visit http://semver.org/. + + +Bug tracker +----------- + +Have a bug? Please create an issue here on GitHub! + +https://github.com/Smartling/api-sdk-python/issues + + +Authors +------- + +Anatoly Artamonov +* https://github.com/anatolija +* aartamonov@smartling.com + +Alex Koval +* https://github.com/junky +* akoval@smartling.com + +Greg Jones +* http://github.com/jones-smartling +* gjones@smartling.com + + +Copyright and license +--------------------- + +Copyright 2012-2025 Smartling, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this work except in compliance with the License. +You may obtain a copy of the License in the LICENSE file, or at: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.txt b/README.txt deleted file mode 100644 index af058fd..0000000 --- a/README.txt +++ /dev/null @@ -1,80 +0,0 @@ -[Smartling Translation API](https://docs.smartling.com/display/docs/Smartling+Translation+API) -================= - -This repository contains the Python sdk for accessing the Smartling Translation API. - -The Smartling Translation API allows developers to seamlessly internationalize their website by automating the translation and integration of their site content. -Developers can upload resource files and download the translated file(s) in a locale of their choosing. There are options to allow for professional translation, community translation and machine translation. - -For a full description of the Smartling Translation API, please read the docs at: https://docs.smartling.com/display/docs/Smartling+Translation+API - - -Quick start ------------ - -Clone the repo, `git clone git@github.com:Smartling/api-sdk-python.git`. -Use examples in ./example directory to see how to work with SDK. -There are 3 examples: -SimpleExample.py - basic API SDK operartions -IntermediateExample.py - a bit more advanced : introduces rename, import and SmartlingDirectives -AdvancedExample.py - calls every API call and test response for a proper value - - -Versioning ----------- - -For transparency and insight into our release cycle, and for striving to maintain backward compatibility, the File Translation API SDK will be maintained under the Semantic Versioning guidelines as much as possible. - -Releases will be numbered with the follow format: - -`..` - -And constructed with the following guidelines: - -* Breaking backward compatibility bumps the major -* New additions without breaking backward compatibility bumps the minor -* Bug fixes and misc changes bump the patch - -For more information on SemVer, please visit http://semver.org/. - - -Bug tracker ------------ - -Have a bug? Please create an issue here on GitHub! - -https://github.com/Smartling/api-sdk-python/issues - - -Authors -------- - -Anatoly Artamonov -* https://github.com/anatolija -* aartamonov@smartling.com - -Alex Koval -* https://github.com/junky -* akoval@smartling.com - -Greg Jones -* http://github.com/jones-smartling -* gjones@smartling.com - - -Copyright and license ---------------------- - -Copyright 2012 Smartling, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this work except in compliance with the License. -You may obtain a copy of the License in the LICENSE file, or at: - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/TROUBLESHOOTING.txt b/TROUBLESHOOTING.txt new file mode 100644 index 0000000..a713b7e --- /dev/null +++ b/TROUBLESHOOTING.txt @@ -0,0 +1,96 @@ +This python SDK is designed to work with Smartling Translation Platform (https://www.smartling.com) +So first you have to be registered to Smartling and have API credentials. +Here's link describing how to do so: +https://help.smartling.com/hc/en-us/articles/1260804661570-Getting-Started-with-the-API + +############################################################### +CredentialsNotSet exception +############################################################### +The most common problem is API credentials are not set properly. +Here is how error stack trace looks like: + +Traceback (most recent call last): + File "", line 1, in + File "/Library/Python/2.7/site-packages/smartlingApiSdk/example/EstimatesExample.py", line 285, in example + t.setUp() + File "/Library/Python/2.7/site-packages/smartlingApiSdk/example/EstimatesExample.py", line 53, in setUp + credentials = Credentials() #Gets your Smartling credentials from environment variables + File "/Library/Python/2.7/site-packages/smartlingApiSdk/Credentials.py", line 73, in __init__ + raise CredentialsNotSet('SL_' + id + suffix, env) +smartlingApiSdk.Credentials.CredentialsNotSet: Missing:SL_PROJECT_ID + don't forget to set real MY_PROJECT_ID, MY_USER_IDENTIFIER, MY_USER_SECRET, MY_LOCALE + in Credentials class + or use environment variables: + export SL_LOCALE=**-** + export SL_USER_IDENTIFIER=****************************** + export SL_USER_SECRET=******************************************************* + + #optional + export SL_ACCOUNT_UID=******* #required only to list projects api call + export SL_PROJECT_ID=******* #required for api calls `projects` and `project_details` + +For *nix based systems the easiest way to set credentials is to export them as environment variables, +same as traceback suggests. + +Another option is to set credentials when calling API constructor: +#----------------------------------- +strings_api = StringsApi(MY_USER_IDENTIFIER, MY_USER_SECRET, MY_PROJECT_ID) +#----------------------------------- + + +############################################################### +Mixed production / staging credentials +############################################################### + +Second possible issue mixing stg / prod credentials. +One can debug API SDK based code on stg and it has own credentials and own +env variable so both stg and prod could be used in same code. + +The traceback above shows different variable names. + +Production: + export SL_USER_IDENTIFIER=****************************** + export SL_USER_SECRET=******************************************************* + +Staging: + export SL_USER_IDENTIFIER_STG=****************************** + export SL_USER_SECRET_STG=******************************************************* + +some examples like StringsExample use stg +others (like EstimatesExample) are set up to use prod +The difference is in adding env='stg' to constructor of Credentials class like: +#----------------------------------- +Credentials(env='stg') +#----------------------------------- + +or API class like: +#----------------------------------- +strings_api = StringsApi(self.MY_USER_IDENTIFIER, self.MY_USER_SECRET, self.MY_PROJECT_ID, proxySettings, env='stg') +#----------------------------------- + + +############################################################### +urllib2.URLError: +############################################################### + +Following error happens when there's no internet connection + + File "/Library/Python/2.7/site-packages/smartlingApiSdk/example/StringsExample.py", line 208, in example + t.checkAddStringsToProject() + File "/Library/Python/2.7/site-packages/smartlingApiSdk/example/StringsExample.py", line 121, in checkAddStringsToProject + res, status = self.strings_api.addStringsToProject(strings=strings, placeholderFormat=placeholderFormat, placeholderFormatCustom=placeholderFormatCustom, namespace=namespace) + File "/Library/Python/2.7/site-packages/smartlingApiSdk/api/StringsApi.py", line 47, in addStringsToProject + response, status = self.commandJson('POST', url, kw) + File "/Library/Python/2.7/site-packages/smartlingApiSdk/FileApiBase.py", line 87, in commandJson + authHeader = self.addAuth(params) + File "/Library/Python/2.7/site-packages/smartlingApiSdk/ApiV2.py", line 47, in addAuth + token = self.authClient.getToken() + File "/Library/Python/2.7/site-packages/smartlingApiSdk/AuthClient.py", line 69, in getToken + self.authenticate() + File "/Library/Python/2.7/site-packages/smartlingApiSdk/AuthClient.py", line 61, in authenticate + self.request(self.authUri, body) + File "/Library/Python/2.7/site-packages/smartlingApiSdk/AuthClient.py", line 43, in request + ReqMethod.POST, uri, params={}, extraHeaders=header, requestBody=body) + File "/Library/Python/2.7/site-packages/smartlingApiSdk/HttpClient.py", line 87, in getHttpResponseAndStatus + raise e +urllib2.URLError: \ No newline at end of file diff --git a/builder/ApiBuilder.py b/builder/ApiBuilder.py new file mode 100644 index 0000000..0ff3f54 --- /dev/null +++ b/builder/ApiBuilder.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +""" Copyright 2012-2025 Smartling, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this work except in compliance with the License. + * You may obtain a copy of the License in the LICENSE file, or at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limit + """ + +import os +import sys + +sys.path.append(os.path.abspath('../')) +isPython3 = sys.version_info[:2] >= (3, 0) + +import json +import collections +from builder.ApiSource import ApiSource +from smartlingApiSdk.HttpClient import HttpClient +from smartlingApiSdk.Logger import Logger +from smartlingApiSdk.Settings import Settings + + +class ApiBuilder: + """ + builds api based in openapi description for specific Api family defined as full_name + """ + + def __init__(self, fullName): + self.api_name = fullName.replace(' ', '').replace('&', '') + json_dict = self.getApiJson() + self.apisrc = ApiSource(fullName, self.api_name) + self.apisrc.collectMethods(json_dict) + + def getApiJson(self): + httpLoader = HttpClient('api-reference.smartling.com') + responseData, statusCode, headers = httpLoader.getHttpResponseAndStatus('GET', '/swagger.json', {}) + if 200 != statusCode: + raise Exception('Can not load openapi description') + if isPython3: + responseData = responseData.decode('utf8') + open("openapi3.json", 'w').write(responseData) + jsonString = responseData + return json.loads(jsonString, object_pairs_hook=collections.OrderedDict) + + def build(self): + built = self.apisrc.build() + outPath = '../smartlingApiSdk/api/%sApi.py' % self.api_name + open(outPath, 'w').write(built) + print (built) + return self # Allow tagged calls : build().buildExample().buildTest() + + def buildExample(self): + built = self.apisrc.buildExample() + + outPath = '../smartlingApiSdk/example/%sExample.py' % self.api_name + open(outPath, 'w').write(built) + print (built) + return self # Allow tagged calls : build().buildExample().buildTest() + + def buildTest(self): + built = self.apisrc.buildTest() + + outPath = '../test/test%s.py' % self.api_name + open(outPath, 'w').write(built) + print (built) + return self # Allow tagged calls : build().buildExample().buildTest() + + +def main(): + sys.stdout = Logger('python-sdk', Settings.logLevel) + ApiBuilder("Jobs").build().buildExample().buildTest() + #ApiBuilder("Job Batches V2").build().buildExample().buildTest() - uses yaml format now + ApiBuilder("Strings").build().buildExample().buildTest() + ApiBuilder("Context").build().buildExample().buildTest() + ApiBuilder("Estimates").build().buildExample().buildTest() + ApiBuilder("Account & Projects").build().buildExample().buildTest() + ApiBuilder("Files").build().buildExample().buildTest() + ApiBuilder("Tags").build().buildExample().buildTest() + +if __name__ == '__main__': + main() diff --git a/builder/ApiSource.py b/builder/ApiSource.py new file mode 100644 index 0000000..c176350 --- /dev/null +++ b/builder/ApiSource.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +""" Copyright 2012-2025 Smartling, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this work except in compliance with the License. + * You may obtain a copy of the License in the LICENSE file, or at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limit + """ + +import importlib +from builder.Parameters import MultipartProperty +from builder.Method import Method +from builder.ExampleData import EXAMPLE_HEADER, EXAMPLE_FOOTER, TESTS_FOOTER, COPYRIGHT_HEADER + + +class ApiSource(): + def __init__(self, fullName, apiName): + self.fullName = fullName + self.methods = [] + self.apiName = apiName + self.apiNameUnderscore = fullName.replace(' ', '_').replace('&', '').replace('__', '_').lower() + + def collectMethods(self, swaggerDict): + toBuild = [] + for k, v in swaggerDict['paths'].items(): + for method, descr in v.items(): + if method == '$ref': + continue + if toBuild and descr['operationId'] not in toBuild: # Debug build for specific methods only + continue + if self.fullName in descr['tags']: + m = Method(self.apiName, self.apiNameUnderscore, k, method, descr, swaggerDict) + self.patchMethods(descr, m, swaggerDict) + self.methods.append(m) + + def patchMethods(self, descr, m, swaggerDict): + if descr['operationId'] in ('getAllSourceStringsByProject'): + m.method = 'post' + m.isJson = True + if descr['operationId'] in ('addStringsToProject'): + for p in m.parameters+m.multipartParameters: + if p._name == 'namespace': + p._default = 'smartling.strings-api.default.namespace' + if descr['operationId'] in ('getAllTranslationsByProject'): + m.method = 'post' + m.isJson = True + if m.parameters[0]._name == 'hashcodes': + p = m.parameters[0] + del (m.parameters[0]) + m.parameters.insert(1, p) + + def build(self): + + header = COPYRIGHT_HEADER + ''' + +from smartlingApiSdk.ApiV2 import ApiV2 + +class %sApi(ApiV2): + + def __init__(self, userIdentifier, userSecret, projectId, proxySettings=None, permanentHeaders={}, env='prod'): + ApiV2.__init__(self, userIdentifier, userSecret, projectId, proxySettings, permanentHeaders=permanentHeaders, env=env) +''' % self.apiName + rows = header.split('\n') + for m in self.methods[:]: + if m.deprecated: continue + built = m.build() + if built: + rows.append(built) + rows.append('') + rows.append('') + return '\n'.join(rows) + + def methodByName(self, name): + for m in self.methods: + if m.operationId == name: + return m + raise Exception("Can't find method:"+name) + + def buildExampleHeader(self): + # Do dynamic imports based on apy_name + testDataModule = importlib.import_module('testdata.'+self.apiName+'TestData') + imports = getattr(testDataModule, 'imports', '') + extraInitializations = getattr(testDataModule, 'extraInitializations') + tearDown = getattr(testDataModule, 'tearDown', '') + testsOrder = getattr(testDataModule, 'testsOrder') + testEnvironment = getattr(testDataModule, 'testEnvironment', 'prod') + + hdr = EXAMPLE_HEADER + if 'stg' == testEnvironment: + hdr = hdr.replace('Credentials()', "Credentials('stg')") + hdr = hdr.replace('proxySettings)', "proxySettings, env='stg')") + tearDownMarker = ' print("tearDown", "OK")' + hdr = hdr.replace(tearDownMarker, tearDown+tearDownMarker) + hdr = hdr.replace("{EXTRA_IMPORTS}\n", imports) + hdr += extraInitializations + + return hdr, testsOrder + + def buildTestOrExample(self, footer, indent): + rows = [] + + hdr, testsOrder = self.buildExampleHeader() + + apiNameApi = self.apiName + "Api" + hdr = hdr.replace('{API_NAME}', apiNameApi) + hdr = hdr.replace('{api_name}', self.apiNameUnderscore) + ftr = footer.replace('{API_NAME}', apiNameApi) + + notTestedCalls = [m.operationId for m in self.methods if not m.deprecated] + notTestedCalls.insert(0, "'''") + notTestedCalls.insert(0, '# not covered by tests #') + rows.append(hdr) + testCalls = [] + + for name in testsOrder: + m = self.methodByName(name) + if name in notTestedCalls: # Test may occur twice in tests list + notTestedCalls.remove(name) + + built = m.buildExample() + capitalized = m.operationId[0].capitalize() + m.operationId[1:] + + testCalls.append('t.check%s()' % capitalized) + + if built: + rows.append(built) + rows.append('') + + notTestedCalls.append("'''") + if len(notTestedCalls) > 3: + testCalls += notTestedCalls + + newlineWithIndent = '\n' + ' ' * indent + rows.append(ftr % newlineWithIndent.join(testCalls)) + return '\n'.join(rows) + + def buildExample(self): + return self.buildTestOrExample(EXAMPLE_FOOTER, indent=1) + + def buildTest(self): + return self.buildTestOrExample(TESTS_FOOTER, indent=2) + + diff --git a/builder/ExampleData.py b/builder/ExampleData.py new file mode 100644 index 0000000..d6a841a --- /dev/null +++ b/builder/ExampleData.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +""" Copyright 2012-2025 Smartling, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this work except in compliance with the License. + * You may obtain a copy of the License in the LICENSE file, or at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limit +""" + + +class TestData: + def __init__(self, fields, pre = [], post = [], customTestCheck ='', isApiV2Response = True): + self.fields = fields + self.preCalls = pre + self.postCalls = post + self.customTestCheck = customTestCheck + self.is_apiv2_response = isApiV2Response + +COPYRIGHT_HEADER = ''' +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +""" Copyright 2012-2025 Smartling, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this work except in compliance with the License. + * You may obtain a copy of the License in the LICENSE file, or at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +""" +''' + +EXAMPLE_HEADER= COPYRIGHT_HEADER + """ +import os +import sys +import time, datetime + +sys.path += [os.path.abspath('../'), os.path.abspath('../../')] # allow to import ../smartlingApiSdk.api + +import smartlingApiSdk +from smartlingApiSdk.api.{API_NAME} import {API_NAME} +from smartlingApiSdk.ProxySettings import ProxySettings +from smartlingApiSdk.Credentials import Credentials + +isPython3 = sys.version_info[:2] >= (3,0) +{EXTRA_IMPORTS} + +def assert_equal(a,b, comment=''): + if a != b : + err = "Assertion Failed: '%s' != '%s' %s" % (a,b, comment) + if not isPython3 and type(err) == str: + err = err.decode('utf-8', 'ignore') + raise Exception(repr(err)) + +class test{API_NAME}(object): + + CODE_SUCCESS_TOKEN = 'SUCCESS' + ACCEPTED_TOKEN = 'ACCEPTED' + + def tearDown(self): + print("tearDown", "OK") + + def setUp(self): + credentials = Credentials() #Gets your Smartling credetnials from environment variables + self.MY_USER_IDENTIFIER = credentials.MY_USER_IDENTIFIER + self.MY_USER_SECRET = credentials.MY_USER_SECRET + self.MY_PROJECT_ID = credentials.MY_PROJECT_ID + + #needed for testProjects + self.MY_ACCOUNT_UID = credentials.MY_ACCOUNT_UID + self.MY_LOCALE = credentials.MY_LOCALE + + if self.MY_ACCOUNT_UID == "CHANGE_ME": + print("can't test projects api call, set self.MY_ACCOUNT_UID or export SL_ACCOUNT_UID=*********") + return + + useProxy = False + if useProxy : + proxySettings = ProxySettings("login", "password", "proxy_host", "proxy_port or None") + else: + proxySettings = None + + self.{api_name}_api = {API_NAME}(self.MY_USER_IDENTIFIER, self.MY_USER_SECRET, self.MY_PROJECT_ID, proxySettings) + + print("setUp", "OK", "\\n") +""" + +EXAMPLE_FOOTER = """ +def example(): + t = test{API_NAME}() + t.setUp() + %s + t.tearDown() + +if __name__ == '__main__': + example() +""" + +TESTS_FOOTER = """ + def test_all(self): + t = self + %s +""" diff --git a/builder/Method.py b/builder/Method.py new file mode 100644 index 0000000..06d3504 --- /dev/null +++ b/builder/Method.py @@ -0,0 +1,367 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +""" Copyright 2012-2025 Smartling, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this work except in compliance with the License. + * You may obtain a copy of the License in the LICENSE file, or at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limit +""" + +import json +import importlib +from builder.Parameters import ApiCore, Parameter, MultipartProperty + + +class Method(ApiCore): + indent = ' ' + indent2 = indent*2 + indent3 = indent*3 + indent4 = indent*4 + + def __init__(self, apiName, apiNameUnderscore, path, method, descriptionDict, swaggerDict): + ApiCore.__init__(self, swaggerDict) + self.operationId = '' + self.type = '' + self.multipartParameters = [] + self.requestBody = '' + for name in ['summary', 'description', 'tags', 'operationId', 'responses', 'x-code-samples', 'requestBody', 'deprecated']: + setattr(self, name, descriptionDict.get(name, None)) + self.apiName = apiName + self.apiNameUnderscore = apiNameUnderscore + self.method = method + self.path = path + self.parameters = [] + + for p in descriptionDict['parameters']: + if 'projectId' == p['name']: + continue # We have projectId as api class member + parameter = Parameter(p, swaggerDict) + self.parameters .append(parameter) + + self.needMultipart = False + self.isJson = False + self.hasDirectives = False + self.getMultipartProps() + + def build(self): + rows = [ + self.buildName(), + self.buildDoc() + ] + + if self.needMultipart: + rows.append(self.buildMultipart()) + else: + rows.append(self.buildBody()) + + return '\n'.join(rows) + + def buildExample(self): + rows = [ + self.buildTestName(), + self.buildDoc(), + self.buildTestBody(), + '' + ] + joined = '\n'.join(rows) + joined = joined.replace(self.indent2 + '\n', '\n') # Remove whitespaces for separator line + return joined + + def buildTestName(self): + return ''.join([ + self.indent, + 'def check', + self.operationId[0].capitalize() + self.operationId[1:], + '(self):', + ] + ) + + def buildName(self): + return ''.join([ + self.indent, + 'def ', + self.operationId, + '(self', + self.buildParams(), + ', **kwargs):', + ] + ) + + def buildParams(self): + result = '' + joined = self.parameters + self.multipartParameters + self.rearrangeRequired(joined) + if joined: + result += ", " + ", ".join(x.getParamForName() for x in joined) + if self.hasDirectives: + result += ', directives={}' + return result + + def buildDoc(self): + commentMarker = self.indent2 + '"""' + docLines = [ + commentMarker, + self.indent3 + 'method : ' + self.method.upper(), + self.indent3 + 'api url : ' + self.path, + ] + curl_example = self.getCurlExample() + if curl_example: + docLines.append(curl_example) + + nested = self.listNestedValues() + if nested: + docLines.append(nested) + docLines = self.getResponsesDescription(docLines) + details = self.indent3 + 'details : https://api-reference.smartling.com/#operation/' + self.operationId + docLines.append(details) + docLines.append(commentMarker) + + return '\n'.join(docLines) + + def getResponsesDescription(self, docLines): + result = [] + responses = getattr(self, 'responses', {}) + for code, codeDict in responses.items(): + descr = codeDict.get('description', '') + if descr: + result.append(self.indent2 + '%s : %s' % (code, descr)) + if result: + result.insert(0, self.indent + "Responses:") + responses = self.joinWithIndent(result, self.indent2) + docLines.append(responses) + return docLines + + def getCurlExample(self): + result = [] + samples = getattr(self, 'x-code-samples', []) + if not samples: + return '' + + for d in samples: + result.append(self.indent + 'as curl : ' + d['source'].replace('\n', '')) + return self.joinWithIndent(result, self.indent2) + + def getPropertyDescription(self, prop): + prop_dict = {} + for m in prop.prop_list: + prop_dict[m._name] = getattr(m, '_example', '') or m.getDefault() + + result = [self.indent + 'Parameters example:'] + hdr = self.indent + '%s: ' % prop._name + dumped = json.dumps(prop_dict, indent=16) + dumped = dumped.replace('}', self.indent4+'}') + result.append(hdr + dumped) + return result + + def listNestedValues(self): + result = [] + for prop in self.multipartParameters: + if not getattr(prop, 'prop_list', None): + continue + result += self.getPropertyDescription(prop) + if result: + return self.joinWithIndent(result, self.indent2) + return '' + + def joinWithIndent(self, lst, indent): + newlinePlusIndent = '\n'+indent + return indent + newlinePlusIndent.join(lst) + + def buildPathParamsStr(self): + pathParameters = [x for x in self.parameters if x._in == 'path'] + if not pathParameters: + return '' + pthArgs = ['%s=%s' % (x._name, x._name) for x in pathParameters] + return ', ' + ', '.join(pthArgs) + + def buildBody(self): + bodyLines = [] + + valuesToPass = "kw" + bodyLines.append('kw = {') + + kw_params = [x for x in self.parameters if x._in == 'query'] + for p in kw_params: + bodyLines.append(self.indent + "'%s':%s," % (p._name, p._name)) + for m in self.multipartParameters: + if m.isRequestBody: + valuesToPass = m._name + continue + if 'binary' == m._format: + raise Exception("Incompatible parameter format for command") + bodyLines.append(self.indent + "'%s':%s," % (m._name, m._name)) + bodyLines.append('}') + bodyLines.append('kw.update(kwargs)') + bodyLines.append("url = self.urlHelper.getUrl('%s'%s, **kwargs)" % (self.path, self.buildPathParamsStr())) + cmd = "response, status = self.command('%s', url, %s)" % (self.method.upper(), valuesToPass) + if self.method.upper() in ('POST', 'PUT') and self.isJson: + cmd = "response, status = self.commandJson('%s', url, %s)" % (self.method.upper(), valuesToPass) + bodyLines.append(cmd) + bodyLines.append("return response, status") + + return self.joinWithIndent(bodyLines, self.indent2) + + def buildTestBody(self): + bodyLines = [] + + parameters = [] + initialisers = {} + + testDataModule = importlib.import_module('testdata.'+self.apiName+'TestData') + decorators = getattr(testDataModule, 'testDecorators') + + jobsTestData = None + if self.operationId in decorators.keys(): + jobsTestData = decorators[self.operationId] + initialisers = jobsTestData.fields + for line in jobsTestData.preCalls: + bodyLines.append(line) + for p in self.parameters + self.multipartParameters: + if p._required or p._name in initialisers: + parameters.append(p.getParamForMethodCall()) + + kwParams = [x for x in self.parameters if x._in == 'query' or x._in == 'path'] + for p in kwParams + self.multipartParameters: + if p._required or p._name in initialisers: + bodyLines.append(p.getParamForMethodCall(initialisers)) + + callParams = ', '.join(parameters) + bodyLines.append('res, status = self.%s_api.%s(%s)' % (self.apiNameUnderscore, self.operationId, callParams)) + bodyLines.append('') + + if jobsTestData and jobsTestData.customTestCheck: + bodyLines += jobsTestData.customTestCheck.split('\n') + + if jobsTestData and jobsTestData.is_apiv2_response: + bodyLines.append('assert_equal(True, status in [200,202])') + bodyLines.append('assert_equal(True, res.code in [self.CODE_SUCCESS_TOKEN, self.ACCEPTED_TOKEN])') + bodyLines.append("print('%s', 'OK')" % self.operationId) + if jobsTestData: + for line in jobsTestData.postCalls: + bodyLines.append(line) + + return self.joinWithIndent(bodyLines, self.indent2) + + def listProperty(self, name): + if not name: + raise Exception("Can't determine property name") + mp = MultipartProperty(name, {'type': 'array'}, self.swaggerDict) + mp.setRequired() + mp.isRequestBody = True + self.multipartParameters.insert(0, mp) + + def getMultipartProps(self): + self.multipartParameters = [] + if not self.requestBody: + return + self.resolveRequestBodyRef() + self.type = list(self.requestBody['content'].keys())[0] + if 'application/json' == self.type: + self.isJson = True + + schema = self.requestBody['content'][self.type]['schema'] + + refname = '' + if '$ref' == list(schema.keys())[0]: + schema, refname = self.resolveRef(schema['$ref']) + + props = schema.get('properties', None) + if props is None: + if 'array' == schema['type']: + self.listProperty(refname) + return + else: + print (schema) + + self.multipartParameters = self.parseProperties(props) + + for req in schema.get('required', []): + for mp in self.multipartParameters: + if req == mp._name: + mp.setRequired() + + self.rearrangeRequired(self.multipartParameters) + + def resolveRequestBodyRef(self): + for key in self.requestBody: + if '$ref' == key: + resolved, refname = self.resolveRef(self.requestBody['$ref']) + self.requestBody = resolved + return + + def rearrangeRequired(self, params): + needRearrangeIndex = [] + hasOptional = False + positionToInsert = 0 + for p in params: + if not p._required: + hasOptional = True + if not hasOptional: + positionToInsert = params.index(p) + continue + if hasOptional: + needRearrangeIndex.append(params.index(p)) + for idx in needRearrangeIndex: + p = params[idx] + del params[idx] + params.insert(positionToInsert, p) + positionToInsert += 1 + + def parseProperties(self, props): + propList = [] + for k in props.keys(): + propDict = props[k] + if k.startswith('smartling.'): + self.hasDirectives = True + continue + + mp = MultipartProperty(k, propDict, self.swaggerDict) + propList.append(mp) + + if 'application/json' == self.type: + if mp._description and 'required' in mp._description: + mp.setRequired() + + if propDict.get('properties', None): + mp.prop_list = self.parseProperties(propDict['properties']) + + if mp._format == 'binary': + self.needMultipart = True + return propList + + def buildMultipart(self): + result = [ + 'kw = {', + ] + kw_params = [x for x in self.parameters if x._in == 'query'] + for p in kw_params: + result.append(self.indent + "'%s':%s," % (p._name, p._name)) + for m in self.multipartParameters: + if 'binary' == m._format: + result.append(self.indent + "'%s':self.processFile(%s)," % (m._name, m._name)) + elif not 'directives' == m._name: + result.append(self.indent + "'%s':%s," % (m._name, m._name)) + result.append('}') + + result += self.addDirectives() + result.append("url = self.urlHelper.getUrl('%s'%s)" % (self.path, self.buildPathParamsStr())) + result.append("return self.uploadMultipart(url, kw)") + + return self.joinWithIndent(result, self.indent2) + + def addDirectives(self): + if self.hasDirectives: + return ["self.addLibIdDirective(kw)", + 'self.processDirectives(kw, directives)'] + else: + return [] diff --git a/builder/Parameters.py b/builder/Parameters.py new file mode 100644 index 0000000..f840930 --- /dev/null +++ b/builder/Parameters.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +""" Copyright 2012-2025 Smartling, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this work except in compliance with the License. + * You may obtain a copy of the License in the LICENSE file, or at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limit + """ + + +class Code: + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + + +class ApiCore: + def __init__(self, swaggerDict): + self.swaggerDict = swaggerDict + + def resolveRef(self, ref): + if not ref.startswith('#/'): + raise Exception('Unknown $ref:%s' % ref) + pth = ref[2:].split('/') + dct = self.swaggerDict + lastname = '' + for p in pth: + dct = dct[p] + lastname = p + return dct, lastname + + +class Parameter(ApiCore): + def __init__(self, param_dict, swaggerDict): + ApiCore.__init__(self, swaggerDict) + self._required = False + self._name = "" + param_names = ['name', 'description', 'in', 'required', 'schema'] + self.processParams(param_names, param_dict) + self._type = "string" + + if self._schema: + if '$ref' == list(self._schema.keys())[0]: + self._schema, refname = self.resolveRef(self._schema['$ref']) + self._type = self._schema['type'] + self._default = self._schema.get('default', None) + + def processParams(self, param_names, param_dict): + for name in param_names: + n = param_dict.get(name, None) + if 'name' == name: + n = n.replace('[]', '') + setattr(self, '_' + name, n) + + def getParamForName(self): + if self._required: + return self._name + return "%s=%s" % (self._name, self.getDefault()) + + def getParamForMethodCall(self, values={}): + value = values.get(self._name, self._name) + if type(value) == type(Code): + pass + elif value != self._name and type(value) == str: + value = "'" + value + "'" + if 'accountUid' == self._name: + value = 'self.MY_ACCOUNT_UID' + return "%s=%s" % (self._name, value) + + def getDefault(self): + default = getattr(self, '_default', None) + if default is None: + default = "''" + if "string" == self._type and default != "''": + default = "'" + default + "'" + if 'array' == self._type: + return '[]' + if 'integer' == self._type and default == "''": + return '0' + return default + + +class MultipartProperty(Parameter): + def __init__(self, name, paramDict, swaggerDict): + self._format = None + self._type = None + param_names = ['description', 'format', 'type', 'default', 'example'] + self.processParams(param_names, paramDict) + self._required = False + self._name = name.replace('[]', '').split(' ')[0] + self.isRequestBody = False + self.swaggerDict = swaggerDict + + def setRequired(self): + self._required = True + + def __str__(self): + if self._format: + return "%s : %s-%s" % (self._name, self._type, self._format) + return "%s : %s" % (self._name, self._type) + diff --git a/example/__init__.py b/builder/__init__.py similarity index 100% rename from example/__init__.py rename to builder/__init__.py diff --git a/changes.txt b/changes.txt index 95335e1..80d7063 100644 --- a/changes.txt +++ b/changes.txt @@ -1,13 +1,88 @@ -May 20, 2013 - v 2.0.4 +Sep 2, 2025 - 3.1.9 + - FileApiBase.commandJson supports ignore_errors flag, fix by @kenvac + +Sep 2, 2025 - 3.1.8 + - fixed failing FileTest + +May 29, 2025 - 3.1.7 + - removed deprecated /file/authorized-locales endpoint + - minor tests fix, copyright 2012-2025 updated + +Aug 29, 2024 - 3.1.6 + - updated according the latest changes in Smartling SDK + +Dec 09, 2022 - 3.1.5 + - fix of ApiResponse get attribute may cause stack overflow on requesting non-existng attribute + +May 24, 2022 - 3.1.4 + - old tests updated to handle 423 errors for FilesAPI + +May 24, 2022 - 3.1.3 + - Context API changes updated + +Feb 22, 2022 - 3.1.2 + - Timeout fix for requests with body + +Feb 15, 2022 - 3.1.1 + - ContextApi updates + +Jan 19, 2022 - 3.1.0 + - optional context for http request to bypass certificate check + - allow empty namespace in Strings API + - logged X-SL-RequestId header for non-200 responses + - added response headers as attribute of ApiResponse object for possible further use + - upload string instead of file - added example to testJobBatchesV2Api.checkUploadFileToJobBatchV2 + - fixed logging + changed logging to log to current directory + - fixed refesh timestamp check + - user-agent could be customized in Settings.userAgent + - logs path could be customized in Settings.logPath + - Credentials.LOCALE is optional + +Dec 15, 2021 - 3.0.3 + - json request body parameters can be optional + - fixed download json file error + +Oct 13, 2021 - 3.0.2 + - reorganised pip package to store all modules, examples, resources within smartlingApiSdk directory + - added TROUBLESHOOTING.txt mini-guide + +Oct 11, 2021 - 3.0.1 + - renamed api directory into less generic smartlingApi + - this file is updated + +Oct 11, 2021 - 3.0.0 + - major release with multiple APIs added + - APIs + examples are generated from swagger file + +Feb 03, 2020 - 2.2.3 + - fix in ApiClient + - HttpClient support for python3 versions < 3.4.3 + +June 20, 2017 - 2.2.2 + - fixed auto version create for pypi upload + +June 20, 2017 - 2.2.1 + - added directive client_lib_id to upload api call parameters + +June 13, 2017 - 2.2.0 + - python3 support is added + +Dec 21, 2016 - v 2.1.1 + - minor typo fixes + +Dec 21, 2016 - v 2.1.0 + - Projects API is separated from File Api into own module : SmartlingProjectsApiV2.py + +May 20, 2016 - v 2.0.4 - Minor bugs fixed -May 16, 2013 - v 2.0.3 +May 16, 2016 - v 2.0.3 - Credentials default values fix -May 16, 2013 - v 2.0.2 +May 16, 2016 - v 2.0.2 - Added more examples + bugfix -May 13, 2013 - v 2.0.1 +May 13, 2016 - v 2.0.1 - Added APIv2 support http://docs.smartling.com/pages/API/v2 Dec 24, 2015 - v 1.2.9 @@ -23,7 +98,7 @@ Apr 27, 2015 - v 1.2.6 - reworked proxy support Dec 25, 2014 - v 1.2.5 - - added encoding definition to all py files and changes for publishig sdk to pypi https://pypi.python.org/pypi/SmartlingApiSdk + - added encoding definition to all py files and changes for publishing sdk to pypi https://pypi.python.org/pypi/SmartlingApiSdk Dec 11, 2014 - v 1.2.4 - added optional support for proxy with basic authentication @@ -45,8 +120,8 @@ Jun 05, 2014 - v 1.2 - added import command support - added last_modified command support - added json parsing - - imporved documentation - - added http error handings for http post + - improved documentation + - added http error handlings for http post Feb 11, 2014 - v 1.1.6 - remove trailing whitespace diff --git a/example/AdvancedExample.py b/example/AdvancedExample.py deleted file mode 100644 index f611fad..0000000 --- a/example/AdvancedExample.py +++ /dev/null @@ -1,378 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - - -''' Copyright 2012 Smartling, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this work except in compliance with the License. - * You may obtain a copy of the License in the LICENSE file, or at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -''' - -import os -import sys -import time -import zipfile -import StringIO -from datetime import date - -lib_path = os.path.abspath('../') -sys.path.append(lib_path) # allow to import ../smartlingApiSdk/SmartlingFileApi - -from smartlingApiSdk.SmartlingFileApiV2 import SmartlingFileApiV2 -from smartlingApiSdk.ProxySettings import ProxySettings -from smartlingApiSdk.version import version -from smartlingApiSdk.Credentials import Credentials -from smartlingApiSdk.Constants import FileTypes - -def assert_equal(a,b): - if a != b : - err = "Assertion Failed: '%s' != '%s'" % (a,b) - if type(err) == unicode: - err = err.decode('utf-8', 'ignore') - raise `err` - -class testFapiV2(object): - - FILE_NAME = "java.properties" - FILE_NAME_16 = "javaUTF16.properties" - FILE_TYPE = "javaProperties" - FILE_TYPE_CSV = "csv" - FILE_PATH = "../resources/" - FILE_NAME_NEW = "java.properties.renamed" - FILE_NAME_NEW_16 = "javaUTF16.properties.renamed" - FILE_NAME_CSV = "test.csv" - - FILE_NAME_IMPORT_ORIG = "test_import.xml" - FILE_NAME_IMPORT_TRANSLATED = "test_import_es.xml" - FILE_TYPE_IMPORT = "android" - - CALLBACK_URL = "http://google.com/?q=hello" - - CODE_SUCCESS_TOKEN = 'SUCCESS' - - - def setUp(self): - credentials = Credentials() #Gets your Smartling credetnials from environment variables - self.MY_USER_IDENTIFIER = credentials.MY_USER_IDENTIFIER - self.MY_USER_SECRET = credentials.MY_USER_SECRET - self.MY_PROJECT_ID = credentials.MY_PROJECT_ID - self.MY_LOCALE = credentials.MY_LOCALE - - #needed for testProjects - self.MY_ACCOUNT_UID = credentials.MY_ACCOUNT_UID - - useProxy = False - if useProxy : - proxySettings = ProxySettings("login", "password", "proxy_host", "proxy_port or None") - else: - proxySettings = None - self.fapi = SmartlingFileApiV2(self.MY_USER_IDENTIFIER, self.MY_USER_SECRET, self.MY_PROJECT_ID, proxySettings) - unique_suffix = "_" + version + "_" + `time.time()` - self.uri = self.FILE_NAME + unique_suffix - self.doUpload(self.FILE_NAME, self.uri, self.FILE_TYPE) - - self.uri_csv = self.FILE_NAME_CSV + unique_suffix - - self.uri16 = self.FILE_NAME_16 + unique_suffix - self.doUpload(self.FILE_NAME_16, self.uri16, self.FILE_TYPE) - - self.uri_to_rename = self.FILE_NAME_NEW + unique_suffix - self.uri_import = self.FILE_NAME_IMPORT_ORIG + unique_suffix - - print "setUp", "OK", "\n\n\n" - - - def tearDown(self): - res, status = self.fapi.delete(self.uri) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - res, status = self.fapi.delete(self.uri16) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - print "tearDown", "OK" - - def doUpload(self, name, uri, type): - #ensure file is uploaded which is necessary for all tests - uniqueUriForUploadTestFile = uri - localeIdsToAuthorize = [self.MY_LOCALE] - res, status = self.fapi.upload(self.FILE_PATH + name, type, fileUri = uniqueUriForUploadTestFile, localeIdsToAuthorize = localeIdsToAuthorize ) - - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - print status, res - return res, status - - def testFileList(self): - res, status = self.fapi.list(fileTypes=[FileTypes.android, FileTypes.javaProperties]) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - uris = map(lambda x:x['fileUri'], res.data.items) - - assert_equal(True, self.uri in uris) - assert_equal(True, self.uri16 in uris) - - print "testFileList", "OK" - - def testFileListTypes(self): - res, status = self.fapi.list_file_types() - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - print "testFileListTypes", "OK" - - def testGet(self): - res, status = self.fapi.get(self.uri, self.MY_LOCALE) - assert_equal(200, status) - - resp_lines_count = len(res.split("\n")) - file_lines_count = len( open(self.FILE_PATH + self.FILE_NAME, "rb").readlines() ) - assert_equal(resp_lines_count, file_lines_count) - - print "testGet", "OK" - - res, status = self.fapi.get(self.uri16, self.MY_LOCALE) - assert_equal(200, status) - - resp_lines_count = len(res.split("\n")) - file_lines_count = len( open(self.FILE_PATH + self.FILE_NAME_16, "rb").readlines() ) - assert_equal(resp_lines_count, file_lines_count) - print "testGet Utf16", "OK" - - - def testGetMultipleLocalesAsZip(self): - res, status = self.fapi.get_multiple_locales([self.uri,self.uri16], [self.MY_LOCALE]) - assert_equal(200, status) - - zfile = zipfile.ZipFile(StringIO.StringIO(res)) - names = zfile.namelist() - - assert_equal(True, self.MY_LOCALE+"/"+self.uri in names) - assert_equal(True, self.MY_LOCALE+"/"+self.uri16 in names) - - print "testGetMultipleLocalesAsZip", "OK" - - - def testGetAllLocalesZip(self): - res, status = self.fapi.get_all_locales(self.uri) - assert_equal(200, status) - - zfile = zipfile.ZipFile(StringIO.StringIO(res)) - names = zfile.namelist() - - assert_equal(True, self.MY_LOCALE+"/"+self.uri in names) - - print "testGetAllLocalesZip", "OK" - - - def testGetOriginal(self): - res, status = self.fapi.get_original(self.uri) - assert_equal(200, status) - - orig = open(self.FILE_PATH + self.FILE_NAME, "r").read() - assert_equal(res, orig) - - print "testGetOriginal", "OK" - - res, status = self.fapi.get_original(self.uri16) - assert_equal(200, status) - - resp_lines_count = len(res.split("\n")) - file_lines_count = len( open(self.FILE_PATH + self.FILE_NAME_16, "rb").readlines() ) - assert_equal(resp_lines_count, file_lines_count) - print "testGetOriginal Utf16", "OK" - - - def testGetAllLocalesCsv(self): - self.doUpload(self.FILE_NAME_CSV, self.uri_csv, self.FILE_TYPE_CSV) - res, status = self.fapi.get_all_locales_csv(self.uri_csv) - assert_equal(200, status) - - res, status = self.fapi.delete(self.uri_csv) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - print "testGetAllLocalesCsv", "OK" - - def testProjects(self): - if self.MY_ACCOUNT_UID == "CHANGE_ME": - print "can't test projects api call, set self.MY_ACCOUNT_UID or export SL_ACCOUNT_UID=*********" - return - res, status = self.fapi.projects(self.MY_ACCOUNT_UID) - - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - projects = map(lambda x:x['projectId'], res.data.items) - - assert_equal(True, self.MY_PROJECT_ID in projects) - print "testProjects", "OK" - - def testProjectDetails(self): - res, status = self.fapi.project_details() - - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - assert_equal(self.MY_PROJECT_ID, res.data.projectId) - - print "testProjectDetails", "OK" - - def testStatus(self): - res, status = self.fapi.status(self.uri) - - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - assert_equal(res.data.fileUri, self.uri) - assert_equal(True, len(res.data.items) > 0) - - print "testStatus", "OK" - - - def testStatusLocale(self): - res, status = self.fapi.status_locale(self.uri, self.MY_LOCALE) - - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - assert_equal(res.data.fileUri, self.uri) - assert_equal(res.data.fileType, self.FILE_TYPE) - - print "testStatusLocale", "OK" - - def testFileRename(self): - - res, status = self.fapi.rename(self.uri, self.uri_to_rename) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - #rename it back so tearDown could remove it - res, status = self.fapi.rename(self.uri_to_rename, self.uri) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - print "testStatusLocale", "OK" - - def testLastModified(self): - resp, status = self.fapi.last_modified(self.uri, self.MY_LOCALE) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, resp.code) - - lm_date = resp.data.lastModified[:10] - assert_equal(lm_date, date.today().isoformat()) - - resp, status = self.fapi.last_modified(self.uri16, self.MY_LOCALE) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, resp.code) - - lm_date = resp.data.lastModified[:10] - assert_equal(lm_date, date.today().isoformat()) - - print "testLastModified", "OK" - - def testLastModifiedAll(self): - resp, status = self.fapi.last_modified_all(self.uri) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, resp.code) - assert_equal(True, resp.data.items > 0) - - resp, status = self.fapi.last_modified_all(self.uri16) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, resp.code) - assert_equal(True, resp.data.items > 0) - - print "testLastModifiedAll", "OK" - - def testImport(self): - res, status = self.fapi.upload(self.FILE_PATH + self.FILE_NAME_IMPORT_ORIG, self.FILE_TYPE_IMPORT , fileUri=self.uri_import) - - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - originalPath = self.FILE_PATH + self.FILE_NAME_IMPORT_ORIG - translatedPath = self.FILE_PATH + self.FILE_NAME_IMPORT_TRANSLATED - - resp, status = self.fapi.import_call(originalPath, translatedPath, - self.FILE_TYPE_IMPORT, self.MY_LOCALE, - fileUri=self.uri_import, translationState="PUBLISHED") - - assert_equal(resp.code, self.CODE_SUCCESS_TOKEN) - assert_equal(resp.data.wordCount, 2) - assert_equal(resp.data.stringCount, 2) - assert_equal(resp.data.translationImportErrors, []) - - res, status = self.fapi.delete(self.uri_import) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - print "testImport", "OK" - - def testListAuthorizedLocales(self): - resp, status = self.fapi.last_modified_all(self.uri) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, resp.code) - assert_equal(True, resp.data.items > 0) - - locales = map(lambda x:x['localeId'], resp.data.items) - assert_equal(True, self.MY_LOCALE in locales) - print "testListAuthorizedLocales", "OK" - - - def testAuthorize(self): - res, status = self.fapi.authorize(self.uri, [self.MY_LOCALE, 'uk-UA']) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - print "testAuthorize", "OK" - - def testUnauthorize(self): - res, status = self.fapi.unauthorize(self.uri, [self.MY_LOCALE, 'uk-UA']) - assert_equal(200, status) - assert_equal(self.CODE_SUCCESS_TOKEN, res.code) - - print "testUnauthorize", "OK" - - - def testGetTranslations(self): - res, status = self.fapi.get_translations(self.uri, self.FILE_PATH+self.FILE_NAME, (self.MY_LOCALE)) - - assert_equal(200, status) - - resp_lines_count = len(res.split("\n")) - file_lines_count = len( open(self.FILE_PATH + self.FILE_NAME, "rb").readlines() ) - assert_equal(resp_lines_count, file_lines_count) - - print "testUnauthorize", "OK" - - -t = testFapiV2() -t.setUp() - -t.testStatusLocale() - -t.testGetTranslations() -t.testGetOriginal() -t.testGetAllLocalesZip() -t.testGetAllLocalesCsv() -t.testGetMultipleLocalesAsZip() -t.testFileListTypes() -t.testGet() -t.testFileList() -t.testProjects() -t.testProjectDetails() -t.testStatusLocale() -t.testStatus() -t.testFileRename() -t.testLastModified() -t.testLastModifiedAll() -t.testImport() -t.testListAuthorizedLocales() -t.testUnauthorize() -t.testAuthorize() - -t.tearDown() \ No newline at end of file diff --git a/example/IntermediateExample.py b/example/IntermediateExample.py deleted file mode 100644 index 1e1bfac..0000000 --- a/example/IntermediateExample.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - - -''' Copyright 2012 Smartling, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this work except in compliance with the License. - * You may obtain a copy of the License in the LICENSE file, or at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -''' - -import os -import sys -lib_path = os.path.abspath('../') -sys.path.append(lib_path) # allow to import ../smartlingApiSdk/SmartlingFileApi - -from smartlingApiSdk.SmartlingFileApiV2 import SmartlingFileApiV2 -from smartlingApiSdk.ProxySettings import ProxySettings -from smartlingApiSdk.SmartlingDirective import SmartlingDirective -from smartlingApiSdk.Credentials import Credentials - -class SmartlingApiExample: - - def __init__(self, file_name, file_type, new_name): - credentials = Credentials() #Gets your Smartling credetnials from environment variables - - self.MY_USER_IDENTIFIER = credentials.MY_USER_IDENTIFIER - self.MY_USER_SECRET = credentials.MY_USER_SECRET - self.MY_PROJECT_ID = credentials.MY_PROJECT_ID - self.MY_LOCALE = credentials.MY_LOCALE - - useProxy = False - if useProxy : - proxySettings = ProxySettings("login", "password", "proxy_host", "proxy_port") - else: - proxySettings = None - - self.fapi = SmartlingFileApiV2( self.MY_USER_IDENTIFIER, self.MY_USER_SECRET, self.MY_PROJECT_ID, proxySettings) - self.file_type = file_type - self.file_name = file_name - self.new_name = new_name - - def printMarker(self, caption): - print "--" + caption + "-" * 40 - - def test_import(self, name_to_import): - """ this method tests `import` command """ - self.printMarker("file upload") - #upload file first to be able upload it's translations later - - path = FILE_PATH + self.file_name - resp, code = self.fapi.upload(path, self.file_type) - if 200!=code: - raise "failed" - - print resp, code - - self.printMarker("files list") - #list all files to ensure upload worked - resp, code = self.fapi.list() - print resp, code - - self.printMarker("importing uploaded") - #set correct uri/name for file to be imported - path_to_import = FILE_PATH + name_to_import - - #import translations from file - resp, code = self.fapi.import_call(path, path_to_import, - self.file_type, self.MY_LOCALE, - translationState="PUBLISHED") - print resp, code - - if 200!=code: - raise "failed" - - #perform `last_modified` command - self.printMarker("last modified") - resp, code = self.fapi.last_modified(path, self.MY_LOCALE) - print "resp.code=", resp.code - print "resp.data", resp.data - - self.printMarker("delete from server goes here") - #delete test file imported in the beginning of test - resp, code = self.fapi.delete(path) - print resp, code - - def test(self): - """ simple illustration for set of API commands: upload, list, status, get, rename, delete """ - self.printMarker("file upload") - path = FILE_PATH + self.file_name - directives={"placeholder_format_custom" : "\[.+?\]"} - resp, code = self.fapi.upload(path, self.file_type, authorize="true", callbackUrl=CALLBACK_URL, directives=directives) - print resp, code - if 200!=code: - raise "failed" - - self.printMarker("files list") - resp, code = self.fapi.list() - print resp, code - - self.printMarker("file status") - resp, code = self.fapi.status(path) - print resp, code - - self.printMarker("file from server goes here") - resp, code = self.fapi.get(path, self.MY_LOCALE) - print resp, code - - self.printMarker("renaming file") - resp, code = self.fapi.rename(path, self.new_name) - print resp, code - - self.printMarker("delete from server goes here") - resp, code = self.fapi.delete(self.new_name) - print resp, code - - self.printMarker("doing list again to see if it's deleted") - resp, code = self.fapi.list() - print resp, code - - -FILE_NAME = "java.properties" -FILE_NAME_UTF16 = "javaUTF16.properties" -FILE_TYPE = "javaProperties" -FILE_PATH = "../resources/" -FILE_NAME_RENAMED = "java.properties.renamed" -CALLBACK_URL = "http://yourdomain.com/callback" - - -FILE_NAME_IMPORT = "test_import.xml" -FILE_NAME_TO_IMPORT = "test_import_es.xml" -FILE_TYPE_IMPORT ="android" - -def upload_test(): - #test simple file - example = SmartlingApiExample(FILE_NAME, FILE_TYPE, FILE_NAME_RENAMED) - example.test() - -def import_test(): - #example for import and last_modified commands - example = SmartlingApiExample(FILE_NAME_IMPORT, FILE_TYPE_IMPORT, FILE_NAME_RENAMED) - example.test_import(FILE_NAME_TO_IMPORT) - -upload_test() -import_test() diff --git a/example/SimpleExample.py b/example/SimpleExample.py deleted file mode 100644 index 94b7020..0000000 --- a/example/SimpleExample.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - - -''' Copyright 2012 Smartling, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this work except in compliance with the License. - * You may obtain a copy of the License in the LICENSE file, or at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -''' - -import os -import sys -lib_path = os.path.abspath('../') -sys.path.append(lib_path) # allow to import ../smartlingApiSdk/SmartlingFileApi - -from smartlingApiSdk.SmartlingFileApiV2 import SmartlingFileApiV2 -from smartlingApiSdk.ProxySettings import ProxySettings -from smartlingApiSdk.Credentials import Credentials - -#File attributes to upload to Smartling server -FILE_TYPE = "javaProperties" -FILE_NAME = "java.properties" -FILE_PATH = "../resources/" - -#set Smartling creadentials via helper Credentials class -credentials = Credentials() #Gets your Smartling credetnials from environment variables -MY_USER_IDENTIFIER = credentials.MY_USER_IDENTIFIER -MY_USER_SECRET = credentials.MY_USER_SECRET -MY_PROJECT_ID = credentials.MY_PROJECT_ID -MY_LOCALE = credentials.MY_LOCALE - -#set proxy settigns if necessary -useProxy = False -if useProxy : - proxySettings = ProxySettings("login", "password", "proxy_host", "proxy_port") -else: - proxySettings = None - -#initializa api object -fapi = SmartlingFileApiV2( MY_USER_IDENTIFIER, MY_USER_SECRET, MY_PROJECT_ID, proxySettings) - -#Upload file to Smartling -print "\nUploading ..." -path = FILE_PATH + FILE_NAME -resp, code = fapi.upload(path, FILE_TYPE, authorize=True) -print resp, code -if 200!=code: - raise "failed" - -#List uploaded files -print "\nList ..." -resp, code = fapi.list() -print "items size= ", len(resp.data.items) -print code, resp - - -#check file status -print "\nFile status ..." -resp, code = fapi.status(path) -print code, resp -print resp.data.fileUri -print "items size=", len(resp.data.items) - -#read uplaoded file -print "\nRead file from server ..." -resp, code = fapi.get(path, MY_LOCALE) -print resp, code - -#delete file -print "\nDelete file ..." -resp, code = fapi.delete(path) -print resp, code - - - - diff --git a/setup.py b/setup.py index 774096b..13a8839 100644 --- a/setup.py +++ b/setup.py @@ -18,21 +18,21 @@ ''' from setuptools import setup +from smartlingApiSdk.version import version setup( name="SmartlingApiSdk", - version = "2.0.4", + version = version, author="Smartling, Inc.", author_email="aartamonov@smartling.com", - description="Smartling python library for file translations", + description="python library to work with Smartling translation services APIs", license='Apache License v2.0', keywords='translation localization internationalization', - url="https://docs.smartling.com/display/docs/Files+API", - long_description="python SDK to work with Smartling API for file translation", - packages=['smartlingApiSdk', "simplejson24", "example", "test"], + url="", + long_description="python SDK to work with Smartling API for computer assisted translation", + packages=['smartlingApiSdk','smartlingApiSdk/example','smartlingApiSdk/api','smartlingApiSdk/resources'], include_package_data = True, package_data = { - '': ['*.properties', '*.xml'], + '': ['*.properties', '*.xml', '*.png', '*.csv'], }, - ) diff --git a/simplejson24/__init__.py b/simplejson24/__init__.py deleted file mode 100644 index a09b10e..0000000 --- a/simplejson24/__init__.py +++ /dev/null @@ -1,563 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - - -r"""JSON (JavaScript Object Notation) is a subset of -JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data -interchange format. - -:mod:`simplejson` exposes an API familiar to users of the standard library -:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained -version of the :mod:`json` library contained in Python 2.6, but maintains -compatibility with Python 2.4 and Python 2.5 and (currently) has -significant performance advantages, even without using the optional C -extension for speedups. - -Encoding basic Python object hierarchies:: - - >>> import simplejson as json - >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) - '["foo", {"bar": ["baz", null, 1.0, 2]}]' - >>> print(json.dumps("\"foo\bar")) - "\"foo\bar" - >>> print(json.dumps(u'\u1234')) - "\u1234" - >>> print(json.dumps('\\')) - "\\" - >>> print(json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True)) - {"a": 0, "b": 0, "c": 0} - >>> from simplejson.compat import StringIO - >>> io = StringIO() - >>> json.dump(['streaming API'], io) - >>> io.getvalue() - '["streaming API"]' - -Compact encoding:: - - >>> import simplejson as json - >>> obj = [1,2,3,{'4': 5, '6': 7}] - >>> json.dumps(obj, separators=(',',':'), sort_keys=True) - '[1,2,3,{"4":5,"6":7}]' - -Pretty printing:: - - >>> import simplejson as json - >>> print(json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=' ')) - { - "4": 5, - "6": 7 - } - -Decoding JSON:: - - >>> import simplejson as json - >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] - >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj - True - >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' - True - >>> from simplejson.compat import StringIO - >>> io = StringIO('["streaming API"]') - >>> json.load(io)[0] == 'streaming API' - True - -Specializing JSON object decoding:: - - >>> import simplejson as json - >>> def as_complex(dct): - ... if '__complex__' in dct: - ... return complex(dct['real'], dct['imag']) - ... return dct - ... - >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', - ... object_hook=as_complex) - (1+2j) - >>> from decimal import Decimal - >>> json.loads('1.1', parse_float=Decimal) == Decimal('1.1') - True - -Specializing JSON object encoding:: - - >>> import simplejson as json - >>> def encode_complex(obj): - ... if isinstance(obj, complex): - ... return [obj.real, obj.imag] - ... raise TypeError(repr(o) + " is not JSON serializable") - ... - >>> json.dumps(2 + 1j, default=encode_complex) - '[2.0, 1.0]' - >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) - '[2.0, 1.0]' - >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) - '[2.0, 1.0]' - - -Using simplejson.tool from the shell to validate and pretty-print:: - - $ echo '{"json":"obj"}' | python -m simplejson.tool - { - "json": "obj" - } - $ echo '{ 1.2:3.4}' | python -m simplejson.tool - Expecting property name: line 1 column 3 (char 2) -""" -__version__ = '3.5.2' -__all__ = [ - 'dump', 'dumps', 'load', 'loads', - 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', - 'OrderedDict', 'simple_first', -] - -__author__ = 'Bob Ippolito ' - -from decimal import Decimal - -from scanner import JSONDecodeError -from decoder import JSONDecoder -from encoder import JSONEncoder, JSONEncoderForHTML -def _import_OrderedDict(): - import collections - try: - return collections.OrderedDict - except AttributeError: - import ordered_dict - return ordered_dict.OrderedDict -OrderedDict = _import_OrderedDict() - -def _import_c_make_encoder(): - try: - from _speedups import make_encoder - return make_encoder - except ImportError: - return None - -_default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - encoding='utf-8', - default=None, - use_decimal=True, - namedtuple_as_object=True, - tuple_as_array=True, - bigint_as_string=False, - item_sort_key=None, - for_json=False, - ignore_nan=False, - int_as_string_bitcount=None, -) - -def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, use_decimal=True, - namedtuple_as_object=True, tuple_as_array=True, - bigint_as_string=False, sort_keys=False, item_sort_key=None, - for_json=False, ignore_nan=False, int_as_string_bitcount=None, **kw): - """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a - ``.write()``-supporting file-like object). - - If *skipkeys* is true then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If *ensure_ascii* is false, then the some chunks written to ``fp`` - may be ``unicode`` instances, subject to normal Python ``str`` to - ``unicode`` coercion rules. Unless ``fp.write()`` explicitly - understands ``unicode`` (as in ``codecs.getwriter()``) this is likely - to cause an error. - - If *check_circular* is false, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If *allow_nan* is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) - in strict compliance of the original JSON specification, instead of using - the JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). See - *ignore_nan* for ECMA-262 compliant behavior. - - If *indent* is a string, then JSON array elements and object members - will be pretty-printed with a newline followed by that string repeated - for each level of nesting. ``None`` (the default) selects the most compact - representation without any newlines. For backwards compatibility with - versions of simplejson earlier than 2.1.0, an integer is also accepted - and is converted to a string with that many spaces. - - If specified, *separators* should be an - ``(item_separator, key_separator)`` tuple. The default is ``(', ', ': ')`` - if *indent* is ``None`` and ``(',', ': ')`` otherwise. To get the most - compact JSON representation, you should specify ``(',', ':')`` to eliminate - whitespace. - - *encoding* is the character encoding for str instances, default is UTF-8. - - *default(obj)* is a function that should return a serializable version - of obj or raise ``TypeError``. The default simply raises ``TypeError``. - - If *use_decimal* is true (default: ``True``) then decimal.Decimal - will be natively serialized to JSON with full precision. - - If *namedtuple_as_object* is true (default: ``True``), - :class:`tuple` subclasses with ``_asdict()`` methods will be encoded - as JSON objects. - - If *tuple_as_array* is true (default: ``True``), - :class:`tuple` (and subclasses) will be encoded as JSON arrays. - - If *bigint_as_string* is true (default: ``False``), ints 2**53 and higher - or lower than -2**53 will be encoded as strings. This is to avoid the - rounding that happens in Javascript otherwise. Note that this is still a - lossy operation that will not round-trip correctly and should be used - sparingly. - - If *int_as_string_bitcount* is a positive number (n), then int of size - greater than or equal to 2**n or lower than or equal to -2**n will be - encoded as strings. - - If specified, *item_sort_key* is a callable used to sort the items in - each dictionary. This is useful if you want to sort items other than - in alphabetical order by key. This option takes precedence over - *sort_keys*. - - If *sort_keys* is true (default: ``False``), the output of dictionaries - will be sorted by item. - - If *for_json* is true (default: ``False``), objects with a ``for_json()`` - method will use the return value of that method for encoding as JSON - instead of the object. - - If *ignore_nan* is true (default: ``False``), then out of range - :class:`float` values (``nan``, ``inf``, ``-inf``) will be serialized as - ``null`` in compliance with the ECMA-262 specification. If true, this will - override *allow_nan*. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. NOTE: You should use *default* or *for_json* instead - of subclassing whenever possible. - - """ - # cached encoder - if (not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and use_decimal - and namedtuple_as_object and tuple_as_array - and not bigint_as_string and int_as_string_bitcount is None - and not item_sort_key and not for_json and not ignore_nan and not kw): - iterable = _default_encoder.iterencode(obj) - else: - if cls is None: - cls = JSONEncoder - iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, - default=default, use_decimal=use_decimal, - namedtuple_as_object=namedtuple_as_object, - tuple_as_array=tuple_as_array, - bigint_as_string=bigint_as_string, - sort_keys=sort_keys, - item_sort_key=item_sort_key, - for_json=for_json, - ignore_nan=ignore_nan, - int_as_string_bitcount=int_as_string_bitcount, - **kw).iterencode(obj) - # could accelerate with writelines in some versions of Python, at - # a debuggability cost - for chunk in iterable: - fp.write(chunk) - - -def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, use_decimal=True, - namedtuple_as_object=True, tuple_as_array=True, - bigint_as_string=False, sort_keys=False, item_sort_key=None, - for_json=False, ignore_nan=False, int_as_string_bitcount=None, **kw): - """Serialize ``obj`` to a JSON formatted ``str``. - - If ``skipkeys`` is false then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is false, then the return value will be a - ``unicode`` instance subject to normal Python ``str`` to ``unicode`` - coercion rules instead of being escaped to an ASCII ``str``. - - If ``check_circular`` is false, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in - strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - If ``indent`` is a string, then JSON array elements and object members - will be pretty-printed with a newline followed by that string repeated - for each level of nesting. ``None`` (the default) selects the most compact - representation without any newlines. For backwards compatibility with - versions of simplejson earlier than 2.1.0, an integer is also accepted - and is converted to a string with that many spaces. - - If specified, ``separators`` should be an - ``(item_separator, key_separator)`` tuple. The default is ``(', ', ': ')`` - if *indent* is ``None`` and ``(',', ': ')`` otherwise. To get the most - compact JSON representation, you should specify ``(',', ':')`` to eliminate - whitespace. - - ``encoding`` is the character encoding for str instances, default is UTF-8. - - ``default(obj)`` is a function that should return a serializable version - of obj or raise TypeError. The default simply raises TypeError. - - If *use_decimal* is true (default: ``True``) then decimal.Decimal - will be natively serialized to JSON with full precision. - - If *namedtuple_as_object* is true (default: ``True``), - :class:`tuple` subclasses with ``_asdict()`` methods will be encoded - as JSON objects. - - If *tuple_as_array* is true (default: ``True``), - :class:`tuple` (and subclasses) will be encoded as JSON arrays. - - If *bigint_as_string* is true (not the default), ints 2**53 and higher - or lower than -2**53 will be encoded as strings. This is to avoid the - rounding that happens in Javascript otherwise. - - If *int_as_string_bitcount* is a positive number (n), then int of size - greater than or equal to 2**n or lower than or equal to -2**n will be - encoded as strings. - - If specified, *item_sort_key* is a callable used to sort the items in - each dictionary. This is useful if you want to sort items other than - in alphabetical order by key. This option takes precendence over - *sort_keys*. - - If *sort_keys* is true (default: ``False``), the output of dictionaries - will be sorted by item. - - If *for_json* is true (default: ``False``), objects with a ``for_json()`` - method will use the return value of that method for encoding as JSON - instead of the object. - - If *ignore_nan* is true (default: ``False``), then out of range - :class:`float` values (``nan``, ``inf``, ``-inf``) will be serialized as - ``null`` in compliance with the ECMA-262 specification. If true, this will - override *allow_nan*. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. NOTE: You should use *default* instead of subclassing - whenever possible. - - """ - # cached encoder - if ( - not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and use_decimal - and namedtuple_as_object and tuple_as_array - and not bigint_as_string and int_as_string_bitcount is None - and not sort_keys and not item_sort_key and not for_json - and not ignore_nan and not kw - ): - return _default_encoder.encode(obj) - if cls is None: - cls = JSONEncoder - return cls( - skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, default=default, - use_decimal=use_decimal, - namedtuple_as_object=namedtuple_as_object, - tuple_as_array=tuple_as_array, - bigint_as_string=bigint_as_string, - sort_keys=sort_keys, - item_sort_key=item_sort_key, - for_json=for_json, - ignore_nan=ignore_nan, - int_as_string_bitcount=int_as_string_bitcount, - **kw).encode(obj) - - -_default_decoder = JSONDecoder(encoding=None, object_hook=None, - object_pairs_hook=None) - - -def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, object_pairs_hook=None, - use_decimal=False, namedtuple_as_object=True, tuple_as_array=True, - **kw): - """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing - a JSON document) to a Python object. - - *encoding* determines the encoding used to interpret any - :class:`str` objects decoded by this instance (``'utf-8'`` by - default). It has no effect when decoding :class:`unicode` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as :class:`unicode`. - - *object_hook*, if specified, will be called with the result of every - JSON object decoded and its return value will be used in place of the - given :class:`dict`. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - - *object_pairs_hook* is an optional function that will be called with - the result of any object literal decode with an ordered list of pairs. - The return value of *object_pairs_hook* will be used instead of the - :class:`dict`. This feature can be used to implement custom decoders - that rely on the order that the key and value pairs are decoded (for - example, :func:`collections.OrderedDict` will remember the order of - insertion). If *object_hook* is also defined, the *object_pairs_hook* - takes priority. - - *parse_float*, if specified, will be called with the string of every - JSON float to be decoded. By default, this is equivalent to - ``float(num_str)``. This can be used to use another datatype or parser - for JSON floats (e.g. :class:`decimal.Decimal`). - - *parse_int*, if specified, will be called with the string of every - JSON int to be decoded. By default, this is equivalent to - ``int(num_str)``. This can be used to use another datatype or parser - for JSON integers (e.g. :class:`float`). - - *parse_constant*, if specified, will be called with one of the - following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This - can be used to raise an exception if invalid JSON numbers are - encountered. - - If *use_decimal* is true (default: ``False``) then it implies - parse_float=decimal.Decimal for parity with ``dump``. - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. NOTE: You should use *object_hook* or *object_pairs_hook* instead - of subclassing whenever possible. - - """ - return loads(fp.read(), - encoding=encoding, cls=cls, object_hook=object_hook, - parse_float=parse_float, parse_int=parse_int, - parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, - use_decimal=use_decimal, **kw) - - -def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, object_pairs_hook=None, - use_decimal=False, **kw): - """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON - document) to a Python object. - - *encoding* determines the encoding used to interpret any - :class:`str` objects decoded by this instance (``'utf-8'`` by - default). It has no effect when decoding :class:`unicode` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as :class:`unicode`. - - *object_hook*, if specified, will be called with the result of every - JSON object decoded and its return value will be used in place of the - given :class:`dict`. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - - *object_pairs_hook* is an optional function that will be called with - the result of any object literal decode with an ordered list of pairs. - The return value of *object_pairs_hook* will be used instead of the - :class:`dict`. This feature can be used to implement custom decoders - that rely on the order that the key and value pairs are decoded (for - example, :func:`collections.OrderedDict` will remember the order of - insertion). If *object_hook* is also defined, the *object_pairs_hook* - takes priority. - - *parse_float*, if specified, will be called with the string of every - JSON float to be decoded. By default, this is equivalent to - ``float(num_str)``. This can be used to use another datatype or parser - for JSON floats (e.g. :class:`decimal.Decimal`). - - *parse_int*, if specified, will be called with the string of every - JSON int to be decoded. By default, this is equivalent to - ``int(num_str)``. This can be used to use another datatype or parser - for JSON integers (e.g. :class:`float`). - - *parse_constant*, if specified, will be called with one of the - following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This - can be used to raise an exception if invalid JSON numbers are - encountered. - - If *use_decimal* is true (default: ``False``) then it implies - parse_float=decimal.Decimal for parity with ``dump``. - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. NOTE: You should use *object_hook* or *object_pairs_hook* instead - of subclassing whenever possible. - - """ - if (cls is None and encoding is None and object_hook is None and - parse_int is None and parse_float is None and - parse_constant is None and object_pairs_hook is None - and not use_decimal and not kw): - return _default_decoder.decode(s) - if cls is None: - cls = JSONDecoder - if object_hook is not None: - kw['object_hook'] = object_hook - if object_pairs_hook is not None: - kw['object_pairs_hook'] = object_pairs_hook - if parse_float is not None: - kw['parse_float'] = parse_float - if parse_int is not None: - kw['parse_int'] = parse_int - if parse_constant is not None: - kw['parse_constant'] = parse_constant - if use_decimal: - if parse_float is not None: - raise TypeError("use_decimal=True implies parse_float=Decimal") - kw['parse_float'] = Decimal - return cls(encoding=encoding, **kw).decode(s) - - -def _toggle_speedups(enabled): - import decoder as dec - import encoder as enc - import scanner as scan - c_make_encoder = _import_c_make_encoder() - if enabled: - dec.scanstring = dec.c_scanstring or dec.py_scanstring - enc.c_make_encoder = c_make_encoder - enc.encode_basestring_ascii = (enc.c_encode_basestring_ascii or - enc.py_encode_basestring_ascii) - scan.make_scanner = scan.c_make_scanner or scan.py_make_scanner - else: - dec.scanstring = dec.py_scanstring - enc.c_make_encoder = None - enc.encode_basestring_ascii = enc.py_encode_basestring_ascii - scan.make_scanner = scan.py_make_scanner - dec.make_scanner = scan.make_scanner - global _default_decoder - _default_decoder = JSONDecoder( - encoding=None, - object_hook=None, - object_pairs_hook=None, - ) - global _default_encoder - _default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - encoding='utf-8', - default=None, - ) - -def simple_first(kv): - """Helper function to pass to item_sort_key to sort simple - elements to the top, then container elements. - """ - return (isinstance(kv[1], (list, dict, tuple)), kv[0]) diff --git a/simplejson24/compat.py b/simplejson24/compat.py deleted file mode 100644 index a5e3859..0000000 --- a/simplejson24/compat.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - - -"""Python 3 compatibility shims -""" -import sys -if sys.version_info[0] < 3: - PY3 = False - def b(s): - return s - def u(s): - return unicode(s, 'unicode_escape') - import cStringIO as StringIO - StringIO = BytesIO = StringIO.StringIO - text_type = unicode - binary_type = str - string_types = (basestring,) - integer_types = (int, long) - unichr = unichr - reload_module = reload - def fromhex(s): - return s.decode('hex') - -else: - PY3 = True - if sys.version_info[:2] >= (3, 4): - from importlib import reload as reload_module - else: - from imp import reload as reload_module - import codecs - def b(s): - return codecs.latin_1_encode(s)[0] - def u(s): - return s - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - text_type = str - binary_type = bytes - string_types = (str,) - integer_types = (int,) - - def unichr(s): - return u(chr(s)) - - def fromhex(s): - return bytes.fromhex(s) - -long_type = integer_types[-1] diff --git a/simplejson24/decoder.py b/simplejson24/decoder.py deleted file mode 100644 index f5f31a1..0000000 --- a/simplejson24/decoder.py +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - - -"""Implementation of JSONDecoder -""" -import re -import sys -import struct -from compat import fromhex, b, u, text_type, binary_type, PY3, unichr -from scanner import make_scanner, JSONDecodeError - -def _import_c_scanstring(): - try: - from _speedups import scanstring - return scanstring - except ImportError: - return None -c_scanstring = _import_c_scanstring() - -# NOTE (3.1.0): JSONDecodeError may still be imported from this module for -# compatibility, but it was never in the __all__ -__all__ = ['JSONDecoder'] - -FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL - -def _floatconstants(): - _BYTES = fromhex('7FF80000000000007FF0000000000000') - # The struct module in Python 2.4 would get frexp() out of range here - # when an endian is specified in the format string. Fixed in Python 2.5+ - if sys.byteorder != 'big': - _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] - nan, inf = struct.unpack('dd', _BYTES) - return nan, inf, -inf - -NaN, PosInf, NegInf = _floatconstants() - -_CONSTANTS = { - '-Infinity': NegInf, - 'Infinity': PosInf, - 'NaN': NaN, -} - -STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) -BACKSLASH = { - '"': u('"'), '\\': u('\u005c'), '/': u('/'), - 'b': u('\b'), 'f': u('\f'), 'n': u('\n'), 'r': u('\r'), 't': u('\t'), -} - -DEFAULT_ENCODING = "utf-8" - -def py_scanstring(s, end, encoding=None, strict=True, - _b=BACKSLASH, _m=STRINGCHUNK.match, _join=u('').join, - _PY3=PY3, _maxunicode=sys.maxunicode): - """Scan the string s for a JSON string. End is the index of the - character in s after the quote that started the JSON string. - Unescapes all valid JSON string escape sequences and raises ValueError - on attempt to decode an invalid string. If strict is False then literal - control characters are allowed in the string. - - Returns a tuple of the decoded string and the index of the character in s - after the end quote.""" - if encoding is None: - encoding = DEFAULT_ENCODING - chunks = [] - _append = chunks.append - begin = end - 1 - while 1: - chunk = _m(s, end) - if chunk is None: - raise JSONDecodeError( - "Unterminated string starting at", s, begin) - end = chunk.end() - content, terminator = chunk.groups() - # Content is contains zero or more unescaped string characters - if content: - if not _PY3 and not isinstance(content, text_type): - content = text_type(content, encoding) - _append(content) - # Terminator is the end of string, a literal control character, - # or a backslash denoting that an escape sequence follows - if terminator == '"': - break - elif terminator != '\\': - if strict: - msg = "Invalid control character %r at" - raise JSONDecodeError(msg, s, end) - else: - _append(terminator) - continue - try: - esc = s[end] - except IndexError: - raise JSONDecodeError( - "Unterminated string starting at", s, begin) - # If not a unicode escape sequence, must be in the lookup table - if esc != 'u': - try: - char = _b[esc] - except KeyError: - msg = "Invalid \\X escape sequence %r" - raise JSONDecodeError(msg, s, end) - end += 1 - else: - # Unicode escape sequence - msg = "Invalid \\uXXXX escape sequence" - esc = s[end + 1:end + 5] - escX = esc[1:2] - if len(esc) != 4 or escX == 'x' or escX == 'X': - raise JSONDecodeError(msg, s, end - 1) - try: - uni = int(esc, 16) - except ValueError: - raise JSONDecodeError(msg, s, end - 1) - end += 5 - # Check for surrogate pair on UCS-4 systems - # Note that this will join high/low surrogate pairs - # but will also pass unpaired surrogates through - if (_maxunicode > 65535 and - uni & 0xfc00 == 0xd800 and - s[end:end + 2] == '\\u'): - esc2 = s[end + 2:end + 6] - escX = esc2[1:2] - if len(esc2) == 4 and not (escX == 'x' or escX == 'X'): - try: - uni2 = int(esc2, 16) - except ValueError: - raise JSONDecodeError(msg, s, end) - if uni2 & 0xfc00 == 0xdc00: - uni = 0x10000 + (((uni - 0xd800) << 10) | - (uni2 - 0xdc00)) - end += 6 - char = unichr(uni) - # Append the unescaped character - _append(char) - return _join(chunks), end - - -# Use speedup if available -scanstring = c_scanstring or py_scanstring - -WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) -WHITESPACE_STR = ' \t\n\r' - -def JSONObject(state, encoding, strict, scan_once, object_hook, - object_pairs_hook, memo=None, - _w=WHITESPACE.match, _ws=WHITESPACE_STR): - (s, end) = state - # Backwards compatibility - if memo is None: - memo = {} - memo_get = memo.setdefault - pairs = [] - # Use a slice to prevent IndexError from being raised, the following - # check will raise a more specific ValueError if the string is empty - nextchar = s[end:end + 1] - # Normally we expect nextchar == '"' - if nextchar != '"': - if nextchar in _ws: - end = _w(s, end).end() - nextchar = s[end:end + 1] - # Trivial empty object - if nextchar == '}': - if object_pairs_hook is not None: - result = object_pairs_hook(pairs) - return result, end + 1 - pairs = {} - if object_hook is not None: - pairs = object_hook(pairs) - return pairs, end + 1 - elif nextchar != '"': - raise JSONDecodeError( - "Expecting property name enclosed in double quotes", - s, end) - end += 1 - while True: - key, end = scanstring(s, end, encoding, strict) - key = memo_get(key, key) - - # To skip some function call overhead we optimize the fast paths where - # the JSON key separator is ": " or just ":". - if s[end:end + 1] != ':': - end = _w(s, end).end() - if s[end:end + 1] != ':': - raise JSONDecodeError("Expecting ':' delimiter", s, end) - - end += 1 - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - value, end = scan_once(s, end) - pairs.append((key, value)) - - try: - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - end += 1 - - if nextchar == '}': - break - elif nextchar != ',': - raise JSONDecodeError("Expecting ',' delimiter or '}'", s, end - 1) - - try: - nextchar = s[end] - if nextchar in _ws: - end += 1 - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - - end += 1 - if nextchar != '"': - raise JSONDecodeError( - "Expecting property name enclosed in double quotes", - s, end - 1) - - if object_pairs_hook is not None: - result = object_pairs_hook(pairs) - return result, end - pairs = dict(pairs) - if object_hook is not None: - pairs = object_hook(pairs) - return pairs, end - -def JSONArray(state, scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): - (s, end) = state - values = [] - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - # Look-ahead for trivial empty array - if nextchar == ']': - return values, end + 1 - elif nextchar == '': - raise JSONDecodeError("Expecting value or ']'", s, end) - _append = values.append - while True: - value, end = scan_once(s, end) - _append(value) - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == ']': - break - elif nextchar != ',': - raise JSONDecodeError("Expecting ',' delimiter or ']'", s, end - 1) - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - return values, end - -class JSONDecoder(object): - """Simple JSON decoder - - Performs the following translations in decoding by default: - - +---------------+-------------------+ - | JSON | Python | - +===============+===================+ - | object | dict | - +---------------+-------------------+ - | array | list | - +---------------+-------------------+ - | string | str, unicode | - +---------------+-------------------+ - | number (int) | int, long | - +---------------+-------------------+ - | number (real) | float | - +---------------+-------------------+ - | true | True | - +---------------+-------------------+ - | false | False | - +---------------+-------------------+ - | null | None | - +---------------+-------------------+ - - It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as - their corresponding ``float`` values, which is outside the JSON spec. - - """ - - def __init__(self, encoding=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, strict=True, - object_pairs_hook=None): - """ - *encoding* determines the encoding used to interpret any - :class:`str` objects decoded by this instance (``'utf-8'`` by - default). It has no effect when decoding :class:`unicode` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as :class:`unicode`. - - *object_hook*, if specified, will be called with the result of every - JSON object decoded and its return value will be used in place of the - given :class:`dict`. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - - *object_pairs_hook* is an optional function that will be called with - the result of any object literal decode with an ordered list of pairs. - The return value of *object_pairs_hook* will be used instead of the - :class:`dict`. This feature can be used to implement custom decoders - that rely on the order that the key and value pairs are decoded (for - example, :func:`collections.OrderedDict` will remember the order of - insertion). If *object_hook* is also defined, the *object_pairs_hook* - takes priority. - - *parse_float*, if specified, will be called with the string of every - JSON float to be decoded. By default, this is equivalent to - ``float(num_str)``. This can be used to use another datatype or parser - for JSON floats (e.g. :class:`decimal.Decimal`). - - *parse_int*, if specified, will be called with the string of every - JSON int to be decoded. By default, this is equivalent to - ``int(num_str)``. This can be used to use another datatype or parser - for JSON integers (e.g. :class:`float`). - - *parse_constant*, if specified, will be called with one of the - following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This - can be used to raise an exception if invalid JSON numbers are - encountered. - - *strict* controls the parser's behavior when it encounters an - invalid control character in a string. The default setting of - ``True`` means that unescaped control characters are parse errors, if - ``False`` then control characters will be allowed in strings. - - """ - if encoding is None: - encoding = DEFAULT_ENCODING - self.encoding = encoding - self.object_hook = object_hook - self.object_pairs_hook = object_pairs_hook - self.parse_float = parse_float or float - self.parse_int = parse_int or int - self.parse_constant = parse_constant or _CONSTANTS.__getitem__ - self.strict = strict - self.parse_object = JSONObject - self.parse_array = JSONArray - self.parse_string = scanstring - self.memo = {} - self.scan_once = make_scanner(self) - - def decode(self, s, _w=WHITESPACE.match, _PY3=PY3): - """Return the Python representation of ``s`` (a ``str`` or ``unicode`` - instance containing a JSON document) - - """ - if _PY3 and isinstance(s, binary_type): - s = s.decode(self.encoding) - obj, end = self.raw_decode(s) - end = _w(s, end).end() - if end != len(s): - raise JSONDecodeError("Extra data", s, end, len(s)) - return obj - - def raw_decode(self, s, idx=0, _w=WHITESPACE.match, _PY3=PY3): - """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` - beginning with a JSON document) and return a 2-tuple of the Python - representation and the index in ``s`` where the document ended. - Optionally, ``idx`` can be used to specify an offset in ``s`` where - the JSON document begins. - - This can be used to decode a JSON document from a string that may - have extraneous data at the end. - - """ - if _PY3 and not isinstance(s, text_type): - raise TypeError("Input string must be text, not bytes") - return self.scan_once(s, idx=_w(s, idx).end()) diff --git a/simplejson24/encoder.py b/simplejson24/encoder.py deleted file mode 100644 index 20f3ca5..0000000 --- a/simplejson24/encoder.py +++ /dev/null @@ -1,652 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - - -"""Implementation of JSONEncoder -""" -import re -from operator import itemgetter -from decimal import Decimal -from compat import u, unichr, binary_type, string_types, integer_types, PY3 -def _import_speedups(): - try: - import _speedups - return _speedups.encode_basestring_ascii, _speedups.make_encoder - except ImportError: - return None, None -c_encode_basestring_ascii, c_make_encoder = _import_speedups() - -from simplejson.decoder import PosInf - -#ESCAPE = re.compile(ur'[\x00-\x1f\\"\b\f\n\r\t\u2028\u2029]') -# This is required because u() will mangle the string and ur'' isn't valid -# python3 syntax -ESCAPE = re.compile(u'[\\x00-\\x1f\\\\"\\b\\f\\n\\r\\t\u2028\u2029]') -ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') -HAS_UTF8 = re.compile(r'[\x80-\xff]') -ESCAPE_DCT = { - '\\': '\\\\', - '"': '\\"', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', -} -for i in range(0x20): - #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) - ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) -for i in [0x2028, 0x2029]: - ESCAPE_DCT.setdefault(unichr(i), '\\u%04x' % (i,)) - -FLOAT_REPR = repr - -def encode_basestring(s, _PY3=PY3, _q=u('"')): - """Return a JSON representation of a Python string - - """ - if _PY3: - if isinstance(s, binary_type): - s = s.decode('utf-8') - else: - if isinstance(s, str) and HAS_UTF8.search(s) is not None: - s = s.decode('utf-8') - def replace(match): - return ESCAPE_DCT[match.group(0)] - return _q + ESCAPE.sub(replace, s) + _q - - -def py_encode_basestring_ascii(s, _PY3=PY3): - """Return an ASCII-only JSON representation of a Python string - - """ - if _PY3: - if isinstance(s, binary_type): - s = s.decode('utf-8') - else: - if isinstance(s, str) and HAS_UTF8.search(s) is not None: - s = s.decode('utf-8') - def replace(match): - s = match.group(0) - try: - return ESCAPE_DCT[s] - except KeyError: - n = ord(s) - if n < 0x10000: - #return '\\u{0:04x}'.format(n) - return '\\u%04x' % (n,) - else: - # surrogate pair - n -= 0x10000 - s1 = 0xd800 | ((n >> 10) & 0x3ff) - s2 = 0xdc00 | (n & 0x3ff) - #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) - return '\\u%04x\\u%04x' % (s1, s2) - return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' - - -encode_basestring_ascii = ( - c_encode_basestring_ascii or py_encode_basestring_ascii) - -class JSONEncoder(object): - """Extensible JSON encoder for Python data structures. - - Supports the following objects and types by default: - - +-------------------+---------------+ - | Python | JSON | - +===================+===============+ - | dict, namedtuple | object | - +-------------------+---------------+ - | list, tuple | array | - +-------------------+---------------+ - | str, unicode | string | - +-------------------+---------------+ - | int, long, float | number | - +-------------------+---------------+ - | True | true | - +-------------------+---------------+ - | False | false | - +-------------------+---------------+ - | None | null | - +-------------------+---------------+ - - To extend this to recognize other objects, subclass and implement a - ``.default()`` method with another method that returns a serializable - object for ``o`` if possible, otherwise it should call the superclass - implementation (to raise ``TypeError``). - - """ - item_separator = ', ' - key_separator = ': ' - - def __init__(self, skipkeys=False, ensure_ascii=True, - check_circular=True, allow_nan=True, sort_keys=False, - indent=None, separators=None, encoding='utf-8', default=None, - use_decimal=True, namedtuple_as_object=True, - tuple_as_array=True, bigint_as_string=False, - item_sort_key=None, for_json=False, ignore_nan=False, - int_as_string_bitcount=None): - """Constructor for JSONEncoder, with sensible defaults. - - If skipkeys is false, then it is a TypeError to attempt - encoding of keys that are not str, int, long, float or None. If - skipkeys is True, such items are simply skipped. - - If ensure_ascii is true, the output is guaranteed to be str - objects with all incoming unicode characters escaped. If - ensure_ascii is false, the output will be unicode object. - - If check_circular is true, then lists, dicts, and custom encoded - objects will be checked for circular references during encoding to - prevent an infinite recursion (which would cause an OverflowError). - Otherwise, no such check takes place. - - If allow_nan is true, then NaN, Infinity, and -Infinity will be - encoded as such. This behavior is not JSON specification compliant, - but is consistent with most JavaScript based encoders and decoders. - Otherwise, it will be a ValueError to encode such floats. - - If sort_keys is true, then the output of dictionaries will be - sorted by key; this is useful for regression tests to ensure - that JSON serializations can be compared on a day-to-day basis. - - If indent is a string, then JSON array elements and object members - will be pretty-printed with a newline followed by that string repeated - for each level of nesting. ``None`` (the default) selects the most compact - representation without any newlines. For backwards compatibility with - versions of simplejson earlier than 2.1.0, an integer is also accepted - and is converted to a string with that many spaces. - - If specified, separators should be an (item_separator, key_separator) - tuple. The default is (', ', ': ') if *indent* is ``None`` and - (',', ': ') otherwise. To get the most compact JSON representation, - you should specify (',', ':') to eliminate whitespace. - - If specified, default is a function that gets called for objects - that can't otherwise be serialized. It should return a JSON encodable - version of the object or raise a ``TypeError``. - - If encoding is not None, then all input strings will be - transformed into unicode using that encoding prior to JSON-encoding. - The default is UTF-8. - - If use_decimal is true (not the default), ``decimal.Decimal`` will - be supported directly by the encoder. For the inverse, decode JSON - with ``parse_float=decimal.Decimal``. - - If namedtuple_as_object is true (the default), objects with - ``_asdict()`` methods will be encoded as JSON objects. - - If tuple_as_array is true (the default), tuple (and subclasses) will - be encoded as JSON arrays. - - If bigint_as_string is true (not the default), ints 2**53 and higher - or lower than -2**53 will be encoded as strings. This is to avoid the - rounding that happens in Javascript otherwise. - - If int_as_string_bitcount is a positive number (n), then int of size - greater than or equal to 2**n or lower than or equal to -2**n will be - encoded as strings. - - If specified, item_sort_key is a callable used to sort the items in - each dictionary. This is useful if you want to sort items other than - in alphabetical order by key. - - If for_json is true (not the default), objects with a ``for_json()`` - method will use the return value of that method for encoding as JSON - instead of the object. - - If *ignore_nan* is true (default: ``False``), then out of range - :class:`float` values (``nan``, ``inf``, ``-inf``) will be serialized - as ``null`` in compliance with the ECMA-262 specification. If true, - this will override *allow_nan*. - - """ - - self.skipkeys = skipkeys - self.ensure_ascii = ensure_ascii - self.check_circular = check_circular - self.allow_nan = allow_nan - self.sort_keys = sort_keys - self.use_decimal = use_decimal - self.namedtuple_as_object = namedtuple_as_object - self.tuple_as_array = tuple_as_array - self.bigint_as_string = bigint_as_string - self.item_sort_key = item_sort_key - self.for_json = for_json - self.ignore_nan = ignore_nan - self.int_as_string_bitcount = int_as_string_bitcount - if indent is not None and not isinstance(indent, string_types): - indent = indent * ' ' - self.indent = indent - if separators is not None: - self.item_separator, self.key_separator = separators - elif indent is not None: - self.item_separator = ',' - if default is not None: - self.default = default - self.encoding = encoding - - def default(self, o): - """Implement this method in a subclass such that it returns - a serializable object for ``o``, or calls the base implementation - (to raise a ``TypeError``). - - For example, to support arbitrary iterators, you could - implement default like this:: - - def default(self, o): - try: - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) - - """ - raise TypeError(repr(o) + " is not JSON serializable") - - def encode(self, o): - """Return a JSON string representation of a Python data structure. - - >>> from simplejson import JSONEncoder - >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) - '{"foo": ["bar", "baz"]}' - - """ - # This is for extremely simple cases and benchmarks. - if isinstance(o, binary_type): - _encoding = self.encoding - if (_encoding is not None and not (_encoding == 'utf-8')): - o = o.decode(_encoding) - if isinstance(o, string_types): - if self.ensure_ascii: - return encode_basestring_ascii(o) - else: - return encode_basestring(o) - # This doesn't pass the iterator directly to ''.join() because the - # exceptions aren't as detailed. The list call should be roughly - # equivalent to the PySequence_Fast that ''.join() would do. - chunks = self.iterencode(o, _one_shot=True) - if not isinstance(chunks, (list, tuple)): - chunks = list(chunks) - if self.ensure_ascii: - return ''.join(chunks) - else: - return u''.join(chunks) - - def iterencode(self, o, _one_shot=False): - """Encode the given object and yield each string - representation as available. - - For example:: - - for chunk in JSONEncoder().iterencode(bigobject): - mysocket.write(chunk) - - """ - if self.check_circular: - markers = {} - else: - markers = None - if self.ensure_ascii: - _encoder = encode_basestring_ascii - else: - _encoder = encode_basestring - if self.encoding != 'utf-8': - def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): - if isinstance(o, binary_type): - o = o.decode(_encoding) - return _orig_encoder(o) - - def floatstr(o, allow_nan=self.allow_nan, ignore_nan=self.ignore_nan, - _repr=FLOAT_REPR, _inf=PosInf, _neginf=-PosInf): - # Check for specials. Note that this type of test is processor - # and/or platform-specific, so do tests which don't depend on - # the internals. - - if o != o: - text = 'NaN' - elif o == _inf: - text = 'Infinity' - elif o == _neginf: - text = '-Infinity' - else: - return _repr(o) - - if ignore_nan: - text = 'null' - elif not allow_nan: - raise ValueError( - "Out of range float values are not JSON compliant: " + - repr(o)) - - return text - - key_memo = {} - if self.bigint_as_string: bc=53 - else: bc = self.int_as_string_bitcount - int_as_string_bitcount = (bc) - if (_one_shot and c_make_encoder is not None - and self.indent is None): - _iterencode = c_make_encoder( - markers, self.default, _encoder, self.indent, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, self.allow_nan, key_memo, self.use_decimal, - self.namedtuple_as_object, self.tuple_as_array, - int_as_string_bitcount, - self.item_sort_key, self.encoding, self.for_json, - self.ignore_nan, Decimal) - else: - _iterencode = _make_iterencode( - markers, self.default, _encoder, self.indent, floatstr, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, _one_shot, self.use_decimal, - self.namedtuple_as_object, self.tuple_as_array, - int_as_string_bitcount, - self.item_sort_key, self.encoding, self.for_json, - Decimal=Decimal) - try: - return _iterencode(o, 0) - finally: - key_memo.clear() - - -class JSONEncoderForHTML(JSONEncoder): - """An encoder that produces JSON safe to embed in HTML. - - To embed JSON content in, say, a script tag on a web page, the - characters &, < and > should be escaped. They cannot be escaped - with the usual entities (e.g. &) because they are not expanded - within