diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed89b7b..bbd1484 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,7 @@ jobs: tox -e lint - tox-coveralls: + coveralls: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -61,26 +61,12 @@ jobs: 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: Install python dependencies run: | python -m pip install --upgrade pip python -m pip install tox pytest pytest-cov coverage responses if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Tox testenv - run: | - tox - - name: Pytest run: | pytest diff --git a/README.md b/README.md index bd2e98d..016cc5e 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,13 @@ on `osx / linux / bashonwindows`: curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - ``` -- Once you have **Poetry** installed you need to install the projects dependencies with this command: +- Once you have **Poetry** installed you need to install the projects dependencies with this command from the project root: ```bash -poetry install +bin/setup ``` - -- After you have installed **Poetry**, you need to start the python environment managed by Poetry by - running `poetry shell` in your terminal. - -```bash -poetry shell - ``` +- This script will install all dependencies specified in `pyproject.toml` via `Poetry` and install the `pre-commit` hooks +this project uses. ## Adding dependencies to the project If your changes require you to install a python package/module using `poetry add ` or @@ -100,21 +95,11 @@ poetry export -f requirements.txt --output requirements.txt --without-hashes --d ## Pre-Commit Hooks We are using [Pre-Commit](https://pre-commit.com/) to enforce formatting, lint rules, and code analysis so that this repo is always in good health. - - If you choose not to globally install `pre-commit`, then you can skip installing via `pip` or `homebrew` directly. - You can simply run either `pip install -r requirements.txt` or `poetry install` -To be able to push a PR to the repo after making changes locally, you will need to install `pre-commit` which -is a tool that runs linting, formatting, and code analysis on your changes. -```bash -pip install pre-commit # Install via pip - -OR +- `Pre-Commit` is installed and initialized when you run `bin/setup` from the project root as outlined above. -brew install pre-commit # Install via homebrew -``` -- After you have run either `pip install -r requirements.txt`, `poetry install`, or globally installed - [Pre-Commit](https://pre-commit.com/) using the above commands you need to run the following command - in the project directory locally. This allows the pre-commit hooks to run when you are looking to commit - and push code to this repository. +- If you choose not to use `Poetry` and prefer `pip` you can simply run `pip install -r requirements.txt` +To be able to commit & push a PR to the repo after making changes locally, you will need to install `pre-commit` which +is a tool that runs tests, linting, formatting, and code analysis on your changes. ```bash pre-commit install ``` diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..1ef36e2 --- /dev/null +++ b/bin/setup @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +poetry install +pre-commit install +poetry shell diff --git a/poetry.lock b/poetry.lock index fdfb143..4e38846 100644 --- a/poetry.lock +++ b/poetry.lock @@ -219,6 +219,17 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + [[package]] name = "filelock" version = "3.0.12" @@ -509,6 +520,18 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-cache" +version = "1.0" +description = "pytest plugin with mechanisms for caching across test runs" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +execnet = ">=1.1.dev1" +pytest = ">=2.2" + [[package]] name = "pytest-cov" version = "2.12.1" @@ -539,6 +562,20 @@ pytest = ">=5.0" [package.extras] dev = ["pre-commit", "tox", "pytest-asyncio"] +[[package]] +name = "pytest-watch" +version = "4.2.0" +description = "Local continuous test runner with pytest and watchdog." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = ">=0.3.3" +docopt = ">=0.4.0" +pytest = ">=2.6.4" +watchdog = ">=0.6.0" + [[package]] name = "python-dotenv" version = "0.15.0" @@ -825,6 +862,17 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +[[package]] +name = "watchdog" +version = "2.1.3" +description = "Filesystem events monitoring" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] + [[package]] name = "yarl" version = "1.6.3" @@ -853,7 +901,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "283e5c399488f650f34c803603e03da438fed630ae384b275e070f96c37e2fa4" +content-hash = "44a12436d6071b62002beab90bd98605720f47f4c08c492d52f1b482efaeadbf" [metadata.files] aiohttp = [ @@ -1019,6 +1067,10 @@ docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] +execnet = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, @@ -1193,6 +1245,9 @@ pytest = [ {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] +pytest-cache = [ + {file = "pytest-cache-1.0.tar.gz", hash = "sha256:be7468edd4d3d83f1e844959fd6e3fd28e77a481440a7118d430130ea31b07a9"}, +] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, @@ -1201,6 +1256,9 @@ pytest-mock = [ {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, ] +pytest-watch = [ + {file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"}, +] python-dotenv = [ {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, @@ -1388,6 +1446,29 @@ virtualenv = [ {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, ] +watchdog = [ + {file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"}, + {file = "watchdog-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acc4e2d5be6f140f02ee8590e51c002829e2c33ee199036fcd61311d558d89f4"}, + {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b851237cf3533fabbc034ffcd84d0fa52014b3121454e5f8b86974b531560c"}, + {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a12539ecf2478a94e4ba4d13476bb2c7a2e0a2080af2bb37df84d88b1b01358a"}, + {file = "watchdog-2.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6fe9c8533e955c6589cfea6f3f0a1a95fb16867a211125236c82e1815932b5d7"}, + {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d9456f0433845e7153b102fffeb767bde2406b76042f2216838af3b21707894e"}, + {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd8c595d5a93abd441ee7c5bb3ff0d7170e79031520d113d6f401d0cf49d7c8f"}, + {file = "watchdog-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0bcfe904c7d404eb6905f7106c54873503b442e8e918cc226e1828f498bdc0ca"}, + {file = "watchdog-2.1.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf84bd94cbaad8f6b9cbaeef43080920f4cb0e61ad90af7106b3de402f5fe127"}, + {file = "watchdog-2.1.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b8ddb2c9f92e0c686ea77341dcb58216fa5ff7d5f992c7278ee8a392a06e86bb"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8805a5f468862daf1e4f4447b0ccf3acaff626eaa57fbb46d7960d1cf09f2e6d"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:3e305ea2757f81d8ebd8559d1a944ed83e3ab1bdf68bcf16ec851b97c08dc035"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_i686.whl", hash = "sha256:431a3ea70b20962e6dee65f0eeecd768cd3085ea613ccb9b53c8969de9f6ebd2"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:e4929ac2aaa2e4f1a30a36751160be391911da463a8799460340901517298b13"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:201cadf0b8c11922f54ec97482f95b2aafca429c4c3a4bb869a14f3c20c32686"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:3a7d242a7963174684206093846537220ee37ba9986b824a326a8bb4ef329a33"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:54e057727dd18bd01a3060dbf5104eb5a495ca26316487e0f32a394fd5fe725a"}, + {file = "watchdog-2.1.3-py3-none-win32.whl", hash = "sha256:b5fc5c127bad6983eecf1ad117ab3418949f18af9c8758bd10158be3647298a9"}, + {file = "watchdog-2.1.3-py3-none-win_amd64.whl", hash = "sha256:44acad6f642996a2b50bb9ce4fb3730dde08f23e79e20cd3d8e2a2076b730381"}, + {file = "watchdog-2.1.3-py3-none-win_ia64.whl", hash = "sha256:0bcdf7b99b56a3ae069866c33d247c9994ffde91b620eaf0306b27e099bd1ae0"}, + {file = "watchdog-2.1.3.tar.gz", hash = "sha256:e5236a8e8602ab6db4b873664c2d356c365ab3cac96fbdec4970ad616415dd45"}, +] yarl = [ {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, diff --git a/pyproject.toml b/pyproject.toml index 7d73e52..1270093 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ isort = "^5.8.0" responses = "^0.13.3" coveralls = "^3.1.0" pre-commit = "2.13.0" +pytest-cache = "^1.0" +pytest-watch = "^4.2.0" [tool.black] line-length = 100 diff --git a/requirements.txt b/requirements.txt index 6c995d9..ec44660 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ dataclasses-json==0.5.4; python_version >= "3.6" distlib==0.3.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" docopt==0.6.2; python_version >= "3.5" docutils==0.16; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" +execnet==1.9.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" filelock==3.0.12; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") fuuid==0.1.0; python_version >= "3.7" and python_version < "4.0" @@ -45,8 +46,10 @@ pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or py pygments==2.9.0; python_version >= "3.5" pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" pytest==6.2.4; python_version >= "3.6" +pytest-cache==1.0 pytest-cov==2.12.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") pytest-mock==3.6.1; python_version >= "3.6" +pytest-watch==4.2.0 python-dotenv==0.15.0 pytz==2021.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" pyyaml==5.4.1; python_full_version >= "3.6.1" @@ -70,5 +73,6 @@ typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" typing-inspect==0.7.1; python_version >= "3.6" urllib3==1.26.6; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.5" virtualenv==20.4.7; python_full_version >= "3.6.1" +watchdog==2.1.3; python_version >= "3.6" yarl==1.6.3; python_version >= "3.6" zipp==3.5.0; python_version < "3.8" and python_version >= "3.6" diff --git a/shipengine_sdk/events/__init__.py b/shipengine_sdk/events/__init__.py index d54be51..d0a5cde 100644 --- a/shipengine_sdk/events/__init__.py +++ b/shipengine_sdk/events/__init__.py @@ -96,9 +96,11 @@ def __init__(self, events: Optional[List[str]] = None) -> None: def get_subscribers(self, event: Optional[str] = None): return self.events[event] - def register(self, event, subscriber, callback: Optional[Callable] = None): - if callback is None: - callback = getattr(subscriber, "update") + def register(self, event, subscriber, callback: Union[Callable, str] = None): + if event is Events.ON_REQUEST_SENT.value and callback is None: + callback = getattr(subscriber, "catch_request_sent_event") + elif event is Events.ON_RESPONSE_RECEIVED.value and callback is None: + callback = getattr(subscriber, "catch_response_received_event") self.get_subscribers(event)[subscriber] = callback def unregister(self, event, subscriber): @@ -126,6 +128,14 @@ def __init__(self, name=None) -> None: def update(event: Union[RequestSentEvent, ResponseReceivedEvent]): return event + @staticmethod + def catch_request_sent_event(event: RequestSentEvent): + return event + + @staticmethod + def catch_response_received_event(event: ResponseReceivedEvent): + return event + def to_dict(self): return (lambda o: o.__dict__)(self) @@ -163,7 +173,7 @@ def emit_event(emitted_event_type: str, event_data, dispatcher: Dispatcher): request_id=event_data.id, base_uri=event_data.base_uri, status_code=event_data.status_code, - headers=event_data.request_headers, + headers=event_data.response_headers, body=event_data.body, retry=event_data.retry, elapsed=event_data.elapsed, diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index 2e1a0da..ae2bf2a 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -33,7 +33,19 @@ def base_url(config) -> str: return config.base_uri if os.getenv("CLIENT_BASE_URI") is None else os.getenv("CLIENT_BASE_URI") -def generate_event_message(retry: int, method: str, base_uri: str) -> str: +def generate_event_message( + retry: int, + method: str, + base_uri: str, + status_code: Optional[int] = None, + message_type: Optional[str] = None, +) -> str: + if message_type == "received": + if retry > 0: + f"Retrying the ShipEngine {method} API at {base_uri}" + else: + return f"Received an HTTP {status_code} response from the ShipEngine {method} API" + if retry == 0: return ShipEngineEvent.new_event_message( method=method, base_uri=base_uri, message_type="base_message" @@ -44,11 +56,12 @@ def generate_event_message(retry: int, method: str, base_uri: str) -> str: ) -def request_headers(user_agent: str) -> Dict[str, Any]: +def request_headers(user_agent: str, api_key: str) -> Dict[str, Any]: return { "User-Agent": user_agent, "Content-Type": "application/json", "Accept": "application/json", + "Api-Key": api_key, } @@ -86,7 +99,7 @@ def send_rpc_request( base_uri = base_url(config=config) request_body: Dict[str, Any] = wrap_request(method=method, params=params) - req_headers = request_headers(self._derive_user_agent()) + req_headers = request_headers(user_agent=self._derive_user_agent(), api_key=config.api_key) req: Request = Request( method="POST", url=base_uri, @@ -128,8 +141,15 @@ def send_rpc_request( resp_body: Dict[str, Any] = resp.json() status_code: int = resp.status_code + response_received_message = generate_event_message( + retry=retry, + method=method, + base_uri=base_uri, + status_code=status_code, + message_type="received", + ) response_event_data = EventOptions( - message=request_event_message, + message=response_received_message, id=request_body["id"], base_uri=base_uri, status_code=status_code, @@ -175,8 +195,7 @@ def _derive_user_agent() -> str: :rtype: str """ sdk_version: str = f"shipengine-python/{__version__}" - os_kernel: str = platform.platform(terse=True) python_version: str = platform.python_version() python_implementation: str = platform.python_implementation() - return f"{sdk_version} {os_kernel} {python_version} {python_implementation}" + return f"{sdk_version} {python_implementation}-v{python_version}" diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index e6a3e3c..7502077 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -35,8 +35,8 @@ def rpc_request_loop( and err.retry_after < config.timeout ): time.sleep(err.retry_after) + retry += 1 continue else: raise err - retry += 1 return api_response diff --git a/shipengine_sdk/models/address/__init__.py b/shipengine_sdk/models/address/__init__.py index b55a71a..a47a399 100644 --- a/shipengine_sdk/models/address/__init__.py +++ b/shipengine_sdk/models/address/__init__.py @@ -22,7 +22,7 @@ class Address: state_province: str postal_code: str country_code: str - is_residential: Optional[bool] = None + is_residential: Optional[bool] = False name: Optional[str] = "" phone: Optional[str] = "" company: Optional[str] = "" diff --git a/shipengine_sdk/models/enums/__init__.py b/shipengine_sdk/models/enums/__init__.py index e0c228e..be38bf1 100644 --- a/shipengine_sdk/models/enums/__init__.py +++ b/shipengine_sdk/models/enums/__init__.py @@ -9,10 +9,16 @@ from .regex_patterns import RegexPatterns +class Constants(Enum): + """Test API Key for use with Simengine.""" + + STUB_API_KEY = "TEST_vMiVbICUjBz4BZjq0TRBLC/9MrxY4+yjvb1G1RMxlJs" + CARRIER_ACCOUNT_ID_STUB = "car_41GrQHn5uouiPZc2TNE6PU29tZU9ud" + + class Endpoints(Enum): """API Endpoint URI's used throughout the ShipEngine SDK.""" - TEST_RPC_URL = "https://shipengine-web-api.herokuapp.com/jsonrpc" SHIPENGINE_RPC_URL = "https://api.shipengine.com/jsonrpc" diff --git a/shipengine_sdk/models/package/__init__.py b/shipengine_sdk/models/package/__init__.py index 6332234..daeeff0 100644 --- a/shipengine_sdk/models/package/__init__.py +++ b/shipengine_sdk/models/package/__init__.py @@ -17,7 +17,7 @@ class Shipment: shipment_id: Optional[str] = None account_id: Optional[str] = None carrier_account: Optional[CarrierAccount] = None - carrier: Carrier + carrier: Optional[Carrier] = None estimated_delivery_date: Union[IsoString, str] actual_delivery_date: Union[IsoString, str] @@ -77,7 +77,7 @@ def to_json(self) -> str: return json.dumps(self, default=lambda o: o.__dict__, indent=2) def __repr__(self): - return f"Shipment({self.shipment_id}, {self.account_id}, {self.carrier_account}, {self.carrier}, {self.estimated_delivery_date}, {self.actual_delivery_date})" # noqa + return f"Shipment({self.shipment_id}, {self.account_id})" class Package: @@ -184,6 +184,9 @@ def __init__(self, event: Dict[str, Any]) -> None: self.carrier_status_code = ( event["carrierStatusCode"] if "carrierStatusCode" in event else None ) + self.carrier_detail_code = ( + event["carrierDetailCode"] if "carrierDetailCode" in event else None + ) self.signer = event["signer"] if "signer" in event else None self.location = ( Location(event["location"]) diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py index 4fc39f1..cdd02e3 100644 --- a/shipengine_sdk/util/iso_string.py +++ b/shipengine_sdk/util/iso_string.py @@ -11,9 +11,7 @@ def __init__(self, iso_string: str) -> None: A string representing a Date, DateTime, or DateTime with Timezone. The object also has a method to return a `datetime.datetime` object, which is the native datetime object in python as of 3.7. - This class object takes in an **ISO-8601** string. Learn more here: https://en.wikipedia.org/wiki/ISO_8601 - :param str iso_string: An `ISO-8601` string. Learn more here: https://en.wikipedia.org/wiki/ISO_8601 """ self.iso_string = iso_string @@ -28,15 +26,15 @@ def to_datetime_object(self) -> datetime: iso_string = self._maybe_add_microseconds(self.iso_string) if self.has_timezone(): return datetime.strptime(iso_string, "%Y-%m-%dT%H:%M:%S.%fZ") - elif self._is_valid_iso_string_no_tz(self.iso_string): + else: return datetime.fromisoformat(iso_string) def has_timezone(self) -> bool: - if self.is_valid_iso_string(self.iso_string): - return False if self._is_valid_iso_string_no_tz(self.iso_string) else True + if self.is_valid_iso_string_with_tz(self.iso_string): + return False if self.is_valid_iso_string_with_tz_no_tz(self.iso_string) else True @staticmethod - def is_valid_iso_string(iso_str: str): + def is_valid_iso_string_with_tz(iso_str: str): pattern = re.compile(RegexPatterns.VALID_ISO_STRING.value) if pattern.match(iso_str): return True @@ -44,7 +42,7 @@ def is_valid_iso_string(iso_str: str): return False @staticmethod - def _is_valid_iso_string_no_tz(iso_str: str): + def is_valid_iso_string_with_tz_no_tz(iso_str: str): pattern = re.compile(RegexPatterns.VALID_ISO_STRING_NO_TZ.value) if pattern.match(iso_str): return True diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 740fb6b..b45c2a0 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -199,7 +199,8 @@ def check_response_for_errors(status_code: int, response_body: Dict[str, Any], c # Check if status_code is 429 and raises an error if so. if "error" in response_body and status_code == 429: error = response_body["error"] - retry_after = error["data"]["retryAfter"] + error_data = error["data"] + retry_after = error_data["details"]["retryAfter"] if retry_after > config.timeout: raise ClientTimeoutError( retry_after=config.timeout, diff --git a/tests/events/test_emitted_events.py b/tests/events/test_emitted_events.py new file mode 100644 index 0000000..ab5a604 --- /dev/null +++ b/tests/events/test_emitted_events.py @@ -0,0 +1,238 @@ +"""Test that `RequestSentEvents` are emitted from the SDK properly.""" +from datetime import datetime + +from pytest_mock import MockerFixture + +from shipengine_sdk import __version__ +from shipengine_sdk.errors import ( + ClientTimeoutError, + RateLimitExceededError, + ShipEngineError, +) +from shipengine_sdk.events import ( + RequestSentEvent, + ResponseReceivedEvent, + ShipEngineEventListener, +) +from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType +from shipengine_sdk.models.enums import Constants + +from ..util import ( + assert_on_429_exception, + configurable_stub_shipengine_instance, + valid_residential_address, +) + + +class TestEmittedEvents: + def test_user_agent_includes_correct_sdk_version(self, mocker: MockerFixture) -> None: + """DX-1517 - Test user agent includes correct SDK version.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 1, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + shipengine.validate_address(address=valid_residential_address()) + request_sent_return = request_sent_spy.spy_return + assert request_sent_return.headers["User-Agent"].split(" ")[0].split("/")[1] == __version__ + + def test_request_sent_event_on_retries(self, mocker: MockerFixture) -> None: + """DX-1521 - Test that a RequestSentEvent is emitted on retries.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 1, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 2 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 1 + assert type(request_sent_return.timestamp) == datetime + assert request_sent_return.timeout == config["timeout"] + assert request_sent_return.body["method"] == "carrier.listAccounts.v1" + assert request_sent_return.base_uri == "https://api.shipengine.com/jsonrpc" + assert request_sent_return.headers["Api-Key"] == Constants.STUB_API_KEY.value + assert request_sent_return.headers["Content-Type"] == "application/json" + assert ( + request_sent_return.message == "Retrying the ShipEngine carrier.listAccounts.v1 API" + " at https://api.shipengine.com/jsonrpc" + ) + + def test_response_received_event_success(self, mocker: MockerFixture) -> None: + """DX-1522 Test response received event success.""" + test_start_time = datetime.now() + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 2, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + shipengine.validate_address(address=valid_residential_address()) + + response_recd_return = response_received_spy.spy_return + assert response_received_spy.call_count == 1 + assert type(response_recd_return) == ResponseReceivedEvent + assert ( + response_recd_return.message + == "Received an HTTP 200 response from the ShipEngine address.validate.v1 API" + ) + assert response_recd_return.status_code == 200 + assert response_recd_return.base_uri == "https://api.shipengine.com/jsonrpc" + assert response_recd_return.body["method"] == "address.validate.v1" + assert response_recd_return.retry == 0 + assert response_recd_return.elapsed < test_start_time.second + assert response_recd_return.headers["Content-Type"].split(";")[0] == "application/json" + + def test_response_received_on_error(self, mocker: MockerFixture) -> None: + """DX-1523 - Test response received event on error.""" + test_start_time = datetime.now() + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 1, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + response_recd_return = response_received_spy.spy_return + assert type(response_recd_return) == ResponseReceivedEvent + assert ( + response_recd_return.message + == "Retrying the ShipEngine carrier.listAccounts.v1 API at https://api.shipengine.com/jsonrpc" + ) + assert response_recd_return.status_code == 429 + assert response_recd_return.base_uri == "https://api.shipengine.com/jsonrpc" + assert response_recd_return.body["method"] == "carrier.listAccounts.v1" + assert response_recd_return.retry == 1 + assert response_recd_return.elapsed < test_start_time.second + assert (response_recd_return.timestamp - test_start_time).total_seconds() > 1 + + def test_config_with_retries_disabled(self, mocker: MockerFixture) -> None: + """DX-1527 - Tests that the SDK does not automatically retry if retries in config is set to 0.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + shipengine = configurable_stub_shipengine_instance( + { + "api_key": Constants.STUB_API_KEY.value, + "retries": 0, + "timeout": 10, + } + ) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 0 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 0 + + def test_config_with_custom_retries(self, mocker: MockerFixture) -> None: + """DX-1528 - Test config with custom retries.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + shipengine = configurable_stub_shipengine_instance( + { + "api_key": Constants.STUB_API_KEY.value, + "retries": 3, + "timeout": 21, + } + ) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 4 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 3 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 4 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 3 + + def test_timeout_err_when_retry_greater_than_timeout(self, mocker: MockerFixture) -> None: + """DX-1529 - Test timeout error when retry_after is greater than timeout.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 3, + "timeout": 1, + } + shipengine = configurable_stub_shipengine_instance(config=config) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert type(err) == ClientTimeoutError + assert err.request_id is not None + assert err.request_id.startswith("req_") + assert ( + err.message + == f"The request took longer than the {config['timeout']} seconds allowed." + ) + assert err.source is ErrorSource.SHIPENGINE.value + assert err.error_type is ErrorType.SYSTEM.value + assert err.error_code is ErrorCode.TIMEOUT.value + assert err.url == "https://www.shipengine.com/docs/rate-limits" + + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 0 + assert request_sent_return.timeout == 1 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 0 + + def test_retry_waits_correct_amount_of_time(self, mocker: MockerFixture) -> None: + """DX-1530 - retry waits the correct amount of time.""" + test_start_time = datetime.now() + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + shipengine = configurable_stub_shipengine_instance( + { + "api_key": Constants.STUB_API_KEY.value, + "retries": 2, + "timeout": 10, + } + ) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 3 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 2 + assert request_sent_return.timeout == 10 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 3 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 2 + assert ( + int(str(round((test_start_time - datetime.now()).total_seconds())).strip("-")) == 5 + ) diff --git a/tests/events/test_request_sent_event.py b/tests/events/test_request_sent_event.py deleted file mode 100644 index b0fa376..0000000 --- a/tests/events/test_request_sent_event.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Test that `RequestSentEvents` are emitted from the SDK properly.""" -from pytest_mock import MockerFixture - -# from shipengine_sdk.events import ShipEngineEventListener - -# from ..util import stub_shipengine_config - - -class TestRequestSentEvent: - def test_config_with_retries_disabled(self, mocker: MockerFixture) -> None: - """Tests that the SDK does not automatically retry if retries in config is set to 0.""" - # spy = mocker.spy(ShipEngineEventListener, "update") - # shipengine = stub_shipengine_config() diff --git a/tests/http_client/test_http_client.py b/tests/http_client/test_http_client.py index af6884a..b41776e 100644 --- a/tests/http_client/test_http_client.py +++ b/tests/http_client/test_http_client.py @@ -13,7 +13,6 @@ def validate_address(address): shipengine = ShipEngine( dict( api_key="baz", - base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=10, @@ -47,7 +46,7 @@ class TestShipEngineClient: def test_500_server_response(self): responses.add( responses.POST, - Endpoints.TEST_RPC_URL.value, + Endpoints.SHIPENGINE_RPC_URL.value, json={ "jsonrpc": "2.0", "id": "req_DezVNUvRkAP819f3JeqiuS", @@ -74,7 +73,7 @@ def test_500_server_response(self): def test_404_server_response(self): responses.add( responses.POST, - Endpoints.TEST_RPC_URL.value, + Endpoints.SHIPENGINE_RPC_URL.value, json={ "jsonrpc": "2.0", "id": "req_DezVNUvRkAP819f3JeqiuS", diff --git a/tests/models/address/test_address.py b/tests/models/address/test_address.py index 2c0856b..150c6f7 100644 --- a/tests/models/address/test_address.py +++ b/tests/models/address/test_address.py @@ -3,7 +3,7 @@ from shipengine_sdk.errors import ValidationError from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType -from tests.util.test_helpers import address_with_too_many_lines, empty_address_lines +from tests.util import address_with_too_many_lines, empty_address_lines def address_line_assertions(err: ValidationError, variant: str) -> None: diff --git a/tests/models/track_package/test_shipment.py b/tests/models/track_package/test_shipment.py index 995a700..ba30ac7 100644 --- a/tests/models/track_package/test_shipment.py +++ b/tests/models/track_package/test_shipment.py @@ -5,6 +5,7 @@ from shipengine_sdk.errors import ShipEngineError from shipengine_sdk.models import Shipment +from shipengine_sdk.models.enums import Constants from shipengine_sdk.util.iso_string import IsoString from ...util import stub_shipengine_config @@ -17,7 +18,7 @@ def stub_valid_shipment_data() -> Dict[str, Any]: """ return { "carrierCode": "fedex", - "carrierAccountId": "car_kfUjTZSEAQ8gHeT", + "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value, "shipmentId": "shp_yuh3GkfUjTZSEAQ", "estimatedDelivery": "2021-06-15T21:00:00.000Z", } diff --git a/tests/models/track_package/test_track_package_result.py b/tests/models/track_package/test_track_package_result.py index 2e27234..f7541a0 100644 --- a/tests/models/track_package/test_track_package_result.py +++ b/tests/models/track_package/test_track_package_result.py @@ -2,6 +2,7 @@ from typing import Any, Dict from shipengine_sdk.models import TrackingEvent, TrackPackageResult +from shipengine_sdk.models.enums import Constants from ...util import stub_shipengine_config @@ -17,7 +18,7 @@ def stub_track_package_data() -> Dict[str, Any]: "result": { "shipment": { "carrierCode": "fedex", - "carrierAccountId": "car_kfUjTZSEAQ8gHeT", + "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value, "shipmentId": "shp_tJUaQJz3Twz57iL", "estimatedDelivery": "2021-06-15T21:00:00.000Z", }, diff --git a/tests/services/test_track_package.py b/tests/services/test_track_package.py index ec16eda..49fd566 100644 --- a/tests/services/test_track_package.py +++ b/tests/services/test_track_package.py @@ -24,9 +24,9 @@ def assertions_on_delivered_after_exception_or_multiple_attempts( assert tracking_result.events[0].status == "accepted" assert tracking_result.events[1].status == "in_transit" assert tracking_result.events[2].status == "in_transit" - assert tracking_result.events[3].status == "exception" + assert tracking_result.events[3].status == "unknown" assert tracking_result.events[4].status == "exception" - assert tracking_result.events[5].status == "attempted_delivery" + assert tracking_result.events[5].status == "exception" assert tracking_result.events[6].status == "attempted_delivery" assert tracking_result.events[7].status == "delivered" assert tracking_result.events[-1].status == "delivered" @@ -128,13 +128,13 @@ def test_multiple_delivery_attempts(self) -> None: tracking_result = shipengine.track_package(tracking_data=package_id) track_package_assertions(tracking_result=tracking_result) - assert len(tracking_result.events) == 5 + assert len(tracking_result.events) == 9 assert_events_in_order(tracking_result.events) assert tracking_result.events[0].status == "accepted" assert tracking_result.events[1].status == "in_transit" - assert tracking_result.events[2].status == "attempted_delivery" - assert tracking_result.events[3].status == "attempted_delivery" - assert tracking_result.events[4].status == "delivered" + assert tracking_result.events[2].status == "unknown" + assert tracking_result.events[3].status == "in_transit" + assert tracking_result.events[-1].status == "delivered" def test_delivered_on_first_try(self) -> None: """DX-1091 - Test delivered on first try tracking event.""" @@ -211,7 +211,7 @@ def test_track_with_multiple_exceptions(self) -> None: assert len(tracking_result.events) == 8 assert tracking_result.events[0].status == "accepted" assert tracking_result.events[4].status == "exception" - assert tracking_result.events[5].status == "attempted_delivery" + assert tracking_result.events[5].status == "exception" assert tracking_result.events[7].status == "delivered" assert tracking_result.events[-1].status == "delivered" @@ -228,8 +228,6 @@ def test_multiple_locations_in_tracking_event(self) -> None: assert tracking_result.events[1].location.longitude is None assert type(tracking_result.events[2].location.latitude) is float assert type(tracking_result.events[2].location.longitude) is float - assert type(tracking_result.events[4].location.latitude) is float - assert type(tracking_result.events[4].location.longitude) is float def test_carrier_date_time_without_timezone(self) -> None: """DX-1098 - Test track package where carrierDateTime has no timezone.""" diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index 5357b74..81b392f 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -14,9 +14,7 @@ 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 - ) + return dict(api_key="baz_sim", page_size=50, retries=2, timeout=15) def valid_residential_address() -> Address: @@ -59,7 +57,7 @@ def set_config_timeout(timeout: int) -> ShipEngineConfig: :raises: :class:`InvalidFieldValueError`: If invalid value is passed into `ShipEngineConfig` object at instantiation. """ - return ShipEngineConfig(dict(api_key="baz", timeout=timeout)) + return ShipEngineConfig(dict(api_key="baz_sim", timeout=timeout)) def set_config_retries(retries: int) -> ShipEngineConfig: @@ -73,7 +71,7 @@ def set_config_retries(retries: int) -> ShipEngineConfig: :raises: :class:`InvalidFieldValueError`: If invalid value is passed into `ShipEngineConfig` object at instantiation. """ - return ShipEngineConfig(dict(api_key="baz", retries=retries)) + return ShipEngineConfig(dict(api_key="baz_sim", retries=retries)) def complete_valid_config() -> ShipEngineConfig: @@ -83,8 +81,7 @@ def complete_valid_config() -> ShipEngineConfig: """ return ShipEngineConfig( dict( - api_key="baz", - base_uri=Endpoints.TEST_RPC_URL.value, + api_key="baz_sim", page_size=50, retries=2, timeout=10, @@ -99,8 +96,8 @@ def test_valid_custom_config(self): 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.api_key == "baz_sim" + assert valid_config.base_uri is Endpoints.SHIPENGINE_RPC_URL.value assert valid_config.page_size == 50 assert valid_config.retries == 2 assert valid_config.timeout == 10 @@ -127,7 +124,7 @@ 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.api_key == "baz_sim" assert valid_retries.retries == retries def test_invalid_retries_provided(self): @@ -195,7 +192,7 @@ def test_invalid_api_key_in_method_call(self): def test_config_defaults(self) -> None: """Test default retries.""" - config = ShipEngineConfig(dict(api_key="baz")) + config = ShipEngineConfig(dict(api_key="baz_sim")) assert config.retries == 1 assert config.page_size == 50 diff --git a/tests/util/test_helpers.py b/tests/util/test_helpers.py index 9dbf8e2..706155a 100644 --- a/tests/util/test_helpers.py +++ b/tests/util/test_helpers.py @@ -2,12 +2,16 @@ from typing import Dict, Optional, Union from shipengine_sdk import ShipEngine, ShipEngineConfig +from shipengine_sdk.errors import ShipEngineError from shipengine_sdk.models import ( Address, AddressValidateResult, - Endpoints, + ErrorCode, + ErrorSource, + ErrorType, TrackingQuery, ) +from shipengine_sdk.models.enums import Constants def stub_config( @@ -18,8 +22,7 @@ def stub_config( when instantiating the ShipEngine object. """ return dict( - api_key="baz_sim", - base_uri=Endpoints.TEST_RPC_URL.value, + api_key=Constants.STUB_API_KEY.value, page_size=50, retries=retries, timeout=15, @@ -240,6 +243,17 @@ def address_with_invalid_postal_code() -> Address: ) +def get_429_address() -> Address: + """Return an address that fetches a 429 fixture from the server.""" + return Address( + street=["429 Rate Limit Error"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + def get_server_side_error() -> Address: """Return an address that will cause the server to return a 500 server error.""" return Address( @@ -392,3 +406,16 @@ def canada_valid_normalize_assertions( assert normalized_address.postal_code == "M6K 3C3" assert normalized_address.country_code == original_address.country_code.upper() assert normalized_address.is_residential is expected_residential_indicator + + +def assert_on_429_exception(err: ShipEngineError, error_class: object) -> None: + error = err.to_dict() + assert type(err) == error_class + assert error["request_id"] is not None + assert error["request_id"].startswith("req_") + assert error["source"] is ErrorSource.SHIPENGINE.value + assert error["error_type"] is ErrorType.SYSTEM.value + assert error["error_code"] is ErrorCode.RATE_LIMIT_EXCEEDED.value + assert error["message"] == "You have exceeded the rate limit." + assert error["url"] is not None + assert error["url"] == "https://www.shipengine.com/docs/rate-limits" diff --git a/tests/util/test_iso_string.py b/tests/util/test_iso_string.py index 1ec43e7..b204828 100644 --- a/tests/util/test_iso_string.py +++ b/tests/util/test_iso_string.py @@ -9,16 +9,14 @@ class TestIsoString: def test_to_string(self) -> None: iso_str = IsoString(self._test_iso_string_no_tz).to_string() - assert type(iso_str) is str def test_to_datetime_object(self) -> None: iso_str = IsoString(self._test_iso_string_no_tz).to_datetime_object() - assert type(iso_str) is datetime.datetime def test_static_valid_iso_check(self) -> None: - assert IsoString.is_valid_iso_string(self._test_iso_string_no_tz) is True + assert IsoString.is_valid_iso_string_with_tz(self._test_iso_string_no_tz) is True def test_static_valid_iso_check_failure(self) -> None: - assert IsoString.is_valid_iso_string("2021-06-10T21:00:00.000K") is False + assert IsoString.is_valid_iso_string_with_tz("2021-06-10T21:00:00.000K") is False diff --git a/tox.ini b/tox.ini index 84fe137..f69bfac 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,7 @@ changedir = tests deps = pytest pytest-cov + pytest-mock flake8 coverage coveralls