diff --git a/.travis.yml b/.travis.yml index 4676a0c..7537736 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ python: install: - pip install -U pip -- pip install pypandoc - pip install .[test] - if [[ $TRAVIS_PYTHON_VERSION != 2.7 ]]; then pip install .[async]; fi diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..c2493a7 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,110 @@ +## History + +### 0.1.0 (2016-11-09) + + - First release on gemfury + +### 0.1.1 (2016-11-09) + + - change names + +### 0.1.2 (2016-11-09) + + - fix issue with 401-retry + +### 0.1.3 (2016-11-10) + + - add dependencies to setup.py + +### 0.1.4 (2016-11-11) + + - cli tool + +### 0.1.5 (2016-11-11) + + - fix apikey url query param error + +### 0.1.6 (2016-11-11) + + - introduce different token\_issuer\_host thatn api\_host + +### 0.1.7 (2016-12-06) + + - Introduce context\_getter on session object, defaulted to holding + CorrelationId=random\_uuid + +### 1.0.0 (2017-02-01) + + - first release as oss, major refactoring of inner machinery (session + objects, retry policies, cli, tests etc) + +### 1.1.0 (2017-06-12) + + - fixed logging so it does not use root logger. according to best + practices mentioned in + + - removed dependency on httpretty since it is not supporting py3 + +### 2.0.0 (2017-09-29) + + - DEPRECATED: create\_session is getting deprecated, use + trustpilot.client.default\_session.setup instead + - now able to query public endpoints without being authenticated + +### 2.1.0 (2017-10-05) + + - fixed issue in cli.post & cli.put where 'content\_type' should be + 'content-type' + +### 3.0.0 (2018-01-18) + +DELETED DO NOT USE\!\! + + - add async-client + +### 3.0.1 (2018-01-18) + + - removed prints + - made async\_client retry on unauthorized + +### 4.0.0 (2018-06-06) + + - drop support for Python 3.3 + +### 4.0.1 (2018-06-06) + + - Switch to non-deprecated session object for utility method calls + +### 4.0.2 (2018-10-30) + + - Upgrade requests to 2.20.0 + +### 5.0.0 (2019-01-04) + + - Update to authentication methods + +### 5.0.1 (2019-02-04) + + - Fix documentation formatting + +### 6.0.0 (2019-02-06) + + - reorganize code + - add user-agent header + - get access\_token with async call in async\_client + +### 6.0.3 (2019-08-15) + + - Added support for 'API Version' parameter for Client initialisation. + +### 6.0.4 (2019-08-15) + + - Remove auto-deploy to travis + +### 6.0.5 (2019-08-15) + + - allow newer version of requests dependency + +### 6.0.6 (2019-09-18) + + - specify user agent through env-var or kwarg \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst deleted file mode 100644 index 7ab638a..0000000 --- a/HISTORY.rst +++ /dev/null @@ -1,127 +0,0 @@ -======= -History -======= - -0.1.0 (2016-11-09) ------------------- - -* First release on gemfury - -0.1.1 (2016-11-09) ------------------- - -* change names - -0.1.2 (2016-11-09) ------------------- - -* fix issue with 401-retry - -0.1.3 (2016-11-10) ------------------- - -* add dependencies to setup.py - -0.1.4 (2016-11-11) ------------------- - -* cli tool - -0.1.5 (2016-11-11) ------------------- - -* fix apikey url query param error - -0.1.6 (2016-11-11) ------------------- - -* introduce different token_issuer_host thatn api_host - -0.1.7 (2016-12-06) ------------------- - -* Introduce context_getter on session object, defaulted to holding CorrelationId=random_uuid - -1.0.0 (2017-02-01) ------------------- - -* first release as oss, major refactoring of inner machinery (session objects, retry policies, cli, tests etc) - -1.1.0 (2017-06-12) ------------------- - -* fixed logging so it does not use root logger. according to best practices mentioned in http://pythonsweetness.tumblr.com/post/67394619015/use-of-logging-package-from-within-a-library -* removed dependency on httpretty since it is not supporting py3 - -2.0.0 (2017-09-29) ------------------- - -* DEPRECATED: create_session is getting deprecated, use trustpilot.client.default_session.setup instead -* now able to query public endpoints without being authenticated - -2.1.0 (2017-10-05) ------------------- - -* fixed issue in cli.post & cli.put where 'content_type' should be 'content-type' - -3.0.0 (2018-01-18) ------------------- - -DELETED DO NOT USE!! - -* add async-client - - -3.0.1 (2018-01-18) ------------------- - -* removed prints -* made async_client retry on unauthorized - -4.0.0 (2018-06-06) ------------------- - -* drop support for Python 3.3 - - -4.0.1 (2018-06-06) ------------------- - -* Switch to non-deprecated session object for utility method calls - -4.0.2 (2018-10-30) ------------------- - -* Upgrade requests to 2.20.0 - -5.0.0 (2019-01-04) ------------------- - -* Update to authentication methods - -5.0.1 (2019-02-04) ------------------- - -* Fix documentation formatting - -6.0.0 (2019-02-06) ------------------- - -* reorganize code -* add user-agent header -* get access_token with async call in async_client - -6.0.3 (2019-08-15) ------------------- - -* Added support for 'API Version' parameter for Client initialisation. - -6.0.4 (2019-08-15) ------------------- - -* Remove auto-deploy to travis - -6.0.5 (2019-08-15) ------------------- - -* allow newer version of requests dependency \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 7f84572..483f23c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ -include CONTRIBUTING.rst -include HISTORY.rst +include HISTORY.md include README.md recursive-include tests * diff --git a/Makefile b/Makefile index 68d2ee3..62074a7 100644 --- a/Makefile +++ b/Makefile @@ -1,81 +1,4 @@ -.PHONY: clean clean-test clean-pyc clean-build docs help -.DEFAULT_GOAL := help -define BROWSER_PYSCRIPT -import os, webbrowser, sys -try: - from urllib import pathname2url -except: - from urllib.request import pathname2url - -webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) -endef -export BROWSER_PYSCRIPT - -define PRINT_HELP_PYSCRIPT -import re, sys - -for line in sys.stdin: - match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) - if match: - target, help = match.groups() - print("%-20s %s" % (target, help)) -endef -export PRINT_HELP_PYSCRIPT -BROWSER := python -c "$$BROWSER_PYSCRIPT" - -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) - -clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts - - -clean-build: ## remove build artifacts - rm -fr build/ - rm -fr dist/ - rm -fr .eggs/ - find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + - -clean-pyc: ## remove Python file artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: ## remove test and coverage artifacts - rm -fr .tox/ - rm -f .coverage - rm -fr htmlcov/ - rm -f test_readme.py - - -lint: ## check style with flake8 - flake8 python_simple_trustpilo_api_client tests - -test: ## run tests quickly with the default Python - py.test - -tox: ## run tests quickly with the default Python - rm -rf .tox - detox - -test-all: clean ## run all versions/tests with dtox (dockerized) - @echo "Running all tests in Docker using dtox" - rm -rf .tox - docker run --rm -it -v "$$PWD":/src:ro realcundo/dtox "$$PWD" - -coverage: ## check code coverage quickly with the default Python - coverage run --source python_simple_trustpilo_api_client -m pytest - - coverage report -m - coverage html - $(BROWSER) htmlcov/index.html - -release: clean ## package and upload a release - python setup.py sdist upload - python setup.py bdist_wheel upload - -dist: clean ## builds source and wheel package - python setup.py sdist - python setup.py bdist_wheel - ls -l dist +release: + rm dist/* + python setup.py sdist build + python -m twine upload dist/* \ No newline at end of file diff --git a/README.md b/README.md index 1bcfc24..f94e298 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,14 @@ async def get_response(): loop.run_until_complete(get_response()) ``` +## Setup User Agent + +A UserAgent header can be specified in two ways: + +1. By populating the `TRUSTPILOT_USER_AGENT` environment var +2. By creating your own (async/sync)-client instance, or calling `setup` on the `default_session`, and supplying the kwargs `user_agent=foobar` + +If no user-agent is given it will autopopulate using the function in `get_user_agent` function in [auth.py](./trustpilot/auth.py) ## CLI diff --git a/conftest.py b/conftest.py index 9f1b89c..ac33099 100644 --- a/conftest.py +++ b/conftest.py @@ -9,4 +9,5 @@ collect_ignore.append("trustpilot/async_client.py") from pytest_readme import setup + setup() diff --git a/setup.py b/setup.py index 82e9302..71f52c0 100644 --- a/setup.py +++ b/setup.py @@ -5,77 +5,55 @@ import sys import os -try: - from pypandoc import convert - read_md = lambda f: convert(f, 'rst') -except ImportError: - print("warning: pypandoc module not found, could not convert Markdown to RST") - read_md = lambda f: open(f, 'r').read() +readme = open("README.md", "r").read() -readme = read_md('README.md') +history = open("HISTORY.md").read() -with open('HISTORY.rst') as history_file: - history = history_file.read() - -requirements = [ - 'Click==6.7', - 'requests>=2.20.0', -] +requirements = ["Click==6.7", "requests>=2.20.0"] test_requirements = [ - 'responses==0.6', - 'mock', - 'pytest==3.2.0', - 'pytest-readme==1.0.0' + "responses==0.6", + "mock", + "pytest==3.2.0", + "pytest-readme==1.0.0", ] + requirements -if sys.version_info >= (3,5): - test_requirements.append('aioresponses==0.3.1') +if sys.version_info >= (3, 5): + test_requirements.append("aioresponses==0.3.1") -async_requirements = [ - 'aiohttp==2.3.9' -] + requirements +async_requirements = ["aiohttp==2.3.9"] + requirements -extras = { - 'test' : test_requirements, - 'async' : async_requirements -} +extras = {"test": test_requirements, "async": async_requirements} # get version metadata = {} -version_filename = os.path.join(os.path.dirname(__file__), 'trustpilot','__init__.py') +version_filename = os.path.join(os.path.dirname(__file__), "trustpilot", "__init__.py") exec(open(version_filename).read(), None, metadata) setup( - name='trustpilot', - version=metadata['__version__'], + name="trustpilot", + version=metadata["__version__"], description="trustpilot api client including cli tool", - long_description=readme + '\n\n' + history, - author="jgv", - author_email='jgv@trustpilot.com', - url='https://github.com/trustpilot/python-trustpilot', - packages=[ - 'trustpilot', - ], - package_dir={'trustpilot': - 'trustpilot'}, - entry_points={ - 'console_scripts': [ - 'trustpilot_api_client=trustpilot.cli:cli' - ] - }, + long_description=readme + "\n\n" + history, + long_description_content_type="text/markdown", + author=metadata["__author__"], + author_email=metadata["__email__"], + url="https://github.com/trustpilot/python-trustpilot", + packages=["trustpilot"], + package_dir={"trustpilot": "trustpilot"}, + entry_points={"console_scripts": ["trustpilot_api_client=trustpilot.cli:cli"]}, include_package_data=True, install_requires=requirements, zip_safe=False, - keywords='trustpilot api client', + keywords="trustpilot api client", classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", ], - test_suite='tests', + test_suite="tests", tests_require=test_requirements, - extras_require=extras + extras_require=extras, ) diff --git a/tests/test_async.py b/tests/test_async.py index 1410d7e..e1745ba 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -2,8 +2,10 @@ import pytest import sys -@pytest.mark.skipif(sys.version_info < (3, 5, 2), - reason="requires python 3.5.2 or above") + +@pytest.mark.skipif( + sys.version_info < (3, 5, 2), reason="requires python 3.5.2 or above" +) def test_async_client_auth_and_get(): import os from aioresponses import aioresponses @@ -13,36 +15,26 @@ def test_async_client_auth_and_get(): loop = asyncio.get_event_loop() with aioresponses() as m: - m.get( - 'https://api.tp-staging.com/v1/foo/bar', - status=401 - ) + m.get("https://api.tp-staging.com/v1/foo/bar", status=401) m.post( - 'https://api.tp-staging.com/v1/oauth/oauth-business-users-for-applications/accesstoken', - payload=dict( - access_token="foobarbaz" - ) - ) - m.get( - 'https://api.tp-staging.com/v1/foo/bar', - payload=dict( - foo='foobarbaz' - ) + "https://api.tp-staging.com/v1/oauth/oauth-business-users-for-applications/accesstoken", + payload=dict(access_token="foobarbaz"), ) + m.get("https://api.tp-staging.com/v1/foo/bar", payload=dict(foo="foobarbaz")) session = async_client.TrustpilotAsyncSession( - api_host='https://api.tp-staging.com', - api_key='something', - api_secret='secret', - username='username', - password='password', - api_version='v1' + api_host="https://api.tp-staging.com", + api_key="something", + api_secret="secret", + username="username", + password="password", + api_version="v1", ) async def get_response(): - response = await session.get('/foo/bar') + response = await session.get("/foo/bar") response_json = await response.json() - assert response_json['foo'] == 'foobarbaz' - assert session.access_token == 'foobarbaz' + assert response_json["foo"] == "foobarbaz" + assert session.access_token == "foobarbaz" resp = loop.run_until_complete(get_response()) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5d066b5..83b27e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,6 @@ import json from click.testing import CliRunner + try: from unittest import mock except ImportError: @@ -12,13 +13,19 @@ _creds_list = [ - "--host", "http://hostname", - "--key", "secret_key", - "--secret", "secret_secret", - "--username", "username", - "--password", "password", + "--host", + "http://hostname", + "--key", + "secret_key", + "--secret", + "secret_secret", + "--username", + "username", + "--password", + "password", ] + class TestCliMethods(unittest.TestCase): def setUp(self): self.runner = CliRunner() @@ -26,26 +33,24 @@ def setUp(self): response_mock.url = "https://api.trustpilot.com/v1/business-units/5400267300006400057a0113/reviews" response_mock.status_code = 401 response_mock.headers = { - "Access-Control-Allow-Headers": "Authorization, Accept, Accept-Charset, Accept-Encoding, Accept-Language, Cache-Control, Connection, Content-Length, Content-Type, Host, Origin, User-Agent, ApiKey, X-Requested-With", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Origin": "*", - "Access-Control-Max-Age": "3628800", - "Content-Type": "application/json", - "Date": "Mon, 30 Jan 2017 08:59:49 GMT", - "Server": "Apigee Router", - "Content-Length": "90", - "Connection": "keep-alive" - } + "Access-Control-Allow-Headers": "Authorization, Accept, Accept-Charset, Accept-Encoding, Accept-Language, Cache-Control, Connection, Content-Length, Content-Type, Host, Origin, User-Agent, ApiKey, X-Requested-With", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Origin": "*", + "Access-Control-Max-Age": "3628800", + "Content-Type": "application/json", + "Date": "Mon, 30 Jan 2017 08:59:49 GMT", + "Server": "Apigee Router", + "Content-Length": "90", + "Connection": "keep-alive", + } response_mock.json.return_value = { - "fault": { - "faultstring": "Invalid ApiKey", - "detail": { - "errorcode": "oauth.v2.InvalidApiKey" - } - } + "fault": { + "faultstring": "Invalid ApiKey", + "detail": {"errorcode": "oauth.v2.InvalidApiKey"}, } + } self.response_mock = response_mock - self.expected_output = u'''{ + self.expected_output = u"""{ "url": "https://api.trustpilot.com/v1/business-units/5400267300006400057a0113/reviews", "status": 401, "content": { @@ -57,8 +62,8 @@ def setUp(self): } } } - ''' - self.expected_verbose_output = u'''{ + """ + self.expected_verbose_output = u"""{ "url": "https://api.trustpilot.com/v1/business-units/5400267300006400057a0113/reviews", "status": 401, "headers": { @@ -81,7 +86,7 @@ def setUp(self): } } } - ''' + """ def assert_output_equal(self, output, expected_output): output = "".join(output.split()) @@ -91,7 +96,7 @@ def assert_output_equal(self, output, expected_output): expected_output = json.loads(expected_output) except ValueError: pass - assert output == expected_output + assert output == expected_output @mock.patch("trustpilot.cli.client", autospec=True) def test_create_access_token(self, client_mock): @@ -105,8 +110,11 @@ def test_no_verbosity_with_get(self, auth_mock, client_mock): client_mock.get.return_value = self.response_mock - result = self.runner.invoke(cli, _creds_list + [ - "get", "/v1/business-units/5400267300006400057a0113/reviews"]) + result = self.runner.invoke( + cli, + _creds_list + + ["get", "/v1/business-units/5400267300006400057a0113/reviews"], + ) self.assert_output_equal(result.output, self.expected_output) @@ -115,48 +123,60 @@ def test_no_verbosity_with_get(self, auth_mock, client_mock): def test_low_verbosity_with_get(self, auth_mock, client_mock): client_mock.get.return_value = self.response_mock - result = self.runner.invoke(cli, _creds_list + [ - "-v", - "get", - "/v1/business-units/5400267300006400057a0113/reviews" - ]) + result = self.runner.invoke( + cli, + _creds_list + + ["-v", "get", "/v1/business-units/5400267300006400057a0113/reviews"], + ) self.assert_output_equal(result.output, self.expected_verbose_output) - @mock.patch("trustpilot.cli.client", autospec=True) @mock.patch("trustpilot.cli.auth") def test_no_verbosity_with_post(self, auth_mock, client_mock): client_mock.post.return_value = self.response_mock - result = self.runner.invoke(cli, _creds_list + [ - "post", - "/v1/business-units/5400267300006400057a0113/reviews", - "--data", - '{"foo": "bar"}', - "--content_type", "application/json"]) + result = self.runner.invoke( + cli, + _creds_list + + [ + "post", + "/v1/business-units/5400267300006400057a0113/reviews", + "--data", + '{"foo": "bar"}', + "--content_type", + "application/json", + ], + ) self.assert_output_equal(result.output, self.expected_output) - @mock.patch("trustpilot.cli.client", autospec=True) @mock.patch("trustpilot.cli.auth") def test_no_verbosity_with_put(self, auth_mock, client_mock): client_mock.put.return_value = self.response_mock - result = self.runner.invoke(cli, _creds_list + [ - "put", - "/v1/business-units/5400267300006400057a0113/reviews", - "--data", - '{"foo": "bar"}', - "--content_type", "application/json"]) + result = self.runner.invoke( + cli, + _creds_list + + [ + "put", + "/v1/business-units/5400267300006400057a0113/reviews", + "--data", + '{"foo": "bar"}', + "--content_type", + "application/json", + ], + ) self.assert_output_equal(result.output, self.expected_output) - @mock.patch("trustpilot.cli.client", autospec=True) @mock.patch("trustpilot.cli.auth") def test_no_verbosity_with_delete(self, auth_mock, client_mock): client_mock.delete.return_value = self.response_mock - result = self.runner.invoke(cli, _creds_list + [ - "delete", "/v1/business-units/5400267300006400057a0113/reviews"]) + result = self.runner.invoke( + cli, + _creds_list + + ["delete", "/v1/business-units/5400267300006400057a0113/reviews"], + ) self.assert_output_equal(result.output, self.expected_output) diff --git a/tests/test_client.py b/tests/test_client.py index 6edd389..0597870 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,9 +8,11 @@ from trustpilot import client + def assert_not_called(mock): assert mock.call_count == 0 + def assert_called_once(mock): assert mock.call_count == 1 @@ -20,17 +22,20 @@ def setUp(self): self.api_host = "https://hostname.com" self.api_key = "secret_api_key" self.api_secret = "secret_api_secret" - self.token_issuer_path = "oauth/oauth-business-users-for-applications/accesstoken" + self.token_issuer_path = ( + "oauth/oauth-business-users-for-applications/accesstoken" + ) self.token_issuer_host = "https://hostname.com" self.username = "username" self.password = "password" - self.api_version='v1' + self.api_version = "v1" self.request_url = "/this/1" self.exp_headers = { - 'apikey': 'secret_api_key', - 'Authorization': 'Bearer access_token'} + "apikey": "secret_api_key", + "Authorization": "Bearer access_token", + } @property def session(self): @@ -42,7 +47,7 @@ def session(self): token_issuer_host=self.token_issuer_host, username=self.username, password=self.password, - api_version=self.api_version + api_version=self.api_version, ) return session @@ -56,51 +61,46 @@ def test_deprecated_create_session(self, auth_mock): token_issuer_host=self.token_issuer_host, username=self.username, password=self.password, - api_version=self.api_version + api_version=self.api_version, ) - for attr in ["api_key", - "api_secret", - "api_host", - "token_issuer_path", - "token_issuer_host", - "username", - "password", - "api_version"]: + for attr in [ + "api_key", + "api_secret", + "api_host", + "token_issuer_path", + "token_issuer_host", + "username", + "password", + "api_version", + ]: assert getattr(self, attr) == getattr(session, attr) @responses.activate def test_get_public_endpoint_with_apikey_and_no_access_token(self): - with responses.RequestsMock( - assert_all_requests_are_fired=True) as rsps: + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="bar", status=200 + responses.GET, "https://hostname.com/v1/this/1", body="bar", status=200 ) session = client.default_session.setup(api_host=self.api_host) response = session.get(self.request_url) - assert response.text == 'bar' + assert response.text == "bar" assert response.status_code == 200 @responses.activate def test_request_renew_auth_token_success(self): - with responses.RequestsMock( - assert_all_requests_are_fired=True) as rsps: + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add( responses.POST, - 'https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken', - body='{"access_token":"access_token"}', status=200 + "https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken", + body='{"access_token":"access_token"}', + status=200, ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="foo", status=401 + responses.GET, "https://hostname.com/v1/this/1", body="foo", status=401 ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="bar", status=200 + responses.GET, "https://hostname.com/v1/this/1", body="bar", status=200 ) session = self.session @@ -113,50 +113,39 @@ def test_request_renew_auth_token_success(self): @responses.activate def test_request_renew_auth_token_fail(self): - with responses.RequestsMock( - assert_all_requests_are_fired=True) as rsps: + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add( responses.POST, - 'https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken', - body='go away', status=401 + "https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken", + body="go away", + status=401, ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="bar", - status=401 + responses.GET, "https://hostname.com/v1/this/1", body="bar", status=401 ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="foo", - status=401 + responses.GET, "https://hostname.com/v1/this/1", body="foo", status=401 ) - + session = self.session response = session.get(self.request_url) assert response.text == "foo" - @responses.activate def test_no_hooks(self): - with responses.RequestsMock( - assert_all_requests_are_fired=True) as rsps: + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add( responses.POST, - 'https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken', - body='{"access_token":"access_token"}', status=200 + "https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken", + body='{"access_token":"access_token"}', + status=200, ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="foo", status=401 + responses.GET, "https://hostname.com/v1/this/1", body="foo", status=401 ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="bar", status=200 + responses.GET, "https://hostname.com/v1/this/1", body="bar", status=200 ) session = self.session hook_mock = mock.Mock() @@ -168,22 +157,18 @@ def test_no_hooks(self): @responses.activate def test_pre_hook(self): - with responses.RequestsMock( - assert_all_requests_are_fired=True) as rsps: + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add( responses.POST, - 'https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken', - body='{"access_token":"access_token"}', status=200 + "https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken", + body='{"access_token":"access_token"}', + status=200, ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="foo", status=401 + responses.GET, "https://hostname.com/v1/this/1", body="foo", status=401 ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="bar", status=200 + responses.GET, "https://hostname.com/v1/this/1", body="bar", status=200 ) session = self.session hook_mock = mock.Mock() @@ -197,22 +182,18 @@ def test_pre_hook(self): @responses.activate def test_post_hook(self): - with responses.RequestsMock( - assert_all_requests_are_fired=True) as rsps: + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add( responses.POST, - 'https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken', - body='{"access_token":"access_token"}', status=200 + "https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken", + body='{"access_token":"access_token"}', + status=200, ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="foo", status=401 + responses.GET, "https://hostname.com/v1/this/1", body="foo", status=401 ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="bar", status=200 + responses.GET, "https://hostname.com/v1/this/1", body="bar", status=200 ) session = self.session hook_mock = mock.Mock() @@ -224,22 +205,18 @@ def test_post_hook(self): @responses.activate def test_pre_and_post_hooks(self): - with responses.RequestsMock( - assert_all_requests_are_fired=True) as rsps: + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add( responses.POST, - 'https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken', - body='{"access_token":"access_token"}', status=200 + "https://hostname.com/v1/oauth/oauth-business-users-for-applications/accesstoken", + body='{"access_token":"access_token"}', + status=200, ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="foo", status=401 + responses.GET, "https://hostname.com/v1/this/1", body="foo", status=401 ) rsps.add( - responses.GET, - 'https://hostname.com/v1/this/1', - body="bar", status=200 + responses.GET, "https://hostname.com/v1/this/1", body="bar", status=200 ) session = self.session hook_mock = mock.Mock() diff --git a/trustpilot/__init__.py b/trustpilot/__init__.py index 2c37ee9..72565bf 100644 --- a/trustpilot/__init__.py +++ b/trustpilot/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__author__ = """sloev""" -__email__ = 'jgv@trustpilot.com' -__version__ = '6.0.5' +__author__ = "sloev" +__email__ = "jgv@trustpilot.com" +__version__ = "6.0.6" diff --git a/trustpilot/async_client.py b/trustpilot/async_client.py index 1964054..1948cc5 100644 --- a/trustpilot/async_client.py +++ b/trustpilot/async_client.py @@ -3,8 +3,7 @@ try: import aiohttp except ImportError: - raise RuntimeError( - "aiohttp is not installed, possibly running in python27?") + raise RuntimeError("aiohttp is not installed, possibly running in python27?") from logging import getLogger logger = getLogger("trustpilot.async_client") @@ -14,45 +13,56 @@ from trustpilot import auth -class TrustpilotAsyncSession(): - __SUPPORTED_HTTP_METHODS = [ - 'post', - 'get', - 'put', - 'delete' - ] +class TrustpilotAsyncSession: + __SUPPORTED_HTTP_METHODS = ["post", "get", "put", "delete"] def __init__(self, *args, **kwargs): self.setup(**kwargs) self.headers = {} - def setup(self, api_host=None, api_version=None, api_key=None, api_secret=None, - username=None, password=None, - access_token=None, token_issuer_path=None, - token_issuer_host=None, **kwargs): - - self.api_version = api_version or environ.get('TRUSTPILOT_API_VERSION', 'v1') - self.api_host = api_host or environ.get('TRUSTPILOT_API_HOST', 'https://api.trustpilot.com') + def setup( + self, + api_host=None, + api_version=None, + api_key=None, + api_secret=None, + username=None, + password=None, + access_token=None, + token_issuer_path=None, + token_issuer_host=None, + user_agent=None, + **kwargs + ): + + self.api_version = api_version or environ.get("TRUSTPILOT_API_VERSION", "v1") + self.api_host = api_host or environ.get( + "TRUSTPILOT_API_HOST", "https://api.trustpilot.com" + ) self.token_issuer_host = token_issuer_host or self.api_host self.access_token = access_token self.token_issuer_path = token_issuer_path or environ.get( - 'TRUSTPILOT_API_TOKEN_ISSUER_PATH', "oauth/oauth-business-users-for-applications/accesstoken") + "TRUSTPILOT_API_TOKEN_ISSUER_PATH", + "oauth/oauth-business-users-for-applications/accesstoken", + ) self.hooks = dict() + self.user_agent = user_agent or environ.get( + "TRUSTPILOT_USER_AGENT", auth.get_user_agent() + ) if not self.api_host.startswith("http"): raise aiohttp.http_exceptions.InvalidURLError( - "'{}' is not a valid api_host url".format(api_host)) + "'{}' is not a valid api_host url".format(api_host) + ) try: - self.api_key = api_key or environ['TRUSTPILOT_API_KEY'] - self.api_secret = api_secret or environ.get( - 'TRUSTPILOT_API_SECRET', '') - self.username=username or environ.get('TRUSTPILOT_USERNAME') - self.password=password or environ.get('TRUSTPILOT_PASSWORD') + self.api_key = api_key or environ["TRUSTPILOT_API_KEY"] + self.api_secret = api_secret or environ.get("TRUSTPILOT_API_SECRET", "") + self.username = username or environ.get("TRUSTPILOT_USERNAME") + self.password = password or environ.get("TRUSTPILOT_PASSWORD") self.access_token = access_token except KeyError as e: - logger.debug( - "Not auth setup, missing env-var or setup for {}".format(e)) + logger.debug("Not auth setup, missing env-var or setup for {}".format(e)) return self @@ -62,16 +72,18 @@ async def get_request_auth_headers(self): async with session.post(url, data=data, headers=headers) as response: response_json = await response.json() self.access_token = response_json["access_token"] - self.headers.update(dict( - Authorization="Bearer {}".format(self.access_token), - apikey=self.api_key - )) + self.headers.update( + dict( + Authorization="Bearer {}".format(self.access_token), + apikey=self.api_key, + ) + ) async def authenticated_request(self, method, url, **kwargs): if method not in self.__SUPPORTED_HTTP_METHODS: raise RuntimeError("Http method {} not supported".format(method)) if not any(prefix in url for prefix in ["http://", "https://"]): - url = "{}/{}{}".format(self.api_host.rstrip('/'), self.api_version, url) + url = "{}/{}{}".format(self.api_host.rstrip("/"), self.api_version, url) async with aiohttp.ClientSession(headers=self.headers) as session: http_method = getattr(session, method) @@ -85,17 +97,17 @@ async def authenticated_request(self, method, url, **kwargs): return response async def post(self, url, *args, **kwargs): - return await self.authenticated_request('post', url, *args, **kwargs) + return await self.authenticated_request("post", url, *args, **kwargs) async def get(self, url, *args, **kwargs): - return await self.authenticated_request('get', url, *args, **kwargs) + return await self.authenticated_request("get", url, *args, **kwargs) async def put(self, url, *args, **kwargs): - return await self.authenticated_request('put', url, *args, **kwargs) + return await self.authenticated_request("put", url, *args, **kwargs) async def delete(self, url, *args, **kwargs): - return await self.authenticated_request('delete', url, *args, **kwargs) + return await self.authenticated_request("delete", url, *args, **kwargs) default_session = TrustpilotAsyncSession() diff --git a/trustpilot/auth.py b/trustpilot/auth.py index aa0373b..2bf062c 100644 --- a/trustpilot/auth.py +++ b/trustpilot/auth.py @@ -7,37 +7,35 @@ OS = platform.system() PYTHON_VERSION = platform.python_version() -SCOPE = 'external' +SCOPE = "external" def get_user_agent(): user_agent = "python-trustpilot-client?scope={scope}&version={version}&python-version={python_version}&os={os}".format( - scope=SCOPE, - version=VERSION, - python_version=PYTHON_VERSION, - os=OS + scope=SCOPE, version=VERSION, python_version=PYTHON_VERSION, os=OS ) return user_agent def create_access_token_request_params(session): url = "{token_issuer_host}/{api_version}/{token_issuer_path}".format( - token_issuer_host=session.token_issuer_host.rstrip('/'), - api_version=session.api_version.rstrip('/'), - token_issuer_path=session.token_issuer_path + token_issuer_host=session.token_issuer_host.rstrip("/"), + api_version=session.api_version.rstrip("/"), + token_issuer_path=session.token_issuer_path, ) data = { "grant_type": "password", "username": session.username, - "password": session.password + "password": session.password, } - + headers = { - "Authorization": "Basic {}".format(base64.b64encode( - (session.api_key + ":" + session.api_secret - ).encode("ascii")).decode("ascii") + "Authorization": "Basic {}".format( + base64.b64encode( + (session.api_key + ":" + session.api_secret).encode("ascii") + ).decode("ascii") ), - 'User-Agent': get_user_agent() + "User-Agent": session.user_agent, } return url, data, headers diff --git a/trustpilot/cli.py b/trustpilot/cli.py index bd9c3c2..97b7ee8 100644 --- a/trustpilot/cli.py +++ b/trustpilot/cli.py @@ -39,25 +39,37 @@ def format_response(response): @click.group(invoke_without_command=True) @click.pass_context -@click.option('--host', type=str, help="host name", - envvar='TRUSTPILOT_API_HOST') -@click.option('--version', type=str, help="api version (e.g. v1)", - envvar='TRUSTPILOT_API_VERSION') -@click.option('--key', type=str, help="api key", - envvar='TRUSTPILOT_API_KEY') -@click.option('--secret', type=str, help="api secret", - envvar='TRUSTPILOT_API_SECRET') -@click.option('--token_issuer_host', type=str, default="", - help="token issuer host name", - envvar='TRUSTPILOT_API_TOKEN_ISSUER_HOST') -@click.option('--username', type=str, default="", help="Trustpilot username", - envvar='TRUSTPILOT_USERNAME') -@click.option('--password', type=str, default="", help="Trustpilot password", - envvar='TRUSTPILOT_PASSWORD') -@click.option('-c', type=str, help="json config file name") -@click.option('-v', '--verbose', count=True, help='verbosity level') +@click.option("--host", type=str, help="host name", envvar="TRUSTPILOT_API_HOST") +@click.option( + "--version", type=str, help="api version (e.g. v1)", envvar="TRUSTPILOT_API_VERSION" +) +@click.option("--key", type=str, help="api key", envvar="TRUSTPILOT_API_KEY") +@click.option("--secret", type=str, help="api secret", envvar="TRUSTPILOT_API_SECRET") +@click.option( + "--token_issuer_host", + type=str, + default="", + help="token issuer host name", + envvar="TRUSTPILOT_API_TOKEN_ISSUER_HOST", +) +@click.option( + "--username", + type=str, + default="", + help="Trustpilot username", + envvar="TRUSTPILOT_USERNAME", +) +@click.option( + "--password", + type=str, + default="", + help="Trustpilot password", + envvar="TRUSTPILOT_PASSWORD", +) +@click.option("-c", type=str, help="json config file name") +@click.option("-v", "--verbose", count=True, help="verbosity level") def cli(ctx, **kwargs): - splash = r''' + splash = r""" _____ _ _ _ _ |_ _| | | (_) | | | | |_ __ _ _ ___| |_ _ __ _| | ___ | |_ @@ -73,9 +85,12 @@ def cli(ctx, **kwargs): | | | | |_) | | | \__/\ | | __/ | | | |_ \_| |_/ .__/|_| \____/_|_|\___|_| |_|\__| | | - |_| ''' - splash = click.style(splash, fg='green') + click.style( - "v{}".format(__version__), fg='red') + "\n" + |_| """ + splash = ( + click.style(splash, fg="green") + + click.style("v{}".format(__version__), fg="red") + + "\n" + ) values_dict = {} config_filename = kwargs.pop("c") @@ -93,10 +108,7 @@ def cli(ctx, **kwargs): # v : headers # vv: logging.INFO level # vvv: logging.DEBUG level - levels = { - 2: logging.INFO, - 3: logging.DEBUG - } + levels = {2: logging.INFO, 3: logging.DEBUG} logging_level = levels.get(verbosity, logging.CRITICAL) logger.setLevel(logging_level) @@ -111,86 +123,93 @@ def cli(ctx, **kwargs): # create default session try: client.create_session( - api_host=kwargs.pop("host") or values_dict.get( - "TRUSTPILOT_API_HOST") or "https://api.tp-staging.com", - api_version=kwargs.pop("version") or values_dict.get( - "TRUSTPILOT_API_VERSION") or "v1", + api_host=kwargs.pop("host") + or values_dict.get("TRUSTPILOT_API_HOST") + or "https://api.tp-staging.com", + api_version=kwargs.pop("version") + or values_dict.get("TRUSTPILOT_API_VERSION") + or "v1", api_key=kwargs.pop("key") or values_dict["TRUSTPILOT_API_KEY"], - api_secret=(kwargs.pop("secret") - or values_dict.get("TRUSTPILOT_API_SECRET", None)), - token_issuer_host=(kwargs.pop("token_issuer_host") or - values_dict.get( - "TRUSTPILOT_API_TOKEN_ISSUER_HOST", None)), + api_secret=( + kwargs.pop("secret") or values_dict.get("TRUSTPILOT_API_SECRET", None) + ), + token_issuer_host=( + kwargs.pop("token_issuer_host") + or values_dict.get("TRUSTPILOT_API_TOKEN_ISSUER_HOST", None) + ), username=kwargs.pop("username") or values_dict["TRUSTPILOT_USERNAME"], - password=kwargs.pop("password") or values_dict["TRUSTPILOT_PASSWORD"] + password=kwargs.pop("password") or values_dict["TRUSTPILOT_PASSWORD"], ) except KeyError as key: raise SystemExit("Missing argument: {}".format(key)) -cli_command = cli.command(context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True -)) +cli_command = cli.command( + context_settings=dict(ignore_unknown_options=True, allow_extra_args=True) +) @cli_command def create_access_token(): - ''' + """ Get an access token - ''' + """ client.default_session.get_request_auth_headers() click.echo(client.default_session.access_token) @cli_command -@click.argument('path') +@click.argument("path") def get(path): - ''' + """ Send a GET request - ''' + """ response = client.get(url=path) click.echo(format_response(response)) @cli_command -@click.argument('path') -@click.option('--data', type=str, help="json_data to post") -@click.option('--content-type', type=str, default="application/json", - help="content-type, default=application/json") +@click.argument("path") +@click.option("--data", type=str, help="json_data to post") +@click.option( + "--content-type", + type=str, + default="application/json", + help="content-type, default=application/json", +) def post(path, data, content_type): - ''' + """ Send a POST request with specified data - ''' - headers = { - 'content-type': content_type - } + """ + headers = {"content-type": content_type} response = client.post(url=path, data=data, headers=headers) click.echo(format_response(response)) @cli_command -@click.argument('path') +@click.argument("path") def delete(path): - ''' + """ Send a DELETE request - ''' + """ response = client.delete(url=path) click.echo(format_response(response)) @cli_command -@click.argument('path') -@click.option('--data', type=str, help="json_data to post") -@click.option('--content-type', type=str, default="application/json", - help="content-type, default=application/json") +@click.argument("path") +@click.option("--data", type=str, help="json_data to post") +@click.option( + "--content-type", + type=str, + default="application/json", + help="content-type, default=application/json", +) def put(path, data, content_type): - ''' + """ Send a PUT request with specified data - ''' - headers = { - 'content-type': content_type - } + """ + headers = {"content-type": content_type} response = client.put(url=path, data=data, headers=headers) click.echo(format_response(response)) diff --git a/trustpilot/client.py b/trustpilot/client.py index 2ce3768..c3e94da 100644 --- a/trustpilot/client.py +++ b/trustpilot/client.py @@ -13,13 +13,17 @@ def disable_ssl_warnings(): try: import requests.packages.urllib3 - urllib3_logger = logging.getLogger('requests') + + urllib3_logger = logging.getLogger("requests") urllib3_logger.setLevel(logging.WARNING) urllib3_logger.propagate = False requests.packages.urllib3.disable_warnings() - logger.info({ - "message": "Ssl warnings from urllib3 disabled! " - "(info: http://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings)"}) + logger.info( + { + "message": "Ssl warnings from urllib3 disabled! " + "(info: http://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings)" + } + ) except ImportError: logger.error("Error importing urllib3 when disabling its logging") @@ -32,30 +36,48 @@ def __init__(self, **kwargs): self._post_hooks = [] self.auth = self._pre_request_callback - def setup(self, api_host=None, api_version=None, api_key=None, api_secret=None, - username=None, password=None, - access_token=None, token_issuer_path=None, - token_issuer_host=None, **kwargs): - - self.api_version = api_version or environ.get('TRUSTPILOT_API_VERSION', 'v1') - self.api_host = api_host or environ.get('TRUSTPILOT_API_HOST', 'https://api.trustpilot.com') + def setup( + self, + api_host=None, + api_version=None, + api_key=None, + api_secret=None, + username=None, + password=None, + access_token=None, + token_issuer_path=None, + token_issuer_host=None, + user_agent=None, + **kwargs + ): + + self.api_version = api_version or environ.get("TRUSTPILOT_API_VERSION", "v1") + self.api_host = api_host or environ.get( + "TRUSTPILOT_API_HOST", "https://api.trustpilot.com" + ) self.token_issuer_host = token_issuer_host or self.api_host self.access_token = access_token self.token_issuer_path = token_issuer_path or environ.get( - 'TRUSTPILOT_API_TOKEN_ISSUER_PATH', "oauth/oauth-business-users-for-applications/accesstoken") + "TRUSTPILOT_API_TOKEN_ISSUER_PATH", + "oauth/oauth-business-users-for-applications/accesstoken", + ) self.hooks = dict() + self.user_agent = user_agent or environ.get( + "TRUSTPILOT_USER_AGENT", auth.get_user_agent() + ) if not self.api_host.startswith("http"): raise requests.URLRequired( - "'{}' is not a valid api_host url".format(api_host)) + "'{}' is not a valid api_host url".format(api_host) + ) try: - self.api_key=api_key or environ['TRUSTPILOT_API_KEY'] - self.api_secret=api_secret or environ.get('TRUSTPILOT_API_SECRET', '') - self.username=username or environ.get('TRUSTPILOT_USERNAME') - self.password=password or environ.get('TRUSTPILOT_PASSWORD') - self.access_token=access_token - self.hooks['response'] = self._post_request_callback + self.api_key = api_key or environ["TRUSTPILOT_API_KEY"] + self.api_secret = api_secret or environ.get("TRUSTPILOT_API_SECRET", "") + self.username = username or environ.get("TRUSTPILOT_USERNAME") + self.password = password or environ.get("TRUSTPILOT_PASSWORD") + self.access_token = access_token + self.hooks["response"] = self._post_request_callback except KeyError as e: logger.debug("Not auth setup, missing env-var or setup for {}".format(e)) @@ -70,10 +92,11 @@ def get_request_auth_headers(self): response_json = response.json() self.access_token = response_json["access_token"] - self.headers.update(dict( - Authorization="Bearer {}".format(self.access_token), - apikey=self.api_key - )) + self.headers.update( + dict( + Authorization="Bearer {}".format(self.access_token), apikey=self.api_key + ) + ) return self.headers def _pre_request_callback(self, request): @@ -86,10 +109,9 @@ def _post_request_callback(self, response, *args, **kwargs): retry = getattr(req, "authentication_retry", True) if retry and response.status_code == requests.codes.unauthorized: - logger.debug({ - "message":"reauthenticating and retrying once", - "url": req.url - }) + logger.debug( + {"message": "reauthenticating and retrying once", "url": req.url} + ) req.authentication_retry = False req.headers.update(self.get_request_auth_headers()) response = self.send(req) @@ -107,24 +129,36 @@ def register_post_hook(self, hook): def request(self, method, url, **kwargs): # pylint: disable=W0221 if not any(prefix in url for prefix in ["http://", "https://"]): - url = "{}/{}{}".format(self.api_host.rstrip('/'), self.api_version, url) + url = "{}/{}{}".format(self.api_host.rstrip("/"), self.api_version, url) return super(TrustpilotSession, self).request(method, url, **kwargs) def get_session(): - warn("'trustpilot.client.get_session' is deprecated!, " - "use trustpilot.client.default_session instead", - DeprecationWarning) + warn( + "'trustpilot.client.get_session' is deprecated!, " + "use trustpilot.client.default_session instead", + DeprecationWarning, + ) return default_session -def create_session(api_host=None, api_version=None, api_key=None, api_secret=None, - username=None, password=None, - access_token_path=None, - token_issuer_host=None, access_token=None): - warn("'trustpilot.client.create_session' is deprecated!, " - "use trustpilot.client.default_session.setup instead", - DeprecationWarning) +def create_session( + api_host=None, + api_version=None, + api_key=None, + api_secret=None, + username=None, + password=None, + access_token_path=None, + token_issuer_host=None, + access_token=None, + user_agent=None, +): + warn( + "'trustpilot.client.create_session' is deprecated!, " + "use trustpilot.client.default_session.setup instead", + DeprecationWarning, + ) default_session.setup( api_host=api_host, @@ -135,7 +169,8 @@ def create_session(api_host=None, api_version=None, api_key=None, api_secret=Non token_issuer_path=access_token_path, token_issuer_host=token_issuer_host, username=username, - password=password + password=password, + user_agent=None, ) return default_session