diff --git a/.circleci/config.yml b/.circleci/config.yml index ae937f05..d50e1261 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,14 +47,14 @@ jobs: command: pip install --user setuptools tox==2.9.1 tox-virtualenv-no-download - restore_cache: - key: tox-v2-py27-{{ checksum "tox.ini" }} + key: tox-v2-py27-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) - run: name: Install tox things command: if [ ! -d ".tox" ]; then python -m tox -e py27 --notest; fi - save_cache: - key: tox-v2-py27-{{ checksum "tox.ini" }} + key: tox-v2-py27-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) paths: - .tox @@ -82,14 +82,14 @@ jobs: command: pip install --user setuptools tox==2.9.1 tox-virtualenv-no-download - restore_cache: - key: tox-v2-py3-{{ checksum "tox.ini" }} + key: tox-v2-py3-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) - run: name: Install tox things command: if [ ! -d ".tox" ]; then python -m tox -e py3 --notest; fi - save_cache: - key: tox-v2-py3-{{ checksum "tox.ini" }} + key: tox-v2-py3-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) paths: - .tox @@ -114,14 +114,14 @@ jobs: command: pip install --user setuptools tox==2.9.1 tox-virtualenv-no-download - restore_cache: - key: tox-v2-mypy27-{{ checksum "tox.ini" }} + key: tox-v2-mypy27-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) - run: name: Install tox things command: if [ ! -d ".tox" ]; then python -m tox -e mypy27 --notest; fi - save_cache: - key: tox-v2-mypy27-{{ checksum "tox.ini" }} + key: tox-v2-mypy27-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) paths: - .tox @@ -146,14 +146,14 @@ jobs: command: pip install --user setuptools tox==2.9.1 tox-virtualenv-no-download - restore_cache: - key: tox-v2-mypy3-{{ checksum "tox.ini" }} + key: tox-v2-mypy3-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) - run: name: Install tox things command: if [ ! -d ".tox" ]; then python -m tox -e mypy3 --notest; fi - save_cache: - key: tox-v2-mypy3-{{ checksum "tox.ini" }} + key: tox-v2-mypy3-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) paths: - .tox @@ -178,14 +178,14 @@ jobs: command: pip install --user setuptools tox==2.9.1 tox-virtualenv-no-download - restore_cache: - key: tox-v2-lint-{{ checksum "tox.ini" }} + key: tox-v2-lint-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) - run: name: Install tox things command: if [ ! -d ".tox" ]; then python -m tox -e lint --notest; fi - save_cache: - key: tox-v2-lint-{{ checksum "tox.ini" }} + key: tox-v2-lint-{{ checksum "tox.ini" }}-({ checksum "Pipfile.lock" }) paths: - .tox diff --git a/Pipfile b/Pipfile index bbf255be..a85fb6d9 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,7 @@ name = "pypi" [packages] "enum34" = "*" +future = "*" requests = "*" typing = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 46777241..ebb58003 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b15e58ee5ed3eb0e7c7d214179647d93f31b428632de72ce9e13ae8c82e8087b" + "sha256": "23ab54b6bb322eb28afa0b9de0f589f604307bfee704e9e86006560875afd45b" }, "pipfile-spec": 6, "requires": {}, @@ -16,10 +16,10 @@ "default": { "certifi": { "hashes": [ - "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", - "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c", + "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a" ], - "version": "==2018.4.16" + "version": "==2018.10.15" }, "chardet": { "hashes": [ @@ -38,6 +38,13 @@ "index": "pypi", "version": "==1.1.6" }, + "future": { + "hashes": [ + "sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb" + ], + "index": "pypi", + "version": "==0.16.0" + }, "idna": { "hashes": [ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", @@ -55,35 +62,36 @@ }, "typing": { "hashes": [ - "sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf", - "sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8", - "sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2" + "sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", + "sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", + "sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a" ], "index": "pypi", - "version": "==3.6.4" + "version": "==3.6.6" }, "urllib3": { "hashes": [ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" ], + "markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version < '4' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==1.23" } }, "develop": { "astroid": { "hashes": [ - "sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a", - "sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a" + "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", + "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" ], - "version": "==1.6.5" + "version": "==2.0.4" }, "attrs": { "hashes": [ - "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", - "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" ], - "version": "==18.1.0" + "version": "==18.2.0" }, "black": { "hashes": [ @@ -95,10 +103,18 @@ }, "click": { "hashes": [ - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "version": "==6.7" + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", + "version": "==7.0" + }, + "filelock": { + "hashes": [ + "sha256:86fe6af56ae08ebc9c66d54ba3398c35b98916d0862d782b276a65816ff39392", + "sha256:97694f181bdf58f213cca0a7cb556dc7bf90e2f8eb9aa3151260adac56701afb" + ], + "version": "==3.0.9" }, "isort": { "hashes": [ @@ -106,6 +122,7 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" ], + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", "version": "==4.3.4" }, "lazy-object-proxy": { @@ -157,38 +174,31 @@ "index": "pypi", "version": "==2.0.0" }, - "packaging": { - "hashes": [ - "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", - "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b" - ], - "version": "==17.1" - }, "pbr": { "hashes": [ - "sha256:4f2b11d95917af76e936811be8361b2b19616e5ef3b55956a429ec7864378e0c", - "sha256:e0f23b61ec42473723b2fec2f33fb12558ff221ee551962f01dd4de9053c2055" + "sha256:1be135151a0da949af8c5d0ee9013d9eafada71237eb80b3ba8896b4f12ec5dc", + "sha256:cf36765bf2218654ae824ec8e14257259ba44e43b117fd573c8d07a9895adbdd" ], - "version": "==4.1.0" + "version": "==4.3.0" }, "pluggy": { "hashes": [ - "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", - "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", - "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" ], - "version": "==0.6.0" + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", + "version": "==0.8.0" }, "py": { "hashes": [ - "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", - "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" ], - "version": "==1.5.4" + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", + "version": "==1.7.0" }, "pycodestyle": { "hashes": [ - "sha256:74abc4e221d393ea5ce1f129ea6903209940c1ecd29e002e8c6933c2b21026e0", "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" ], @@ -197,23 +207,11 @@ }, "pylint": { "hashes": [ - "sha256:a48070545c12430cfc4e865bf62f5ad367784765681b3db442d8230f0960aa3c", - "sha256:fff220bcb996b4f7e2b0f6812fd81507b72ca4d8c4d05daf2655c333800cb9b3" + "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", + "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" ], "index": "pypi", - "version": "==1.9.2" - }, - "pyparsing": { - "hashes": [ - "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", - "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", - "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", - "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", - "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", - "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58", - "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" - ], - "version": "==2.2.0" + "version": "==2.1.1" }, "pytest": { "hashes": [ @@ -242,10 +240,10 @@ }, "rope": { "hashes": [ - "sha256:a09edfd2034fd50099a67822f9bd851fbd0f4e98d3b87519f6267b60e50d80d1" + "sha256:a108c445e1cd897fe19272ab7877d172e7faf3d4148c80e7d20faba42ea8f7b2" ], "index": "pypi", - "version": "==0.10.7" + "version": "==0.11.0" }, "six": { "hashes": [ @@ -254,28 +252,36 @@ ], "version": "==1.11.0" }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, "tox": { "hashes": [ - "sha256:8df73fb0eae939692d67a095c49081b1afb948eca51879e5dc1868d9b0ad11de", - "sha256:9f09ec569b5019ed030d3ed3d486a9263e8964a9752253a98f5d67b46e954055" + "sha256:217fb84aecf9792a98f93f07cfcaf014205a76c64e52bd7c2b4135458e6ad2a1", + "sha256:4baeb3d8ebdcd9f43afce38aa67d06f1165a87d221d5bb21e8b39a0d4880c134" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.5.2" }, "virtualenv": { "hashes": [ "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" ], + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*'", "version": "==16.0.0" }, "wheel": { "hashes": [ - "sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c", - "sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f" + "sha256:9fa1f772f1a2df2bd00ddb4fa57e1cc349301e1facb98fbe62329803a9ff1196", + "sha256:d215f4520a1ba1851a3c00ba2b4122665cd3d6b0834d2ba2816198b1e3024a0e" ], "index": "pypi", - "version": "==0.31.1" + "version": "==0.32.1" }, "wrapt": { "hashes": [ diff --git a/conjure_python_client/_http/__init__.py b/conjure_python_client/_http/__init__.py index 15361277..5608400a 100644 --- a/conjure_python_client/_http/__init__.py +++ b/conjure_python_client/_http/__init__.py @@ -13,8 +13,9 @@ # limitations under the License. from .configuration import SslConfiguration, ServiceConfiguration -from .requests_client import RequestsClient, Service +from .requests_client import RequestsClient, Service, ConjureHTTPError __all__ = [ - 'SslConfiguration', 'ServiceConfiguration', 'RequestsClient', 'Service' + 'SslConfiguration', 'ServiceConfiguration', 'RequestsClient', 'Service', + 'ConjureHTTPError' ] diff --git a/conjure_python_client/_http/requests_client.py b/conjure_python_client/_http/requests_client.py index 004fc783..959e4765 100644 --- a/conjure_python_client/_http/requests_client.py +++ b/conjure_python_client/_http/requests_client.py @@ -13,12 +13,13 @@ # limitations under the License. from requests.adapters import HTTPAdapter -from typing import TypeVar, Type, List +from typing import TypeVar, Type, List, Optional, Dict from requests.exceptions import HTTPError from requests.packages.urllib3.poolmanager import PoolManager from requests.packages.urllib3.util.ssl_ import create_urllib3_context from requests.packages.urllib3.util import Retry from .configuration import ServiceConfiguration +from future.utils import raise_from import requests import random @@ -76,21 +77,9 @@ def _request(self, *args, **kwargs): try: _response.raise_for_status() except HTTPError as e: - if e.response is not None and e.response.content: - try: - detail = e.response.json() - except ValueError: - detail = {'message': e.response.content} - else: - detail = {} - raise HTTPError( - 'Error Name: {}. Message: {}'.format( - detail.get('errorName', 'UnknownError'), - detail.get('message', 'No Message'), - ), - response=_response, - ) - + if e.response is not None and e.response.content is not None: + raise_from(ConjureHTTPError(e), e) + raise e return _response def __repr__(self): @@ -142,3 +131,81 @@ def init_poolmanager( ssl_context=ssl_context, **pool_kwargs ) + + +class ConjureHTTPError(HTTPError): + """A an HTTPError from a Conjure Service with ``SerializableError`` + attributes extracted from the response.""" + + _cause = None # type: Optional[HTTPError] + _error_code = None # type: str + _error_name = None # type: str + _error_instance_id = None # type: str + _parameters = None # type: Dict[str, str] + _trace_id = None # type: str + + def __init__(self, http_error): + # type (HTTPError) -> None + self._cause = http_error + try: + detail = http_error.response.json() + self._error_code = detail.get("errorCode") + self._error_name = detail.get("errorName") + self._error_instance_id = detail.get("errorInstanceId") + self._parameters = detail.get("parameters", dict()) + self._trace_id = http_error.response.headers.get('X-B3-TraceId') + message = "{}. ErrorCode: '{}'. ErrorName: '{}'. " \ + "ErrorInstanceId: '{}'. TraceId: '{}'. Parameters: {}" \ + .format( + http_error, + self._error_code, + self._error_name, + self._error_instance_id, + self._trace_id, + self._parameters + ) + except ValueError: + message = http_error.response.text + super(ConjureHTTPError, self).__init__( + message, + request=http_error.request, + response=http_error.response + ) + + @property + def cause(self): + # type: () -> Optional[HTTPError] + """The wrapped ``HTTPError`` that was the direct cause of + the ``ConjureHTTPError``. + """ + return self._cause + + @property + def error_code(self): + # type: () -> str + """A fixed code word identifying the type of error.""" + return self._error_code + + @property + def error_name(self): + # type: () -> str + """A fixed name identifying the error.""" + return self._error_name + + @property + def error_instance_id(self): + # type: () -> str + """A unique identifier for this error instance.""" + return self._error_instance_id + + @property + def parameters(self): + # type: () -> Dict[str, str] + """A set of parameters that further explain the error.""" + return self._parameters + + @property + def trace_id(self): + # type: () -> str + """The X-B3-TraceId for the request.""" + return self._trace_id diff --git a/mypy.ini b/mypy.ini index 3856bf22..538ea39e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,3 +2,4 @@ check_untyped_defs = True verbosity = 0 show_column_numbers = True +ignore_missing_imports = True diff --git a/setup.py b/setup.py index 48b7006b..d3f8301a 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ def blackCheck(self): # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html - install_requires=["enum34", "requests", "typing"], + install_requires=["enum34", "future", "requests", "typing"], tests_require=["pytest", "pyyaml"], cmdclass={"format": FormatCommand}, ) diff --git a/test/test_http.py b/test/test_http.py index 0f8e970f..b84097a3 100644 --- a/test/test_http.py +++ b/test/test_http.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from conjure_python_client import RequestsClient, ServiceConfiguration +from conjure_python_client import ConjureHTTPError, RequestsClient, ServiceConfiguration import mock import pytest import requests @@ -64,16 +64,13 @@ def test_http_error(self, mock_request): resp._content = b'{"errorCode":"NOT_FOUND",' \ + b'"errorName":"Default:NotFound",' \ + b'"errorInstanceId":"00000000-0000-0000-0000-000000000000",' \ - + b'"parameters":{},' \ - + b'"exceptionClass":"javax.ws.rs.NotFoundException",' \ - + b'"message":"Refer to the server logs with this errorInstanceId:' \ - + b' 00000000-0000-0000-0000-000000000000"}' + + b'"parameters":{}}' http_error = HTTPError("something", response=resp) mock_request.return_value = self._mock_response( status=404, raise_for_status=http_error) - with pytest.raises(HTTPError) as e: + with pytest.raises(ConjureHTTPError) as e: self._test_service().testEndpoint('foo') assert e.match("Default:NotFound") assert e.match("00000000-0000-0000-0000-000000000000") @@ -88,7 +85,6 @@ def test_http_error_not_json(self, mock_request): status=404, raise_for_status=http_error) - with pytest.raises(HTTPError) as e: + with pytest.raises(ConjureHTTPError) as e: self._test_service().testEndpoint('foo') - assert e.match("UnknownError") assert e.match("Content that's not JSON")