From b362e8e920f90096373134628e5736f338ae248b Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 25 May 2021 16:24:11 -0500 Subject: [PATCH 01/41] Acceptance criteria per DX-1442 - Invalid retries at instantiation --- tests/test_shipengine_config.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index cabc6e2..dfd05f7 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -2,7 +2,8 @@ import pytest from shipengine_sdk import ShipEngineConfig -from shipengine_sdk.errors import ValidationError +from shipengine_sdk.errors import InvalidFieldValueError, ValidationError +from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType from shipengine_sdk.util import api_key_validation_error_assertions @@ -21,12 +22,17 @@ def config_with_whitespace_in_api_key(): return ShipEngineConfig(dict(api_key=" ")) +def config_with_invalid_retries(): + """Return an error from an invalid retry value being passed in""" + return ShipEngineConfig(dict(api_key="baz", retries=-3)) + + class TestShipEngineConfig: def test_no_api_key_provided(self) -> None: """DX-1440 - No API Key at instantiation""" try: config_with_no_api_key() - except ValidationError as e: + except Exception as e: api_key_validation_error_assertions(e) with pytest.raises(ValidationError): config_with_no_api_key() @@ -35,7 +41,21 @@ def test_empty_api_key_provided(self) -> None: """DX-1441 - Empty API Key at instantiation.""" try: config_with_empty_api_key() - except ValidationError as e: + except Exception as e: api_key_validation_error_assertions(e) with pytest.raises(ValidationError): config_with_empty_api_key() + + def test_invalid_retries_provided(self): + """DX-1442 - Invalid retries at instantiation.""" + try: + config_with_invalid_retries() + except InvalidFieldValueError as e: + assert type(e) is InvalidFieldValueError + assert e.request_id is None + assert e.error_type is ErrorType.VALIDATION.value + assert e.error_code is ErrorCode.INVALID_FIELD_VALUE.value + assert e.source is ErrorSource.SHIPENGINE.value + assert e.message == "retries - Retries must be zero or greater." + with pytest.raises(InvalidFieldValueError): + config_with_invalid_retries() From 4f1fdf653fcbc64baace7ea31ff70936163d1cb5 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 25 May 2021 16:32:13 -0500 Subject: [PATCH 02/41] argument name adjustment e -> error --- shipengine_sdk/util/sdk_assertions.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 70857c4..4aa904a 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -21,14 +21,15 @@ def is_retries_less_than_zero(config: dict) -> None: field_name="retries", reason="Retries must be zero or greater.", field_value=config["retries"], + source=ErrorSource.SHIPENGINE.value ) -def api_key_validation_error_assertions(e: ValidationError): +def api_key_validation_error_assertions(error): """Helper test function that has common assertions pertaining to ValidationErrors.""" - assert type(e) is ValidationError - assert e.request_id is None - assert e.error_type is ErrorType.VALIDATION.value - assert e.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value - assert e.source is ErrorSource.SHIPENGINE.value - assert e.message == "A ShipEngine API key must be specified." + assert type(error) is ValidationError + assert error.request_id is None + assert error.error_type is ErrorType.VALIDATION.value + assert error.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value + assert error.source is ErrorSource.SHIPENGINE.value + assert error.message == "A ShipEngine API key must be specified." From b7ca5b9685e642a0e180db18ef78527d3a071db1 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 25 May 2021 16:32:59 -0500 Subject: [PATCH 03/41] Adjustment to InvalidFieldValueError arguments to include source - defaults to None --- shipengine_sdk/errors/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shipengine_sdk/errors/__init__.py b/shipengine_sdk/errors/__init__.py index 8e77ec2..9ec4b4e 100644 --- a/shipengine_sdk/errors/__init__.py +++ b/shipengine_sdk/errors/__init__.py @@ -92,14 +92,15 @@ def __init__( class InvalidFieldValueError(ShipEngineError): - def __init__(self, field_name: str, reason: str, field_value) -> None: + def __init__(self, field_name: str, reason: str, field_value, source: str = None) -> None: """This error occurs when a field has been set to an invalid value.""" self.field_name = field_name self.field_value = field_value + self.source = source super(InvalidFieldValueError, self).__init__( request_id=None, message=f"{self.field_name} - {reason}", - source=None, + source=self.source, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.INVALID_FIELD_VALUE.value, ) From 844d495aca6e8aebb9f4de32c633f1576a7e0dd4 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 25 May 2021 16:33:14 -0500 Subject: [PATCH 04/41] Added coveralls badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2436b5f..1ad6593 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ShipEngine SDK - Python ======================= ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ShipEngine/shipengine-python/Python%20package?label=shipengine-python&logo=github&logoColor=white) +[![Coverage Status](https://coveralls.io/repos/github/ShipEngine/shipengine-python/badge.svg?branch=main)](https://coveralls.io/github/ShipEngine/shipengine-python?branch=main) ![OS Compatibility](https://shipengine.github.io/img/badges/os-badges.svg) > ATTN: This project is under development and not ready for consumer use. From 8b43a132e6e313fda24ff62189de3a764836d132 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 25 May 2021 16:34:44 -0500 Subject: [PATCH 05/41] minor change to except statement --- tests/test_shipengine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_shipengine.py b/tests/test_shipengine.py index efc5b85..2781748 100644 --- a/tests/test_shipengine.py +++ b/tests/test_shipengine.py @@ -30,7 +30,7 @@ def test_no_api_key_provided(self) -> None: """DX-1440 - No API Key at instantiation.""" try: shipengine_no_api_key() - except ValidationError as e: + except Exception as e: with pytest.raises(ValidationError): shipengine_no_api_key() api_key_validation_error_assertions(e) @@ -39,7 +39,7 @@ def test_empty_api_key_provided(self) -> None: """DX-1441 - Empty API Key at instantiation.""" try: shipengine_empty_api_key() - except ValidationError as e: + except Exception as e: with pytest.raises(ValidationError): shipengine_empty_api_key() api_key_validation_error_assertions(e) From d9867f8505deb517320e0b9b6e9d41cd746cdff8 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 26 May 2021 12:00:05 -0500 Subject: [PATCH 06/41] work in progress - Acceptance criteria per DX-1443 - Invalid timeout at instantiation. --- .pre-commit-config.yaml | 2 +- pyproject.toml | 3 - shipengine_sdk/errors/__init__.py | 2 +- shipengine_sdk/shipengine_config.py | 7 +- shipengine_sdk/util/__init__.py | 2 +- shipengine_sdk/util/sdk_assertions.py | 14 +++- tests/test_shipengine_config.py | 95 ++++++++++++++++++++++++--- 7 files changed, 105 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55201e7..12165b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 20.8b1 hooks: - id: black - args: [ "--safe", "--diff", "--color" ] + args: [ "--safe", "--quiet"] language_version: python3 - repo: https://github.com/pycqa/isort rev: 5.6.4 diff --git a/pyproject.toml b/pyproject.toml index 5036301..c1b0cc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,6 @@ isort = "^5.8.0" line-length = 100 target-verstion = ["py37"] safe = true -quiet = true -diff = true -color = true [tool.isort] profile = "black" diff --git a/shipengine_sdk/errors/__init__.py b/shipengine_sdk/errors/__init__.py index 9ec4b4e..cbfe8e0 100644 --- a/shipengine_sdk/errors/__init__.py +++ b/shipengine_sdk/errors/__init__.py @@ -99,7 +99,7 @@ def __init__(self, field_name: str, reason: str, field_value, source: str = None self.source = source super(InvalidFieldValueError, self).__init__( request_id=None, - message=f"{self.field_name} - {reason}", + message=f"{self.field_name} - {reason} {self.field_value} was provided.", source=self.source, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.INVALID_FIELD_VALUE.value, diff --git a/shipengine_sdk/shipengine_config.py b/shipengine_sdk/shipengine_config.py index 5c8418a..cf373ac 100644 --- a/shipengine_sdk/shipengine_config.py +++ b/shipengine_sdk/shipengine_config.py @@ -2,8 +2,8 @@ import json from shipengine_sdk.models.enums import Endpoints -from shipengine_sdk.util import is_api_key_valid -from shipengine_sdk.util import is_retries_less_than_zero +from shipengine_sdk.util import is_api_key_valid, is_retries_valid +from shipengine_sdk.util.sdk_assertions import is_timeout_valid class ShipEngineConfig: @@ -28,6 +28,7 @@ def __init__(self, config: dict) -> None: is_api_key_valid(config) self.api_key = config["api_key"] + is_timeout_valid(config) if "timeout" in config: self.timeout = config["timeout"] else: @@ -43,7 +44,7 @@ def __init__(self, config: dict) -> None: else: self.page_size = self.DEFAULT_PAGE_SIZE - is_retries_less_than_zero(config) + is_retries_valid(config) if "retries" in config: self.retries = config["retries"] else: diff --git a/shipengine_sdk/util/__init__.py b/shipengine_sdk/util/__init__.py index f77d2be..01f7d1e 100644 --- a/shipengine_sdk/util/__init__.py +++ b/shipengine_sdk/util/__init__.py @@ -2,5 +2,5 @@ from shipengine_sdk.util.sdk_assertions import ( api_key_validation_error_assertions, is_api_key_valid, - is_retries_less_than_zero, + is_retries_valid, ) diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 4aa904a..c06578a 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -14,14 +14,24 @@ def is_api_key_valid(config: dict) -> None: ) -def is_retries_less_than_zero(config: dict) -> None: +def is_retries_valid(config: dict) -> None: """Checks that config.retries is less than zero.""" if "retries" in config and config["retries"] < 0: raise InvalidFieldValueError( field_name="retries", reason="Retries must be zero or greater.", field_value=config["retries"], - source=ErrorSource.SHIPENGINE.value + source=ErrorSource.SHIPENGINE.value, + ) + + +def is_timeout_valid(config: dict) -> None: + if "timeout" in config and config["timeout"] < 0: + raise InvalidFieldValueError( + field_name="timeout", + reason="Timeout must be zero or greater.", + field_value=config["timeout"], + source=ErrorSource.SHIPENGINE.value, ) diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index dfd05f7..09288a2 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -4,30 +4,82 @@ from shipengine_sdk import ShipEngineConfig from shipengine_sdk.errors import InvalidFieldValueError, ValidationError from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType +from shipengine_sdk.models.enums import Endpoints from shipengine_sdk.util import api_key_validation_error_assertions -def config_with_no_api_key(): +def config_with_no_api_key() -> ShipEngineConfig: """Return an error from no API Key.""" return ShipEngineConfig(dict(retries=2)) -def config_with_empty_api_key(): +def config_with_empty_api_key() -> ShipEngineConfig: """Return an error from empty API Key.""" return ShipEngineConfig(dict(api_key="")) -def config_with_whitespace_in_api_key(): +def config_with_whitespace_in_api_key() -> ShipEngineConfig: """Return an error from whitespace in API Key.""" return ShipEngineConfig(dict(api_key=" ")) -def config_with_invalid_retries(): - """Return an error from an invalid retry value being passed in""" - return ShipEngineConfig(dict(api_key="baz", retries=-3)) +def set_config_timeout(timeout: int) -> ShipEngineConfig: + """ + Return an error from an invalid timeout value being passed in or + returns the successfully created `ShipEngineConfig` object if valid + configuration values are passed in. + + :param int timeout: The timeout to be passed into the `ShipEngineConfig` object. + :returns: :class:`ShipEngineConfig`: Global configuration object for the ShipEngine SDK. + :raises: :class:`InvalidFieldValueError`: If invalid value is passed into `ShipEngineConfig` + object at instantiation. + """ + return ShipEngineConfig(dict(api_key="baz", timeout=timeout)) + + +def set_config_retries(retries: int) -> ShipEngineConfig: + """ + Return a ShipEngineConfig object with the set retries and + API Key, where the rest of the configuration values are + the default values. + + :param int retries: The retries to be passed into the `ShipEngineConfig` object. + :returns: :class:`ShipEngineConfig`: Global configuration object for the ShipEngine SDK. + :raises: :class:`InvalidFieldValueError`: If invalid value is passed into `ShipEngineConfig` + object at instantiation. + """ + return ShipEngineConfig(dict(api_key="baz", retries=retries)) + + +def complete_valid_config() -> ShipEngineConfig: + """ + Return a `ShipEngineConfig` object that has valid custom + values passed in. + """ + return ShipEngineConfig( + dict( + api_key="baz", + base_uri=Endpoints.TEST_RPC_URL.value, + page_size=50, + retries=2, + timeout=10, + ) + ) class TestShipEngineConfig: + def test_valid_custom_config(self): + """ + Test case where a config object has been passed custom + valid values for each attribute. + """ + valid_config: ShipEngineConfig = complete_valid_config() + assert valid_config.api_key == "baz" + assert valid_config.base_uri is Endpoints.TEST_RPC_URL.value + assert valid_config.page_size == 50 + assert valid_config.retries == 2 + assert valid_config.timeout == 10 + def test_no_api_key_provided(self) -> None: """DX-1440 - No API Key at instantiation""" try: @@ -46,16 +98,41 @@ def test_empty_api_key_provided(self) -> None: with pytest.raises(ValidationError): config_with_empty_api_key() + def test_valid_retries(self): + """Test case where a valid value is passed in for the retries.""" + retries = 2 + valid_retries = set_config_retries(retries) + assert valid_retries.api_key == "baz" + assert valid_retries.retries == retries + def test_invalid_retries_provided(self): """DX-1442 - Invalid retries at instantiation.""" + retries = -3 try: - config_with_invalid_retries() + set_config_retries(retries) except InvalidFieldValueError as e: assert type(e) is InvalidFieldValueError assert e.request_id is None assert e.error_type is ErrorType.VALIDATION.value assert e.error_code is ErrorCode.INVALID_FIELD_VALUE.value assert e.source is ErrorSource.SHIPENGINE.value - assert e.message == "retries - Retries must be zero or greater." + assert ( + e.message == f"retries - Retries must be zero or greater. {retries} was provided." + ) with pytest.raises(InvalidFieldValueError): - config_with_invalid_retries() + set_config_retries(retries) + + def test_invalid_timeout_provided(self): + """DX-1443 - Invalid timeout at instantiation.""" + timeout = -5 + try: + set_config_timeout(timeout) + except InvalidFieldValueError as e: + assert type(e) is InvalidFieldValueError + assert e.request_id is None + assert e.error_type is ErrorType.VALIDATION.value + assert e.error_code is ErrorCode.INVALID_FIELD_VALUE.value + assert e.source is ErrorSource.SHIPENGINE.value + assert ( + e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." + ) From 09c3b0ba7c53e2e0ea5bc1ead98fc0aa79abdf8b Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 26 May 2021 17:50:44 -0500 Subject: [PATCH 07/41] work in progress - implementing client for validate address and other methods --- .flake8 | 2 + .pre-commit-config.yaml | 13 +- poetry.lock | 6 +- shipengine_sdk/http_client/__init__.py | 3 +- shipengine_sdk/http_client/client.py | 187 ++++++++++++++++++ shipengine_sdk/jsonrpc/__init__.py | 60 ++++++ shipengine_sdk/services/__init__.py | 2 +- shipengine_sdk/services/address_validation.py | 1 + shipengine_sdk/shipengine_config.py | 6 +- shipengine_sdk/util/sdk_assertions.py | 103 +++++++++- tests/test_shipengine.py | 6 +- tox.ini | 2 + 12 files changed, 373 insertions(+), 18 deletions(-) create mode 100644 shipengine_sdk/jsonrpc/__init__.py create mode 100644 shipengine_sdk/services/address_validation.py diff --git a/.flake8 b/.flake8 index bf8d2ed..0448813 100644 --- a/.flake8 +++ b/.flake8 @@ -4,6 +4,8 @@ per-file-ignores = __init__.py: F401 max-line-length = 120 ignore = + # line break before binary operator + W503 # whitespace before ':' E203 # Missing Docstrings diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12165b5..1f972b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,9 @@ repos: + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + args: [ "--safe", "--diff", "--color"] - repo: https://github.com/psf/black rev: 20.8b1 hooks: @@ -6,17 +11,17 @@ repos: args: [ "--safe", "--quiet"] language_version: python3 - repo: https://github.com/pycqa/isort - rev: 5.6.4 + rev: 5.8.0 hooks: - id: isort args: ["--profile", "black", "--filter-files"] - repo: https://github.com/asottile/blacken-docs - rev: v1.9.2 + rev: v1.10.0 hooks: - id: blacken-docs additional_dependencies: [ black==20.8b1 ] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: check-docstring-first - id: trailing-whitespace @@ -35,7 +40,7 @@ repos: - id: python-use-type-annotations - id: rst-backticks - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.2 hooks: - id: flake8 language_version: python3 diff --git a/poetry.lock b/poetry.lock index f2f7c89..ac532ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -221,7 +221,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.0.1" +version = "4.1.0" description = "Read metadata from Python packages" category = "dev" optional = false @@ -896,8 +896,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"}, - {file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"}, + {file = "importlib_metadata-4.1.0-py3-none-any.whl", hash = "sha256:227b5fae29e4e51343000c1d43e4213f6e3bc6b995c39b6cf6e434dc8026a815"}, + {file = "importlib_metadata-4.1.0.tar.gz", hash = "sha256:daff3ed4f1e4545d7e60d606e989b1727f482de5da7e4fc6b387f4cd5bca8ece"}, ] isort = [ {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, diff --git a/shipengine_sdk/http_client/__init__.py b/shipengine_sdk/http_client/__init__.py index 0cb716b..96e8ffd 100644 --- a/shipengine_sdk/http_client/__init__.py +++ b/shipengine_sdk/http_client/__init__.py @@ -1 +1,2 @@ -"""Initial Docstring.""" +"""Synchronous HTTP Client for ShipEngine SDK.""" +from .client import ShipEngineClient diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index fa65d26..2fcd4d0 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -1 +1,188 @@ """A Python library for ShipEngine API.""" +import os +import platform +from typing import Dict, Optional, Union + +import requests +from requests import PreparedRequest, Request, RequestException, Response, Session +from requests.adapters import HTTPAdapter +from requests.auth import AuthBase +from requests.packages.urllib3.util.retry import Retry + +from shipengine_sdk import ShipEngineConfig, __version__ +from shipengine_sdk.errors import ( + AccountStatusError, + BusinessRuleError, + ClientSecurityError, + ClientSystemError, + ShipEngineError, + ValidationError, +) +from shipengine_sdk.jsonrpc import wrap_request +from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType +from shipengine_sdk.util.sdk_assertions import ( + is_response_404, + is_response_429, + is_response_500, +) + + +class ShipEngineAuth(AuthBase): + def __init__(self, api_key: str) -> None: + """Auth Base appends `Api-Key` header to all requests.""" + self.api_key = api_key + + def __call__(self, request: Request, *args, **kwargs) -> Request: + request.headers["Api-Key"] = self.api_key + return request + + +class ShipEngineClient: + _BASE_URI = "" + + def __init__(self) -> None: + """""" + self.session = requests.session() + + def send_rpc_request( + self, method: str, params: Optional[Dict[str, any]], retry: int, config: ShipEngineConfig + ) -> dict: + """ + Send a `JSON-RPC 2.0` request via HTTP Messages to ShipEngine API. If the response + * is successful, the result is returned. Otherwise, an error is thrown. + + TODO: add param and return docs + """ + # TODO: debug the below base_uri variable to verify ternary logic works as intended. + client: Session = self._request_retry_session(retries=config.retries) + base_uri: Union[str, None] = ( + config.base_uri + if os.getenv("CLIENT_BASE_URI") is None + else os.getenv("CLIENT_BASE_URI") + ) + + request_headers: dict = { + "User-Agent": self._derive_user_agent(), + "Content-Type": "application/json", + "Accept": "application/json", + } + + request_body: dict = wrap_request(method=method, params=params) + + req: Request = Request( + method="POST", + url=base_uri, + data=request_body, + headers=request_headers, + auth=ShipEngineAuth(config.api_key), + ) + prepared_req: PreparedRequest = req.prepare() + + try: + resp: Response = client.send(request=prepared_req, timeout=config.timeout) + except RequestException as err: + raise ShipEngineError( + message=f"An unknown error occurred while calling the ShipEngine {method} API:\n {err.response}", + source=ErrorSource.SHIPENGINE.value, + error_type=ErrorType.SYSTEM.value, + error_code=ErrorCode.UNSPECIFIED.value, + ) + + resp_body = resp.json() + status_code = resp.status_code + + is_response_404(status_code=status_code, response_body=resp_body) + is_response_429(status_code=status_code, response_body=resp_body, config=config) + is_response_500(status_code=status_code, response_body=resp_body) + + return self._handle_response(resp.json()) + + def _request_retry_session( + self, retries: int = 1, backoff_factor=1, status_force_list=(429, 500, 502, 503, 504) + ) -> Session: + """A requests `Session()` that has retries enforced.""" + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_force_list, + ) + adapter = HTTPAdapter(max_retries=retry) + self.session.mount("http://", adapter=adapter) + self.session.mount("https://", adapter=adapter) + return self.session + + @staticmethod + def _handle_response(response_body: dict) -> dict: + """Handles the response from ShipEngine API.""" + if "result" in response_body: + return response_body + + error = response_body["error"] + error_data = error["data"] + error_type = error_data["type"] + if error_type is ErrorType.ACCOUNT_STATUS.value: + raise AccountStatusError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.SECURITY.value: + raise ClientSecurityError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.VALIDATION.value: + raise ValidationError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.BUSINESS_RULES.value: + raise BusinessRuleError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.SYSTEM.value: + raise ClientSystemError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + else: + raise ShipEngineError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + + @staticmethod + def _derive_user_agent() -> str: + """ + Derive a User-Agent header from the environment. This is the user-agent that will + be set on every request via the ShipEngine Client. + + :returns: A user-agent string that will be set in the `ShipEngineClient` request headers. + :rtype: str + """ + sdk_version = f"shipengine-python/{__version__}" + os_kernel = platform.platform(terse=True) + python_version = platform.python_version() + python_implementation = platform.python_implementation() + + return f"{sdk_version} {os_kernel} {python_version} {python_implementation}" diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py new file mode 100644 index 0000000..19d2024 --- /dev/null +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -0,0 +1,60 @@ +""" +A collection of methods that provide `JSON-RPC 2.0` HTTP client +functionality for sending HTTP requests from the ShipEngine SDK. +""" +import os +import time +from typing import Dict, Optional, Union +from uuid import uuid4 + +from shipengine_sdk import ShipEngineConfig +from shipengine_sdk.errors import RateLimitExceededError +from shipengine_sdk.http_client import ShipEngineClient + + +def rpc_request( + method: str, config: ShipEngineConfig, params: Optional[Dict[str, any]] = None +) -> dict: + """ + Create and send a `JSON-RPC 2.0` request over HTTP messages. + TODO: add param and return docs + """ + return rpc_request_loop(method, params, config) + + +def rpc_request_loop(method: str, params: dict, config: ShipEngineConfig) -> dict: + client = ShipEngineClient() + retry: int = 0 + while retry <= config.retries: + try: + api_response = client.send_rpc_request( + method=method, params=params, retry=retry, config=config + ) + except Exception as err: + if ( + retry < config.retries + and type(err) is RateLimitExceededError + and err.retry_after < config.timeout + ): + time.sleep(err.retry_after) + else: + raise err + return api_response # TODO: pick up here + + +def wrap_request(method: str, params: Optional[Dict[str, any]]) -> dict: + """ + Wrap request per `JSON-RPC 2.0` spec. + + :param str method: The RPC Method to be sent to the RPC Server to + invoke a specific remote procedure. + :param params: The request data for the RPC request. This argument + is optional and can either be a dictionary or None. + :type params: Optional[Dict[str, any]] + """ + if params is None: + return dict(id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method) + else: + return dict( + id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method, params=params + ) diff --git a/shipengine_sdk/services/__init__.py b/shipengine_sdk/services/__init__.py index 0cb716b..a677db9 100644 --- a/shipengine_sdk/services/__init__.py +++ b/shipengine_sdk/services/__init__.py @@ -1 +1 @@ -"""Initial Docstring.""" +"""ShipEngine SDK service objects.""" diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py new file mode 100644 index 0000000..b5bcf10 --- /dev/null +++ b/shipengine_sdk/services/address_validation.py @@ -0,0 +1 @@ +"""Initial Docstring""" diff --git a/shipengine_sdk/shipengine_config.py b/shipengine_sdk/shipengine_config.py index cf373ac..9937f63 100644 --- a/shipengine_sdk/shipengine_config.py +++ b/shipengine_sdk/shipengine_config.py @@ -85,5 +85,9 @@ def merge(self, new_config: dict = None): return ShipEngineConfig(config) + @staticmethod + def to_dict(): + return lambda o: o.__dict__ + def to_json(self): - return json.dumps(self, default=lambda o: o.__dict__, indent=2) + return json.dumps(self, default=self.to_dict, indent=2) diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index c06578a..864a262 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -1,10 +1,26 @@ """Assertion helper functions.""" -from shipengine_sdk.errors import InvalidFieldValueError, ValidationError +import re + +from shipengine_sdk import ShipEngineConfig +from shipengine_sdk.errors import ( + ClientSystemError, + ClientTimeoutError, + InvalidFieldValueError, + RateLimitExceededError, + ValidationError, +) from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType def is_api_key_valid(config: dict) -> None: - """Check if API Key is set and is not empty or whitespace.""" + """ + Check if API Key is set and is not empty or whitespace. + + :param dict config: The config dictionary passed into `ShipEngineConfig`. + :returns: None, only raises exceptions. + :rtype: None + """ + match = re.match(r"\s", config["timeout"]) if "api_key" not in config or config["api_key"] == "": raise ValidationError( message="A ShipEngine API key must be specified.", @@ -12,10 +28,24 @@ def is_api_key_valid(config: dict) -> None: error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) + elif match: + raise ValidationError( + message="The API key provided contains whitespace and is invalid.", + source=ErrorSource.SHIPENGINE.value, + error_type=ErrorType.VALIDATION.value, + error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, + url="https://www.shipengine.com/signup/", + ) def is_retries_valid(config: dict) -> None: - """Checks that config.retries is less than zero.""" + """ + Checks that config.retries is a valid value. + + :param dict config: The config dictionary passed into `ShipEngineConfig`. + :returns: None, only raises exceptions. + :rtype: None + """ if "retries" in config and config["retries"] < 0: raise InvalidFieldValueError( field_name="retries", @@ -26,6 +56,13 @@ def is_retries_valid(config: dict) -> None: def is_timeout_valid(config: dict) -> None: + """ + Checks that config.timeout is valid value. + + :param dict config: The config dictionary passed into `ShipEngineConfig`. + :returns: None, only raises exceptions. + :rtype: None + """ if "timeout" in config and config["timeout"] < 0: raise InvalidFieldValueError( field_name="timeout", @@ -35,11 +72,67 @@ def is_timeout_valid(config: dict) -> None: ) -def api_key_validation_error_assertions(error): - """Helper test function that has common assertions pertaining to ValidationErrors.""" +def api_key_validation_error_assertions(error) -> None: + """ + Helper test function that has common assertions pertaining to ValidationErrors. + + :param error: The error to execute assertions on. + :returns: None, only executes assertions. + :rtype: None + """ assert type(error) is ValidationError assert error.request_id is None assert error.error_type is ErrorType.VALIDATION.value assert error.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value assert error.source is ErrorSource.SHIPENGINE.value assert error.message == "A ShipEngine API key must be specified." + + +def is_response_404(status_code: int, response_body: dict) -> None: + """Check if status_code is 404 and raises an error if so.""" + if "error" in response_body: + error = response_body["error"] + error_data = error["data"] + if status_code == 404: + raise ClientSystemError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + + +def is_response_429(status_code: int, response_body: dict, config: ShipEngineConfig) -> None: + """Check if status_code is 429 and raises an error if so.""" + if "error" in response_body: + error = response_body["error"] + retry_after = error["data"]["retryAfter"] + + if retry_after > config.timeout: + raise ClientTimeoutError( + retry_after=config.timeout, + source=ErrorSource.SHIPENGINE.value, + request_id=response_body["id"], + ) + + if status_code == 429: + raise RateLimitExceededError( + retry_after=retry_after, + source=ErrorSource.SHIPENGINE.value, + request_id=response_body["id"], + ) + + +def is_response_500(status_code: int, response_body: dict) -> None: + """Check if the status code is 500 and raises an error if so.""" + if status_code == 500: + error = response_body["error"] + error_data = error["data"] + raise ClientSystemError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) diff --git a/tests/test_shipengine.py b/tests/test_shipengine.py index 2781748..0b47209 100644 --- a/tests/test_shipengine.py +++ b/tests/test_shipengine.py @@ -6,17 +6,17 @@ from shipengine_sdk.util.sdk_assertions import api_key_validation_error_assertions -def shipengine_no_api_key(): +def shipengine_no_api_key() -> ShipEngine: """Return an error from no API Key.""" return ShipEngine(dict(retries=2)) -def shipengine_empty_api_key(): +def shipengine_empty_api_key() -> ShipEngine: """Return an error from empty API Key.""" return ShipEngine(config="") -def shipengine_whitespace_in_api_key(): +def shipengine_whitespace_in_api_key() -> ShipEngine: """Return an error from whitespace in API Key.""" return ShipEngine(config=" ") diff --git a/tox.ini b/tox.ini index 75091d6..6f317da 100644 --- a/tox.ini +++ b/tox.ini @@ -40,6 +40,8 @@ per-file-ignores = ; imported but unused __init__.py: F401 extend-ignore = + ; line break before binary operator + W503 ; whitespace before ':' E203 ; Missing Docstrings From 196bdd427f0ef82ac79b5e9e42bc8d2c1e7f7b85 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 26 May 2021 17:55:25 -0500 Subject: [PATCH 08/41] implementing client for validate address and other methods --- shipengine_sdk/jsonrpc/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index 19d2024..801562c 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -39,6 +39,7 @@ def rpc_request_loop(method: str, params: dict, config: ShipEngineConfig) -> dic time.sleep(err.retry_after) else: raise err + retry += 1 return api_response # TODO: pick up here From 2ffc0ad71a09a59e81c713db5048ef49dace28ff Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 26 May 2021 17:59:37 -0500 Subject: [PATCH 09/41] update lockfile --- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index ac532ec..2d0d725 100644 --- a/poetry.lock +++ b/poetry.lock @@ -221,7 +221,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.1.0" +version = "4.2.0" description = "Read metadata from Python packages" category = "dev" optional = false @@ -658,16 +658,16 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.4" +version = "1.26.5" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] +brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "virtualenv" @@ -896,8 +896,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.1.0-py3-none-any.whl", hash = "sha256:227b5fae29e4e51343000c1d43e4213f6e3bc6b995c39b6cf6e434dc8026a815"}, - {file = "importlib_metadata-4.1.0.tar.gz", hash = "sha256:daff3ed4f1e4545d7e60d606e989b1727f482de5da7e4fc6b387f4cd5bca8ece"}, + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] isort = [ {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, @@ -1210,8 +1210,8 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ - {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, - {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, + {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, + {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, ] virtualenv = [ {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, From ee44d2b001524dd81b38aa7bc97ea0fbcbbf99f5 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Thu, 27 May 2021 10:51:46 -0500 Subject: [PATCH 10/41] quick bug fix in sdk_assertions.py --- .pre-commit-config.yaml | 3 ++- shipengine_sdk/__init__.py | 4 ++-- shipengine_sdk/jsonrpc/__init__.py | 3 +-- shipengine_sdk/util/sdk_assertions.py | 13 +------------ tests/test_shipengine.py | 2 +- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f972b4..9bfd461 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,8 @@ repos: rev: 20.8b1 hooks: - id: black - args: [ "--safe", "--diff", "--color"] + args: ["--diff", "--color"] + verbose: true - repo: https://github.com/psf/black rev: 20.8b1 hooks: diff --git a/shipengine_sdk/__init__.py b/shipengine_sdk/__init__.py index 6edf480..6751847 100644 --- a/shipengine_sdk/__init__.py +++ b/shipengine_sdk/__init__.py @@ -6,7 +6,7 @@ from logging import NullHandler # SDK imports here -from shipengine_sdk.shipengine import ShipEngine -from shipengine_sdk.shipengine_config import ShipEngineConfig +from .shipengine import ShipEngine +from .shipengine_config import ShipEngineConfig logging.getLogger(__name__).addHandler(NullHandler()) diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index 801562c..b8f7b7a 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -2,9 +2,8 @@ A collection of methods that provide `JSON-RPC 2.0` HTTP client functionality for sending HTTP requests from the ShipEngine SDK. """ -import os import time -from typing import Dict, Optional, Union +from typing import Dict, Optional from uuid import uuid4 from shipengine_sdk import ShipEngineConfig diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 864a262..484ad59 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -1,7 +1,5 @@ """Assertion helper functions.""" -import re -from shipengine_sdk import ShipEngineConfig from shipengine_sdk.errors import ( ClientSystemError, ClientTimeoutError, @@ -20,7 +18,6 @@ def is_api_key_valid(config: dict) -> None: :returns: None, only raises exceptions. :rtype: None """ - match = re.match(r"\s", config["timeout"]) if "api_key" not in config or config["api_key"] == "": raise ValidationError( message="A ShipEngine API key must be specified.", @@ -28,14 +25,6 @@ def is_api_key_valid(config: dict) -> None: error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) - elif match: - raise ValidationError( - message="The API key provided contains whitespace and is invalid.", - source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.VALIDATION.value, - error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, - url="https://www.shipengine.com/signup/", - ) def is_retries_valid(config: dict) -> None: @@ -103,7 +92,7 @@ def is_response_404(status_code: int, response_body: dict) -> None: ) -def is_response_429(status_code: int, response_body: dict, config: ShipEngineConfig) -> None: +def is_response_429(status_code: int, response_body: dict, config) -> None: """Check if status_code is 429 and raises an error if so.""" if "error" in response_body: error = response_body["error"] diff --git a/tests/test_shipengine.py b/tests/test_shipengine.py index 0b47209..9d549dc 100644 --- a/tests/test_shipengine.py +++ b/tests/test_shipengine.py @@ -18,7 +18,7 @@ def shipengine_empty_api_key() -> ShipEngine: def shipengine_whitespace_in_api_key() -> ShipEngine: """Return an error from whitespace in API Key.""" - return ShipEngine(config=" ") + return ShipEngine(config=" ") class TestShipEngine: From e0fad495c6b98e867b7413f5e20f963cc1772d9e Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Thu, 27 May 2021 10:52:06 -0500 Subject: [PATCH 11/41] quick bug fix in sdk_assertions.py --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9bfd461..0039f46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 20.8b1 hooks: - id: black - args: ["--diff", "--color"] + args: [ "--safe", "--diff", "--color"] verbose: true - repo: https://github.com/psf/black rev: 20.8b1 From 5adfdd96a6e1ae5f3df0421ee0a37567689a58a0 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Thu, 27 May 2021 14:50:34 -0500 Subject: [PATCH 12/41] Acceptance criteria per DX-1022 - validate address implementation --- poetry.lock | 86 ++++++++++++++++++- pyproject.toml | 1 + shipengine_sdk/models/address/__init__.py | 0 shipengine_sdk/services/address_validation.py | 22 ++++- shipengine_sdk/shipengine.py | 19 +++- shipengine_sdk/shipengine_config.py | 10 +-- 6 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 shipengine_sdk/models/address/__init__.py diff --git a/poetry.lock b/poetry.lock index 2d0d725..ef5d345 100644 --- a/poetry.lock +++ b/poetry.lock @@ -154,6 +154,23 @@ toml = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["toml"] +[[package]] +name = "dataclasses-json" +version = "0.5.3" +description = "Easily serialize dataclasses to and from JSON" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.3.0,<4.0.0" +marshmallow-enum = ">=1.5.1,<2.0.0" +stringcase = "1.2.0" +typing-inspect = ">=0.4.0" + +[package.extras] +dev = ["pytest (>=6.2.3)", "ipython", "mypy (>=0.710)", "hypothesis", "portray", "flake8", "simplejson"] + [[package]] name = "distlib" version = "0.3.1" @@ -270,6 +287,31 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "marshmallow" +version = "3.12.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.0.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.4)"] +lint = ["mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-enum" +version = "1.5.1" +description = "Enum field for Marshmallow" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +marshmallow = ">=2.0.0" + [[package]] name = "mccabe" version = "0.6.1" @@ -298,7 +340,7 @@ python-versions = ">=3.6" name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -609,6 +651,14 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "stringcase" +version = "1.2.0" +description = "String case converter." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "toml" version = "0.10.2" @@ -656,6 +706,18 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "typing-inspect" +version = "0.6.0" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + [[package]] name = "urllib3" version = "1.26.5" @@ -724,7 +786,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "d7f6fec96cf7c4dbffe42d5a228cde1ad3e26587965fda8eac92b4f92b4bb202" +content-hash = "01b596eb1c20c79ed921218837d93cab601909e31299d77392ac5405aed7867f" [metadata.files] aiohttp = [ @@ -867,6 +929,10 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] +dataclasses-json = [ + {file = "dataclasses-json-0.5.3.tar.gz", hash = "sha256:fe17da934cfc4ec792ebe7e9a303434ecf4f5f8d8a7705acfbbe7ccbd34bf1ae"}, + {file = "dataclasses_json-0.5.3-py3-none-any.whl", hash = "sha256:740e7b564d72ddaa0f66406b4ecb799447afda2799c1c425a4a76151bfcfda50"}, +] distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, @@ -943,6 +1009,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] +marshmallow = [ + {file = "marshmallow-3.12.1-py2.py3-none-any.whl", hash = "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"}, + {file = "marshmallow-3.12.1.tar.gz", hash = "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040"}, +] +marshmallow-enum = [ + {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, + {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -1164,6 +1238,9 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +stringcase = [ + {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1209,6 +1286,11 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] +typing-inspect = [ + {file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"}, + {file = "typing_inspect-0.6.0-py3-none-any.whl", hash = "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f"}, + {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"}, +] urllib3 = [ {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, diff --git a/pyproject.toml b/pyproject.toml index c1b0cc2..f411d25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ python = "^3.7" aiohttp = "^3.7.4" requests = "^2.25.1" python-dotenv = "^0.15.0" +dataclasses-json = "^0.5.3" [tool.poetry.dev-dependencies] pytest = "^4.6" diff --git a/shipengine_sdk/models/address/__init__.py b/shipengine_sdk/models/address/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py index b5bcf10..18d1b09 100644 --- a/shipengine_sdk/services/address_validation.py +++ b/shipengine_sdk/services/address_validation.py @@ -1 +1,21 @@ -"""Initial Docstring""" +"""Validate a single address or multiple addresses.""" +from shipengine_sdk import ShipEngineConfig +from shipengine_sdk.jsonrpc import rpc_request +from shipengine_sdk.models.address import Address, AddressValidateResult +from shipengine_sdk.models.enums import RPCMethods + + +def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResult: + """ + Validate a single address via the `address/validate` remote procedure. + + :param Address address: The address to be validate. + :param ShipEngineConfig config: The global ShipEngine configuration object. + :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the + validated and normalized address. + """ + api_response = rpc_request( + method=RPCMethods.ADDRESS_VALIDATE.value, config=config, params=address.to_dict() + ) + + return AddressValidateResult(api_response) diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index 939a663..5e24bdd 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -1,7 +1,8 @@ """The entrypoint to the ShipEngine API SDK.""" -from typing import Dict -from typing import Union +from typing import Dict, Union +from .models.address import Address, AddressValidateResult +from .services.address_validation import validate from .shipengine_config import ShipEngineConfig @@ -25,3 +26,17 @@ def __init__(self, config: Union[str, Dict[str, any]]) -> None: self.config = ShipEngineConfig({"api_key": config}) elif type(config) is dict: self.config = ShipEngineConfig(config) + + def validate_address( + self, address: Address, config: Union[Dict[str, any], ShipEngineConfig] = None + ) -> AddressValidateResult: + """ + Validate an address in nearly any countryCode in the world. + + :param Address address: The address to be validate. + :param ShipEngineConfig config: The global ShipEngine configuration object. + :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the + validated and normalized address. + """ + config = self.config.merge(new_config=config) + return validate(address, config) diff --git a/shipengine_sdk/shipengine_config.py b/shipengine_sdk/shipengine_config.py index 9937f63..14af331 100644 --- a/shipengine_sdk/shipengine_config.py +++ b/shipengine_sdk/shipengine_config.py @@ -1,5 +1,6 @@ """The global configuration object for the ShipEngine SDK.""" import json +from typing import Dict, Optional from shipengine_sdk.models.enums import Endpoints from shipengine_sdk.util import is_api_key_valid, is_retries_valid @@ -51,7 +52,7 @@ def __init__(self, config: dict) -> None: self.retries = self.DEFAULT_RETRIES # TODO: add event listener to config object once it"s implemented. - def merge(self, new_config: dict = None): + def merge(self, new_config: Optional[Dict[str, any]] = None): """ The method allows the merging of a method-level configuration adjustment into the current configuration. @@ -85,9 +86,8 @@ def merge(self, new_config: dict = None): return ShipEngineConfig(config) - @staticmethod - def to_dict(): - return lambda o: o.__dict__ + def to_dict(self): + return (lambda o: o.__dict__)(self) def to_json(self): - return json.dumps(self, default=self.to_dict, indent=2) + return json.dumps(self, default=lambda o: o.__dict__, indent=2) From b36f3cfa317cb27ba4c8d6b39ac20726b45fef6e Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Thu, 27 May 2021 16:29:54 -0500 Subject: [PATCH 13/41] work in progress --- shipengine_sdk/errors/__init__.py | 3 + shipengine_sdk/http_client/client.py | 74 ++------------- shipengine_sdk/jsonrpc/__init__.py | 26 ++---- shipengine_sdk/jsonrpc/process_request.py | 89 +++++++++++++++++++ shipengine_sdk/models/__init__.py | 4 +- shipengine_sdk/models/address/__init__.py | 31 +++++++ shipengine_sdk/models/enums/__init__.py | 11 +++ shipengine_sdk/services/address_validation.py | 6 +- shipengine_sdk/shipengine.py | 5 +- shipengine_sdk/util/__init__.py | 13 +++ shipengine_sdk/util/sdk_assertions.py | 1 - tests/services/test_address_validation.py | 40 +++++++++ 12 files changed, 205 insertions(+), 98 deletions(-) create mode 100644 shipengine_sdk/jsonrpc/process_request.py create mode 100644 tests/services/test_address_validation.py diff --git a/shipengine_sdk/errors/__init__.py b/shipengine_sdk/errors/__init__.py index cbfe8e0..470f6c8 100644 --- a/shipengine_sdk/errors/__init__.py +++ b/shipengine_sdk/errors/__init__.py @@ -46,6 +46,9 @@ def _are_enums_valid(self): f"Error type must be a member of ErrorCode enum - [{self.error_code}] provided." ) + def to_dict(self): + return (lambda o: o.__dict__)(self) + def to_json(self): return json.dumps(self, default=lambda o: o.__dict__, indent=2) diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index 2fcd4d0..ce98b53 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -9,17 +9,11 @@ from requests.auth import AuthBase from requests.packages.urllib3.util.retry import Retry -from shipengine_sdk import ShipEngineConfig, __version__ -from shipengine_sdk.errors import ( - AccountStatusError, - BusinessRuleError, - ClientSecurityError, - ClientSystemError, - ShipEngineError, - ValidationError, -) -from shipengine_sdk.jsonrpc import wrap_request +from shipengine_sdk import __version__ +from shipengine_sdk.errors import ShipEngineError +from shipengine_sdk.jsonrpc.process_request import handle_response, wrap_request from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType +from shipengine_sdk.shipengine_config import ShipEngineConfig from shipengine_sdk.util.sdk_assertions import ( is_response_404, is_response_429, @@ -95,7 +89,7 @@ def send_rpc_request( is_response_429(status_code=status_code, response_body=resp_body, config=config) is_response_500(status_code=status_code, response_body=resp_body) - return self._handle_response(resp.json()) + return handle_response(resp.json()) def _request_retry_session( self, retries: int = 1, backoff_factor=1, status_force_list=(429, 500, 502, 503, 504) @@ -113,64 +107,6 @@ def _request_retry_session( self.session.mount("https://", adapter=adapter) return self.session - @staticmethod - def _handle_response(response_body: dict) -> dict: - """Handles the response from ShipEngine API.""" - if "result" in response_body: - return response_body - - error = response_body["error"] - error_data = error["data"] - error_type = error_data["type"] - if error_type is ErrorType.ACCOUNT_STATUS.value: - raise AccountStatusError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.SECURITY.value: - raise ClientSecurityError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.VALIDATION.value: - raise ValidationError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.BUSINESS_RULES.value: - raise BusinessRuleError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.SYSTEM.value: - raise ClientSystemError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - else: - raise ShipEngineError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - @staticmethod def _derive_user_agent() -> str: """ diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index b8f7b7a..cd18dff 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -4,11 +4,12 @@ """ import time from typing import Dict, Optional -from uuid import uuid4 -from shipengine_sdk import ShipEngineConfig from shipengine_sdk.errors import RateLimitExceededError from shipengine_sdk.http_client import ShipEngineClient +from shipengine_sdk.shipengine_config import ShipEngineConfig + +from .process_request import handle_response, wrap_request def rpc_request( @@ -23,6 +24,7 @@ def rpc_request( def rpc_request_loop(method: str, params: dict, config: ShipEngineConfig) -> dict: client = ShipEngineClient() + api_response = None retry: int = 0 while retry <= config.retries: try: @@ -39,22 +41,4 @@ def rpc_request_loop(method: str, params: dict, config: ShipEngineConfig) -> dic else: raise err retry += 1 - return api_response # TODO: pick up here - - -def wrap_request(method: str, params: Optional[Dict[str, any]]) -> dict: - """ - Wrap request per `JSON-RPC 2.0` spec. - - :param str method: The RPC Method to be sent to the RPC Server to - invoke a specific remote procedure. - :param params: The request data for the RPC request. This argument - is optional and can either be a dictionary or None. - :type params: Optional[Dict[str, any]] - """ - if params is None: - return dict(id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method) - else: - return dict( - id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method, params=params - ) + return api_response diff --git a/shipengine_sdk/jsonrpc/process_request.py b/shipengine_sdk/jsonrpc/process_request.py new file mode 100644 index 0000000..01bc16b --- /dev/null +++ b/shipengine_sdk/jsonrpc/process_request.py @@ -0,0 +1,89 @@ +"""Functions that help with process requests and handle responses.""" +from typing import Dict, Optional +from uuid import uuid4 + +from shipengine_sdk.errors import ( + AccountStatusError, + BusinessRuleError, + ClientSecurityError, + ClientSystemError, + ShipEngineError, + ValidationError, +) +from shipengine_sdk.models import ErrorType + + +def wrap_request(method: str, params: Optional[Dict[str, any]]) -> dict: + """ + Wrap request per `JSON-RPC 2.0` spec. + + :param str method: The RPC Method to be sent to the RPC Server to + invoke a specific remote procedure. + :param params: The request data for the RPC request. This argument + is optional and can either be a dictionary or None. + :type params: Optional[Dict[str, any]] + """ + if params is None: + return dict(id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method) + else: + return dict( + id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method, params=params + ) + + +def handle_response(response_body: dict) -> dict: + """Handles the response from ShipEngine API.""" + if "result" in response_body: + return response_body + + error = response_body["error"] + error_data = error["data"] + error_type = error_data["type"] + if error_type is ErrorType.ACCOUNT_STATUS.value: + raise AccountStatusError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.SECURITY.value: + raise ClientSecurityError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.VALIDATION.value: + raise ValidationError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.BUSINESS_RULES.value: + raise BusinessRuleError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + elif error_type is ErrorType.SYSTEM.value: + raise ClientSystemError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) + else: + raise ShipEngineError( + message=error["message"], + request_id=response_body["id"], + source=error_data["source"], + error_type=error_data["type"], + error_code=error_data["code"], + ) diff --git a/shipengine_sdk/models/__init__.py b/shipengine_sdk/models/__init__.py index ff6f842..c8d770b 100644 --- a/shipengine_sdk/models/__init__.py +++ b/shipengine_sdk/models/__init__.py @@ -1,4 +1,2 @@ """ShipEngine SDK Models & Enumerations""" -from shipengine_sdk.models.enums import ErrorCode -from shipengine_sdk.models.enums import ErrorSource -from shipengine_sdk.models.enums import ErrorType +from shipengine_sdk.models.enums import ErrorCode, ErrorSource, ErrorType diff --git a/shipengine_sdk/models/address/__init__.py b/shipengine_sdk/models/address/__init__.py index e69de29..ebb71e5 100644 --- a/shipengine_sdk/models/address/__init__.py +++ b/shipengine_sdk/models/address/__init__.py @@ -0,0 +1,31 @@ +"""Initial Docstring""" + +from dataclasses import dataclass +from typing import List, Optional + +from dataclasses_json import LetterCase, dataclass_json + + +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class Address: + street: List[str] + city_locality: str + state_province: str + postal_code: str + country_code: str + is_residential: Optional[bool] = None + name: str = "" + phone: str = "" + company: str = "" + + +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass(frozen=True) +class AddressValidateResult: + is_valid: Optional[bool] + request_id: str + address: Address + info: List + warnings: List + errors: List diff --git a/shipengine_sdk/models/enums/__init__.py b/shipengine_sdk/models/enums/__init__.py index 9c1e29f..0d630fc 100644 --- a/shipengine_sdk/models/enums/__init__.py +++ b/shipengine_sdk/models/enums/__init__.py @@ -7,5 +7,16 @@ class Endpoints(Enum): + """API Endpoint URI's used throughout the ShipEngine SDK.""" + TEST_RPC_URL = "https://simengine.herokuapp.com/jsonrpc" SHIPENGINE_RPC_URL = "https://api.shipengine.com/jsonrpc" + + +class RPCMethods(Enum): + """A collection of RPC Methods used throughout the ShipEngine SDK.""" + + ADDRESS_VALIDATE = "address.validate.v1" + CREATE_TAG = "create.tag.v1" + LIST_CARRIERS = "carrier.listAccounts.v1" + TRACK_PACKAGE = "package.track.v1" diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py index 18d1b09..0d47875 100644 --- a/shipengine_sdk/services/address_validation.py +++ b/shipengine_sdk/services/address_validation.py @@ -1,8 +1,8 @@ """Validate a single address or multiple addresses.""" -from shipengine_sdk import ShipEngineConfig from shipengine_sdk.jsonrpc import rpc_request from shipengine_sdk.models.address import Address, AddressValidateResult from shipengine_sdk.models.enums import RPCMethods +from shipengine_sdk.shipengine_config import ShipEngineConfig def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResult: @@ -15,7 +15,9 @@ def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResul validated and normalized address. """ api_response = rpc_request( - method=RPCMethods.ADDRESS_VALIDATE.value, config=config, params=address.to_dict() + method=RPCMethods.ADDRESS_VALIDATE.value, + config=config, + params={"address": address.to_dict()}, ) return AddressValidateResult(api_response) diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index 5e24bdd..bf4e46c 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -1,8 +1,9 @@ """The entrypoint to the ShipEngine API SDK.""" from typing import Dict, Union -from .models.address import Address, AddressValidateResult -from .services.address_validation import validate +from shipengine_sdk.models.address import Address, AddressValidateResult +from shipengine_sdk.services.address_validation import validate + from .shipengine_config import ShipEngineConfig diff --git a/shipengine_sdk/util/__init__.py b/shipengine_sdk/util/__init__.py index 01f7d1e..29de2af 100644 --- a/shipengine_sdk/util/__init__.py +++ b/shipengine_sdk/util/__init__.py @@ -4,3 +4,16 @@ is_api_key_valid, is_retries_valid, ) + + +def snake_to_camel(snake_case_string: str) -> str: + """ + Takes in a `snake_case` string and returns a `camelCase` string. + + :params str snake_case_string: The snake_case string to be converted + into camelCase. + :returns: camelCase string + :rtype: str + """ + initial, *temp = snake_case_string.split("_") + return "".join([initial.lower(), *map(str.title, temp)]) diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 484ad59..0c02d10 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -1,5 +1,4 @@ """Assertion helper functions.""" - from shipengine_sdk.errors import ( ClientSystemError, ClientTimeoutError, diff --git a/tests/services/test_address_validation.py b/tests/services/test_address_validation.py new file mode 100644 index 0000000..687d53b --- /dev/null +++ b/tests/services/test_address_validation.py @@ -0,0 +1,40 @@ +"""Initial Docstring""" +from shipengine_sdk import ShipEngine +from shipengine_sdk.models.address import Address +from shipengine_sdk.models.enums import Endpoints + + +def stub_config() -> dict: + """ + Return a test configuration dictionary to be used + when instantiating the ShipEngine object. + """ + return dict( + api_key="baz", base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=15 + ) + + +def stub_shipengine_instance() -> ShipEngine: + """Return a test instance of the ShipEngine object.""" + return ShipEngine(stub_config()) + + +def valid_residential_address() -> Address: + """ + Return a test Address object with valid residential + address information. + """ + return Address( + street=["4 Jersey St", "Apt. 2b"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + +# class TestValidateAddress: +# def test_valid_residential_address(self) -> None: +# shipengine = stub_shipengine_instance() +# validated_address = shipengine.validate_address(valid_residential_address()) +# assert type(validated_address) is AddressValidateResult From d99462c92182f62d070f2ab9b9efb62a74f6d900 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Thu, 27 May 2021 20:07:55 -0500 Subject: [PATCH 14/41] work in progress --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0039f46..1f972b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,6 @@ repos: hooks: - id: black args: [ "--safe", "--diff", "--color"] - verbose: true - repo: https://github.com/psf/black rev: 20.8b1 hooks: From 1c2db5cae0fa65b48e265bcf98128117605c7a82 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Thu, 27 May 2021 23:06:43 -0500 Subject: [PATCH 15/41] Acceptance criteria per DX-1024 - Valid residential address --- tests/services/test_address_validation.py | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/services/test_address_validation.py b/tests/services/test_address_validation.py index 687d53b..e130959 100644 --- a/tests/services/test_address_validation.py +++ b/tests/services/test_address_validation.py @@ -1,6 +1,6 @@ """Initial Docstring""" from shipengine_sdk import ShipEngine -from shipengine_sdk.models.address import Address +from shipengine_sdk.models.address import Address, AddressValidateResult from shipengine_sdk.models.enums import Endpoints @@ -33,8 +33,23 @@ def valid_residential_address() -> Address: ) -# class TestValidateAddress: -# def test_valid_residential_address(self) -> None: -# shipengine = stub_shipengine_instance() -# validated_address = shipengine.validate_address(valid_residential_address()) -# assert type(validated_address) is AddressValidateResult +class TestValidateAddress: + def test_valid_residential_address(self): + """DX-1024 - Valid residential address""" + shipengine = stub_shipengine_instance() + valid_address = valid_residential_address() + validated_address = shipengine.validate_address(valid_address) + address = validated_address.normalized_address + + assert type(validated_address) is AddressValidateResult + assert validated_address.is_valid is True + assert address is not None + assert ( + address.street[0] + == (valid_address.street[0] + " " + valid_address.street[1]).replace(".", "").upper() + ) + assert address.city_locality == valid_address.city_locality.upper() + assert address.state_province == valid_address.state_province.upper() + assert address.postal_code == valid_address.postal_code + assert address.country_code == valid_address.country_code.upper() + assert address.is_residential is True From 937bb5008c63f4ff84c16cdc1878f50371c723eb Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Thu, 27 May 2021 23:46:06 -0500 Subject: [PATCH 16/41] Acceptance criteria per DX-1446 - Invalid retries in method call configuration. --- .pre-commit-config.yaml | 1 + shipengine_sdk/http_client/client.py | 3 +- shipengine_sdk/jsonrpc/__init__.py | 2 +- shipengine_sdk/models/address/__init__.py | 11 +-- shipengine_sdk/services/address_validation.py | 14 +++- shipengine_sdk/shipengine.py | 2 +- shipengine_sdk/util/sdk_assertions.py | 9 +++ tests/test_shipengine_config.py | 69 +++++++++++++++---- 8 files changed, 89 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f972b4..0039f46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ repos: hooks: - id: black args: [ "--safe", "--diff", "--color"] + verbose: true - repo: https://github.com/psf/black rev: 20.8b1 hooks: diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index ce98b53..1fa5957 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -1,4 +1,5 @@ """A Python library for ShipEngine API.""" +import json import os import platform from typing import Dict, Optional, Union @@ -66,7 +67,7 @@ def send_rpc_request( req: Request = Request( method="POST", url=base_uri, - data=request_body, + data=json.dumps(request_body), headers=request_headers, auth=ShipEngineAuth(config.api_key), ) diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index cd18dff..351f26f 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -41,4 +41,4 @@ def rpc_request_loop(method: str, params: dict, config: ShipEngineConfig) -> dic else: raise err retry += 1 - return api_response + return api_response diff --git a/shipengine_sdk/models/address/__init__.py b/shipengine_sdk/models/address/__init__.py index ebb71e5..0aacafd 100644 --- a/shipengine_sdk/models/address/__init__.py +++ b/shipengine_sdk/models/address/__init__.py @@ -14,7 +14,7 @@ class Address: state_province: str postal_code: str country_code: str - is_residential: Optional[bool] = None + is_residential: bool = False name: str = "" phone: str = "" company: str = "" @@ -25,7 +25,8 @@ class Address: class AddressValidateResult: is_valid: Optional[bool] request_id: str - address: Address - info: List - warnings: List - errors: List + normalized_address: Optional[Address] + messages: List + # info: List + # warnings: List + # errors: List diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py index 0d47875..5f0bbec 100644 --- a/shipengine_sdk/services/address_validation.py +++ b/shipengine_sdk/services/address_validation.py @@ -14,10 +14,20 @@ def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResul :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the validated and normalized address. """ - api_response = rpc_request( + api_response: dict = rpc_request( method=RPCMethods.ADDRESS_VALIDATE.value, config=config, params={"address": address.to_dict()}, ) + result = api_response["result"] + return AddressValidateResult( + is_valid=result["isValid"], + request_id=api_response["id"], + normalized_address=Address.from_dict(result["normalizedAddress"]), + messages=result["messages"], + ) + - return AddressValidateResult(api_response) +def normalize(address: Address, config: ShipEngineConfig) -> Address: + validation_result = validate(address=address, config=config) + return validation_result.normalized_address diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index bf4e46c..10bcb34 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -15,7 +15,7 @@ class ShipEngine: unless specifically overridden when calling a method. """ - def __init__(self, config: Union[str, Dict[str, any]]) -> None: + def __init__(self, config: Union[str, Dict[str, any], ShipEngineConfig]) -> None: """ Exposes the functionality of the ShipEngine API. diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 0c02d10..ce183c7 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -76,6 +76,15 @@ def api_key_validation_error_assertions(error) -> None: assert error.message == "A ShipEngine API key must be specified." +def timeout_validation_error_assertions(error) -> None: + """Helper test function that has common assertions pertaining to InvalidFieldValueError.""" + assert type(error) is InvalidFieldValueError + assert error.request_id is None + assert error.error_type is ErrorType.VALIDATION.value + assert error.error_code is ErrorCode.INVALID_FIELD_VALUE.value + assert error.source is ErrorSource.SHIPENGINE.value + + def is_response_404(status_code: int, response_body: dict) -> None: """Check if status_code is 404 and raises an error if so.""" if "error" in response_body: diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index 09288a2..9381720 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -1,11 +1,36 @@ """Testing the ShipEngineConfig object.""" import pytest -from shipengine_sdk import ShipEngineConfig +from shipengine_sdk import ShipEngine, ShipEngineConfig from shipengine_sdk.errors import InvalidFieldValueError, ValidationError -from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType +from shipengine_sdk.models.address import Address from shipengine_sdk.models.enums import Endpoints from shipengine_sdk.util import api_key_validation_error_assertions +from shipengine_sdk.util.sdk_assertions import timeout_validation_error_assertions + + +def stub_config() -> dict: + """ + Return a test configuration dictionary to be used + when instantiating the ShipEngine object. + """ + return dict( + api_key="baz", base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=15 + ) + + +def valid_residential_address() -> Address: + """ + Return a test Address object with valid residential + address information. + """ + return Address( + street=["4 Jersey St", "Apt. 2b"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) def config_with_no_api_key() -> ShipEngineConfig: @@ -111,11 +136,7 @@ def test_invalid_retries_provided(self): try: set_config_retries(retries) except InvalidFieldValueError as e: - assert type(e) is InvalidFieldValueError - assert e.request_id is None - assert e.error_type is ErrorType.VALIDATION.value - assert e.error_code is ErrorCode.INVALID_FIELD_VALUE.value - assert e.source is ErrorSource.SHIPENGINE.value + timeout_validation_error_assertions(e) assert ( e.message == f"retries - Retries must be zero or greater. {retries} was provided." ) @@ -128,11 +149,35 @@ def test_invalid_timeout_provided(self): try: set_config_timeout(timeout) except InvalidFieldValueError as e: - assert type(e) is InvalidFieldValueError - assert e.request_id is None - assert e.error_type is ErrorType.VALIDATION.value - assert e.error_code is ErrorCode.INVALID_FIELD_VALUE.value - assert e.source is ErrorSource.SHIPENGINE.value + timeout_validation_error_assertions(e) assert ( e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." ) + + def test_invalid_timeout_in_method_call(self): + """DX-1447 - Invalid timeout in method call configuration.""" + timeout = -5 + try: + shipengine = ShipEngine(stub_config()) + shipengine.validate_address( + address=valid_residential_address(), config=dict(timeout=timeout) + ) + except InvalidFieldValueError as e: + timeout_validation_error_assertions(e) + assert ( + e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." + ) + + def test_invalid_retries_in_method_call(self): + """DX-1446 - Invalid retries in method call configuration.""" + retries = -5 + try: + shipengine = ShipEngine(stub_config()) + shipengine.validate_address( + address=valid_residential_address(), config=dict(retries=retries) + ) + except InvalidFieldValueError as e: + timeout_validation_error_assertions(e) + assert ( + e.message == f"retries - Retries must be zero or greater. {retries} was provided." + ) From db711913fcc28c280a0562f49a327322c9debcf8 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 00:03:39 -0500 Subject: [PATCH 17/41] Acceptance criteria per DX-1445 - Invalid api_key in method call configuration. --- tests/test_shipengine_config.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index 9381720..562dd5f 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -181,3 +181,14 @@ def test_invalid_retries_in_method_call(self): assert ( e.message == f"retries - Retries must be zero or greater. {retries} was provided." ) + + def test_invalid_api_key_in_method_call(self): + """DX-1445 - Invalid api_key in method call configuration.""" + api_key = " " + try: + shipengine = ShipEngine(stub_config()) + shipengine.validate_address( + address=valid_residential_address(), config=dict(api_key=api_key) + ) + except Exception as e: + api_key_validation_error_assertions(e) From 7caa67a0704a228bf68c5d8edde93045561fe590 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 00:04:29 -0500 Subject: [PATCH 18/41] Added a regex to check for whitespace chars in api_key field --- shipengine_sdk/util/sdk_assertions.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index ce183c7..51b5ef7 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -1,4 +1,6 @@ """Assertion helper functions.""" +import re + from shipengine_sdk.errors import ( ClientSystemError, ClientTimeoutError, @@ -17,9 +19,18 @@ def is_api_key_valid(config: dict) -> None: :returns: None, only raises exceptions. :rtype: None """ + message = "A ShipEngine API key must be specified." if "api_key" not in config or config["api_key"] == "": raise ValidationError( - message="A ShipEngine API key must be specified.", + message=message, + source=ErrorSource.SHIPENGINE.value, + error_type=ErrorType.VALIDATION.value, + error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, + ) + + if re.match(r"\s", config["api_key"]): + raise ValidationError( + message=message, source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, From 99a4712d2fd6c1dcaf4ddf71f7ed1f24b372b861 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 00:51:17 -0500 Subject: [PATCH 19/41] added coveralls --- .github/workflows/main.yml | 4 ++ poetry.lock | 53 ++++++++++++++++++++++++++- pyproject.toml | 2 + shipengine_sdk/util/__init__.py | 2 +- shipengine_sdk/util/sdk_assertions.py | 6 +-- tests/http_client/test_http_client.py | 48 ++++++++++++++++++++++++ tests/test___init__.py | 8 ++++ 7 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 tests/http_client/test_http_client.py create mode 100644 tests/test___init__.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41a282e..59ddc92 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,3 +52,7 @@ jobs: - name: Test with pytest and coverage via Tox run: | tox + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/poetry.lock b/poetry.lock index ef5d345..c1ff5ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -154,6 +154,22 @@ toml = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["toml"] +[[package]] +name = "coveralls" +version = "3.1.0" +description = "Show coverage stats online via coveralls.io" +category = "dev" +optional = false +python-versions = ">= 3.5" + +[package.dependencies] +coverage = ">=4.1,<6.0" +docopt = ">=0.6.1" +requests = ">=1.0.0" + +[package.extras] +yaml = ["PyYAML (>=3.10)"] + [[package]] name = "dataclasses-json" version = "0.5.3" @@ -179,6 +195,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "docutils" version = "0.16" @@ -533,6 +557,22 @@ urllib3 = ">=1.21.1,<1.27" security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +[[package]] +name = "responses" +version = "0.13.3" +description = "A utility library for mocking out the `requests` Python library." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +requests = ">=2.0" +six = "*" +urllib3 = ">=1.25.10" + +[package.extras] +tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] + [[package]] name = "six" version = "1.16.0" @@ -786,7 +826,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "01b596eb1c20c79ed921218837d93cab601909e31299d77392ac5405aed7867f" +content-hash = "d97d275553d9fb816a0b9191f3edf0a090c8d271d96a62e2cdbd60302e9a76f5" [metadata.files] aiohttp = [ @@ -929,6 +969,10 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] +coveralls = [ + {file = "coveralls-3.1.0-py2.py3-none-any.whl", hash = "sha256:172fb79c5f61c6ede60554f2cac46deff6d64ee735991fb2124fb414e188bdb4"}, + {file = "coveralls-3.1.0.tar.gz", hash = "sha256:9b3236e086627340bf2c95f89f757d093cbed43d17179d3f4fb568c347e7d29a"}, +] dataclasses-json = [ {file = "dataclasses-json-0.5.3.tar.gz", hash = "sha256:fe17da934cfc4ec792ebe7e9a303434ecf4f5f8d8a7705acfbbe7ccbd34bf1ae"}, {file = "dataclasses_json-0.5.3-py3-none-any.whl", hash = "sha256:740e7b564d72ddaa0f66406b4ecb799447afda2799c1c425a4a76151bfcfda50"}, @@ -937,6 +981,9 @@ distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, @@ -1202,6 +1249,10 @@ requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] +responses = [ + {file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"}, + {file = "responses-0.13.3.tar.gz", hash = "sha256:18a5b88eb24143adbf2b4100f328a2f5bfa72fbdacf12d97d41f07c26c45553d"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index f411d25..700fc1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ coverage = "^5.5" pytest-cov = "^2.11.1" pre-commit = "^2.11.0" isort = "^5.8.0" +responses = "^0.13.3" +coveralls = "^3.1.0" [tool.black] line-length = 100 diff --git a/shipengine_sdk/util/__init__.py b/shipengine_sdk/util/__init__.py index 29de2af..d321a47 100644 --- a/shipengine_sdk/util/__init__.py +++ b/shipengine_sdk/util/__init__.py @@ -1,4 +1,4 @@ -"""Initial Docstring""" +"""Testing a string manipulation helper function.""" from shipengine_sdk.util.sdk_assertions import ( api_key_validation_error_assertions, is_api_key_valid, diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 51b5ef7..fc2d4df 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -113,18 +113,16 @@ def is_response_404(status_code: int, response_body: dict) -> None: def is_response_429(status_code: int, response_body: dict, config) -> None: """Check if status_code is 429 and raises an error if so.""" - if "error" in response_body: + if "error" in response_body and status_code == 429: error = response_body["error"] retry_after = error["data"]["retryAfter"] - if retry_after > config.timeout: raise ClientTimeoutError( retry_after=config.timeout, source=ErrorSource.SHIPENGINE.value, request_id=response_body["id"], ) - - if status_code == 429: + else: raise RateLimitExceededError( retry_after=retry_after, source=ErrorSource.SHIPENGINE.value, diff --git a/tests/http_client/test_http_client.py b/tests/http_client/test_http_client.py new file mode 100644 index 0000000..e7c0bfe --- /dev/null +++ b/tests/http_client/test_http_client.py @@ -0,0 +1,48 @@ +"""Testing basic ShipEngineClient functionality.""" +import pytest +import responses + +from shipengine_sdk import ShipEngine +from shipengine_sdk.errors import ClientSystemError +from shipengine_sdk.models.address import Address +from shipengine_sdk.models.enums import Endpoints + + +def get_500_server_error(): + error_address = Address( + street=["500 Server Error"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + shipengine = ShipEngine( + dict( + api_key="baz", + base_uri=Endpoints.TEST_RPC_URL.value, + page_size=50, + retries=2, + timeout=10, + ) + ) + return shipengine.validate_address(error_address) + + +@responses.activate +def test_500_server_response(): + responses.add( + responses.POST, + Endpoints.TEST_RPC_URL.value, + json={ + "jsonrpc": "2.0", + "id": "req_DezVNUvRkAP819f3JeqiuS", + "error": { + "code": "-32603", + "message": "Unable to connect to the database", + "data": {"source": "shipengine", "type": "system", "code": "unspecified"}, + }, + }, + status=500, + ) + with pytest.raises(ClientSystemError): + get_500_server_error() diff --git a/tests/test___init__.py b/tests/test___init__.py new file mode 100644 index 0000000..b2f5f60 --- /dev/null +++ b/tests/test___init__.py @@ -0,0 +1,8 @@ +"""Initial Docstring""" +from shipengine_sdk.util import snake_to_camel + + +class TestSnakeToCamelCase: + def test_snake_to_camel(self): + camel_case = snake_to_camel("python_is_awesome") + assert camel_case == "pythonIsAwesome" From c90afc43bdd4e5994a792faab6f494a4ba3b13e3 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 00:54:46 -0500 Subject: [PATCH 20/41] added coveralls --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6f317da..97c4518 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,7 @@ deps = pytest-cov flake8 coverage + responses commands = pytest {posargs:} From c46b35c2f781526ebf143fe76d9155c8d0af67ad Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 09:37:21 -0500 Subject: [PATCH 21/41] updating coveralls in CI/CD --- .github/workflows/main.yml | 4 ---- tox.ini | 10 ++++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59ddc92..41a282e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,3 @@ jobs: - name: Test with pytest and coverage via Tox run: | tox - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/tox.ini b/tox.ini index 97c4518..0723653 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,14 @@ basepython = python3.7 [testenv] description = run tests with pytest under {basepython} +passenv = + GITHUB_ACTIONS + GITHUB_TOKEN + GITHUB_REF + GITHUB_SHA + GITHUB_HEAD_REF + GITHUB_REPOSITORY + GITHUB_RUN_ID setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} @@ -26,9 +34,11 @@ deps = pytest-cov flake8 coverage + coveralls responses commands = pytest {posargs:} + coveralls [testenv:lint] skip_install = True From 152e060e183410a3b669080625dabd152da77339 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 09:44:23 -0500 Subject: [PATCH 22/41] updating coveralls in CI/CD --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41a282e..3e3c9fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,6 +49,10 @@ jobs: - name: Run linting environment and pre-commit hooks run: | tox -e linting + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Test with pytest and coverage via Tox run: | tox + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 8fd5ef021f7c513171df7b190fef643646aca2f8 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 09:51:31 -0500 Subject: [PATCH 23/41] updating coveralls in CI/CD --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0723653..a86979c 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ deps = responses commands = pytest {posargs:} - coveralls + coveralls --submit={toxworkdir}/.coverage.py37 [testenv:lint] skip_install = True From 07bbbf0dc0560d9c89bf1e8b1b479d36259625ad Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:00:34 -0500 Subject: [PATCH 24/41] updating coveralls in CI/CD --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e3c9fa..39d74d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,8 +51,12 @@ jobs: tox -e linting env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_FLAG_NAME: ${{ matrix.python-version }} + COVERALLS_PARALLEL: true - name: Test with pytest and coverage via Tox run: | tox env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_FLAG_NAME: ${{ matrix.python-version }} + COVERALLS_PARALLEL: true From 7a92554d1035fc2c04112053d36714969d523dbc Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:04:50 -0500 Subject: [PATCH 25/41] updating coveralls in CI/CD --- .github/workflows/main.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 39d74d4..4a93f06 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,7 +48,7 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics - name: Run linting environment and pre-commit hooks run: | - tox -e linting + tox -e lint env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.python-version }} diff --git a/tox.ini b/tox.ini index a86979c..8e15056 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ deps = responses commands = pytest {posargs:} - coveralls --submit={toxworkdir}/.coverage.py37 + coveralls --submit={env:COVERAGE_FILE:{toxworkdir}/.coverage.py37} [testenv:lint] skip_install = True From 811c3e60f6921bd11dc1f00bea313972e68402cc Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:09:42 -0500 Subject: [PATCH 26/41] updating coveralls in CI/CD from tox.ini --- tests/errors/test_errors.py | 18 ++++++++++-------- tox.ini | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/errors/test_errors.py b/tests/errors/test_errors.py index d763ac6..4b5c51d 100644 --- a/tests/errors/test_errors.py +++ b/tests/errors/test_errors.py @@ -1,14 +1,16 @@ """Tests for the ShipEngine SDK Errors""" import pytest -from shipengine_sdk.errors import AccountStatusError -from shipengine_sdk.errors import BusinessRuleError -from shipengine_sdk.errors import ClientSecurityError -from shipengine_sdk.errors import ClientTimeoutError -from shipengine_sdk.errors import InvalidFieldValueError -from shipengine_sdk.errors import RateLimitExceededError -from shipengine_sdk.errors import ShipEngineError -from shipengine_sdk.errors import ValidationError +from shipengine_sdk.errors import ( + AccountStatusError, + BusinessRuleError, + ClientSecurityError, + ClientTimeoutError, + InvalidFieldValueError, + RateLimitExceededError, + ShipEngineError, + ValidationError, +) def shipengine_error(): diff --git a/tox.ini b/tox.ini index 8e15056..9373750 100644 --- a/tox.ini +++ b/tox.ini @@ -43,7 +43,7 @@ commands = [testenv:lint] skip_install = True deps = pre-commit>=2.9.3 -commands = pre-commit run --all-files --show-diff-on-failure {posargs:} +commands = pre-commit run --show-diff-on-failure {posargs:} [flake8] max-line-length = 100 From 52fa53a4e9d78c45b614b35aaf1fd1292f67f7d2 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:12:53 -0500 Subject: [PATCH 27/41] updating coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 4 ---- tox.ini | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4a93f06..e5c17b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,10 +49,6 @@ jobs: - name: Run linting environment and pre-commit hooks run: | tox -e lint - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: ${{ matrix.python-version }} - COVERALLS_PARALLEL: true - name: Test with pytest and coverage via Tox run: | tox diff --git a/tox.ini b/tox.ini index 9373750..4d6e604 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ deps = responses commands = pytest {posargs:} - coveralls --submit={env:COVERAGE_FILE:{toxworkdir}/.coverage.py37} + coveralls [testenv:lint] skip_install = True From fc686117a0fb5e3d463113bcf1b86ececc32db95 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:15:20 -0500 Subject: [PATCH 28/41] debugging coveralls in CI/CD from tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4d6e604..c713d42 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ deps = responses commands = pytest {posargs:} - coveralls + coveralls --submit=.tox/.coverage.py37} [testenv:lint] skip_install = True From b8df57a592c86d943bc99bde834c628fb5aa9966 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:19:52 -0500 Subject: [PATCH 29/41] debugging coveralls in CI/CD from tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c713d42..ddfb6b0 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ deps = responses commands = pytest {posargs:} - coveralls --submit=.tox/.coverage.py37} + coveralls --submit=.tox/.coverage.py37 [testenv:lint] skip_install = True From 708b0ffb04bcd34da9664ddfdd9688ad1572f326 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:35:45 -0500 Subject: [PATCH 30/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 4 +++- tox.ini | 10 ---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5c17b7..84e189b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,9 @@ jobs: - name: Test with pytest and coverage via Tox run: | tox - env: + - name: Coveralls + uses: coverallsapp/github-action@master + with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.python-version }} COVERALLS_PARALLEL: true diff --git a/tox.ini b/tox.ini index ddfb6b0..82fae48 100644 --- a/tox.ini +++ b/tox.ini @@ -17,14 +17,6 @@ basepython = python3.7 [testenv] description = run tests with pytest under {basepython} -passenv = - GITHUB_ACTIONS - GITHUB_TOKEN - GITHUB_REF - GITHUB_SHA - GITHUB_HEAD_REF - GITHUB_REPOSITORY - GITHUB_RUN_ID setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} @@ -34,11 +26,9 @@ deps = pytest-cov flake8 coverage - coveralls responses commands = pytest {posargs:} - coveralls --submit=.tox/.coverage.py37 [testenv:lint] skip_install = True From e665663db3fd30ea6efdadbd46036add99b54c66 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:41:21 -0500 Subject: [PATCH 31/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84e189b..fd13da0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,6 +55,5 @@ jobs: - name: Coveralls uses: coverallsapp/github-action@master with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: ${{ matrix.python-version }} - COVERALLS_PARALLEL: true + github_token: ${{ secrets.GITHUB_TOKEN }} + flag-name: ${{ matrix.python-version }} From 59833feb753e8631904a3c633f67dc4224368f31 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 10:58:30 -0500 Subject: [PATCH 32/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 42 +++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fd13da0..c76a018 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,7 @@ on: - main jobs: - build: - + lint-test-coverage: runs-on: ubuntu-latest strategy: matrix: @@ -49,11 +48,34 @@ jobs: - name: Run linting environment and pre-commit hooks run: | tox -e lint - - name: Test with pytest and coverage via Tox - run: | - tox - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - flag-name: ${{ matrix.python-version }} + + + tox-coveralls: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Tox testenv and coveralls + run: | + tox + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: ${{ matrix.python-version }} + parallel: true From 89ab9faa492fd903ec473f63d06f67b9d31e1dbe Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 11:01:41 -0500 Subject: [PATCH 33/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c76a018..0af6df4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,6 +69,12 @@ jobs: with: python-version: 3.9 + - name: Install python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Tox testenv and coveralls run: | tox From b10dce6e36eff9ae7459e2109e9225083bcd8ab0 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 11:41:46 -0500 Subject: [PATCH 34/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 16 +++++++++++++--- pyproject.toml | 3 +++ tox.ini | 9 +++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0af6df4..4c5881a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,10 +20,12 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Cache pip uses: actions/cache@v2 with: @@ -80,8 +82,16 @@ jobs: tox - name: Coveralls - uses: coverallsapp/github-action@master + uses: AndreMiras/coveralls-python-action@develop with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: ${{ matrix.python-version }} parallel: true + flag-name: Python Test Suite + + coveralls_finish: + needs: tox-coveralls + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true diff --git a/pyproject.toml b/pyproject.toml index 700fc1a..fc3d5df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ safe = true profile = "black" multi_line_output = 3 +[tool.coverage.run] +relative_files = true + [tool.poetry.scripts] [build-system] diff --git a/tox.ini b/tox.ini index 82fae48..1944568 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,11 @@ basepython = python3.7 [testenv] description = run tests with pytest under {basepython} +passenv = + GITHUB_TOKEN + GITHUB_ACTION + GITHUB_REPOSITORY + GITHUB_RUN_ID setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} @@ -26,6 +31,7 @@ deps = pytest-cov flake8 coverage + coveralls responses commands = pytest {posargs:} @@ -35,6 +41,9 @@ skip_install = True deps = pre-commit>=2.9.3 commands = pre-commit run --show-diff-on-failure {posargs:} +[coverage:run] +relative_files = True + [flake8] max-line-length = 100 per-file-ignores = From 7b82871f9b7307f23ea407bce752dc5edd2b6bf0 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 11:45:32 -0500 Subject: [PATCH 35/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c5881a..7cd6f4f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,7 +86,6 @@ jobs: with: parallel: true flag-name: Python Test Suite - coveralls_finish: needs: tox-coveralls runs-on: ubuntu-latest From b2de7ff163c466bae321c755e881db961d6e1e98 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 11:49:44 -0500 Subject: [PATCH 36/41] debugging coveralls in CI/CD from tox.ini --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 1944568..e068e20 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,7 @@ deps = responses commands = pytest {posargs:} + coveralls [testenv:lint] skip_install = True From 842b6283e67d6db6577d27252dd2feb5e122ad46 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 11:51:46 -0500 Subject: [PATCH 37/41] debugging coveralls in CI/CD from tox.ini --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index e068e20..16be80c 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ passenv = setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} + COVERALLS_REPO_TOKEN = GITHUB_TOKEN changedir = tests deps = pytest From 9f387f6d70d8ba154d97bfb6d2d57543c3d25c58 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 11:53:52 -0500 Subject: [PATCH 38/41] debugging coveralls in CI/CD from tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 16be80c..0c41107 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ deps = responses commands = pytest {posargs:} - coveralls + coveralls --submit={toxworkdir}/.coverage.{envname} [testenv:lint] skip_install = True From fddad7ffaaa72d8b9e9fb75ea02a9a49764d4109 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 12:03:39 -0500 Subject: [PATCH 39/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 9 +++++++-- tox.ini | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7cd6f4f..9967d0f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -74,18 +74,23 @@ jobs: - name: Install python dependencies run: | python -m pip install --upgrade pip - python -m pip install tox + python -m pip install tox pytest pytest-cov coverage if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Tox testenv and coveralls + - name: Tox testenv run: | tox + - name: Pytest + run: | + pytest + - name: Coveralls uses: AndreMiras/coveralls-python-action@develop with: parallel: true flag-name: Python Test Suite + coveralls_finish: needs: tox-coveralls runs-on: ubuntu-latest diff --git a/tox.ini b/tox.ini index 0c41107..114a92e 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ deps = responses commands = pytest {posargs:} - coveralls --submit={toxworkdir}/.coverage.{envname} +; coveralls --submit={toxworkdir}/.coverage.{envname} [testenv:lint] skip_install = True From def7783fd15913f756a5a99534380fafbf75b539 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 12:05:51 -0500 Subject: [PATCH 40/41] debugging coveralls in CI/CD from tox.ini --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9967d0f..3cc5b97 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -74,7 +74,7 @@ jobs: - name: Install python dependencies run: | python -m pip install --upgrade pip - python -m pip install tox pytest pytest-cov coverage + python -m pip install tox pytest pytest-cov coverage responses if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Tox testenv From 9eba013b4607e9b5fc783b983eeadc3fd19a8dbb Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 28 May 2021 12:24:00 -0500 Subject: [PATCH 41/41] debugging coveralls in CI/CD from tox.ini --- requirements.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cc82309 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +aiohttp==3.7.4.post0; python_version >= "3.6" +async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" +attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +certifi==2020.12.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +dataclasses-json==0.5.3; python_version >= "3.6" +idna==2.10; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +marshmallow==3.12.1; python_version >= "3.6" +marshmallow-enum==1.5.1; python_version >= "3.6" +multidict==5.1.0; python_version >= "3.6" +mypy-extensions==0.4.3; python_version >= "3.6" +python-dotenv==0.15.0 +requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +stringcase==1.2.0; python_version >= "3.6" +typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" +typing-inspect==0.6.0; python_version >= "3.6" +urllib3==1.26.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" +yarl==1.6.3; python_version >= "3.6"