diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml
new file mode 100644
index 0000000..50c07d8
--- /dev/null
+++ b/.github/workflows/CD.yml
@@ -0,0 +1,45 @@
+on:
+ push:
+ branches:
+ - main
+
+name: ShipEngine Python CD
+jobs:
+ release-please:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: GoogleCloudPlatform/release-please-action@v2
+ id: release
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ release-type: python
+ package-name: shipengine
+
+ # Checkout code if release was created
+ - uses: actions/checkout@v2
+ if: ${{ setps.release.outputs.release_created }}
+
+ # Setup Python if release was created
+ - name: Install Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+ if: ${{ steps.release.outputs.release_created }}
+
+ - name: Install dependancies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install poetry
+ if: ${{ steps.release.outputs.release_created }}
+
+ - name: Build the code
+ run: |
+ poetry build
+ if: ${{ steps.release.outputs.release_created }}
+
+ - name: Publish package
+ run: |
+ poetry publish
+ env:
+ POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
+ if: ${{ steps.release.outputs.release_created }}
diff --git a/.github/workflows/main.yml b/.github/workflows/CI.yml
similarity index 99%
rename from .github/workflows/main.yml
rename to .github/workflows/CI.yml
index 504eafe..c7f39be 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/CI.yml
@@ -1,8 +1,6 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-name: ShipEngine SDK
-
on:
push:
branches:
@@ -11,6 +9,7 @@ on:
branches:
- main
+name: ShipEngine SDK CI
jobs:
lint_and_pytest:
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 9400a61..d10e795 100644
--- a/README.md
+++ b/README.md
@@ -26,20 +26,22 @@ pip install shipengine
Instantiate ShipEngine Class
----------------------------
+
```python
import os
-from shipengine_sdk import ShipEngine
+from shipengine import ShipEngine
api_key = os.getenv("SHIPENGINE_API_KEY")
shipengine = ShipEngine(api_key)
```
- You can also pass in a `dictionary` containing configuration options instead of just passing in a string that is your `API Key`.
+
```python
import os
-from shipengine_sdk import ShipEngine
+from shipengine import ShipEngine
api_key = os.getenv("SHIPENGINE_API_KEY")
diff --git a/docs/address_validation_example.md b/docs/address_validation_example.md
index 8a62bf9..3434e96 100644
--- a/docs/address_validation_example.md
+++ b/docs/address_validation_example.md
@@ -61,7 +61,7 @@ Input Parameters
----------------
The `validate_address` method accepts an address object containing the properties listed below.
-You can import the [`Address`](../shipengine_sdk/models/address/__init__.py)
+You can import the [`Address`](../shipengine/models/address/__init__.py)
type into your project to take advantage of your
IDE's code completion functionality.
@@ -108,7 +108,7 @@ A *string* between `0` and `1000` characters indicating the company name, if thi
Output
------
The `validate_address` method returns an address validation result object containing the properties listed below.
-You can import the [`AddressValidationResult`](../shipengine_sdk/models/address/__init__.py)
+You can import the [`AddressValidationResult`](../shipengine/models/address/__init__.py)
type into your project to take advantage of your IDE's code completion functionality.
* `is_valid`
@@ -179,8 +179,8 @@ Examples:
```python
import os
-from shipengine_sdk import ShipEngine
-from shipengine_sdk.models import Address
+from shipengine import ShipEngine
+from shipengine.models import Address
api_key = os.getenv("SHIPENGINE_API_KEY")
@@ -309,5 +309,5 @@ Exceptions
==========
- This method will only throw an exception that is an instance/extension of
- ([ShipEngineError](../shipengine_sdk/errors/__init__.py)) if there is a problem if a problem occurs, such as a
+ ([ShipEngineError](../shipengine/errors/__init__.py)) if there is a problem if a problem occurs, such as a
network error or an error response from the API.
diff --git a/docs/normalize_address_example.md b/docs/normalize_address_example.md
index 9b426f4..29c322f 100644
--- a/docs/normalize_address_example.md
+++ b/docs/normalize_address_example.md
@@ -32,7 +32,7 @@ containing method-level configuration options.
- **Behavior**: The `normalize_address` method will either return a normalized version of the address you pass in. This
will throw an exception if address validation fails, or an invalid address is provided. The normalized address will
- be returned as an instance of the [Address](../shipengine_sdk/models/address/__init__.py) class.
+ be returned as an instance of the [Address](../shipengine/models/address/__init__.py) class.
- **Method level configuration** - You can optionally pass in an list that contains `configuration` values to be used
for the current method call. The options are `api_key`, `base_uri`, `page_size`,
@@ -150,8 +150,8 @@ Examples:
```python
import os
-from shipengine_sdk import ShipEngine
-from shipengine_sdk.models import Address
+from shipengine import ShipEngine
+from shipengine.models import Address
api_key = os.getenv("SHIPENGINE_API_KEY")
diff --git a/docs/track_package_example.md b/docs/track_package_example.md
index e6c7f74..baa851d 100644
--- a/docs/track_package_example.md
+++ b/docs/track_package_example.md
@@ -246,11 +246,12 @@ An *array of objects* representing the individual tracking events that have occu
Example
=======
+
```python
import os
-from shipengine_sdk import ShipEngine
-from shipengine_sdk.models import TrackingQuery
+from shipengine import ShipEngine
+from shipengine.models import TrackingQuery
api_key = os.getenv("SHIPENGINE_API_KEY")
diff --git a/poetry.lock b/poetry.lock
index 4e38846..c98f783 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -74,6 +74,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pytz = ">=2015.7"
+[[package]]
+name = "backports.entry-points-selectable"
+version = "1.1.0"
+description = "Compatibility shim providing selectable entry points for older implementations"
+category = "dev"
+optional = false
+python-versions = ">=2.7"
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+
[[package]]
name = "base58"
version = "2.1.0"
@@ -131,6 +146,17 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+[[package]]
+name = "charset-normalizer"
+version = "2.0.4"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
[[package]]
name = "click"
version = "8.0.1"
@@ -164,7 +190,7 @@ toml = ["toml"]
[[package]]
name = "coveralls"
-version = "3.1.0"
+version = "3.2.0"
description = "Show coverage stats online via coveralls.io"
category = "dev"
optional = false
@@ -220,15 +246,12 @@ 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"
+name = "dunamai"
+version = "1.5.5"
+description = "Dynamic version generation"
+category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[package.extras]
-testing = ["pre-commit"]
+python-versions = ">=3.5,<4.0"
[[package]]
name = "filelock"
@@ -265,7 +288,7 @@ base58 = ">=2.1.0,<3.0.0"
[[package]]
name = "identify"
-version = "2.2.10"
+version = "2.2.12"
description = "File identification library for Python"
category = "dev"
optional = false
@@ -276,11 +299,11 @@ license = ["editdistance-s"]
[[package]]
name = "idna"
-version = "2.10"
+version = "3.2"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.5"
[[package]]
name = "imagesize"
@@ -292,7 +315,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
-version = "4.6.1"
+version = "4.6.3"
description = "Read metadata from Python packages"
category = "dev"
optional = false
@@ -317,7 +340,7 @@ python-versions = "*"
[[package]]
name = "isort"
-version = "5.9.1"
+version = "5.9.3"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
@@ -333,7 +356,7 @@ plugins = ["setuptools"]
name = "jinja2"
version = "3.0.1"
description = "A very fast and expressive template engine."
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
@@ -347,22 +370,22 @@ i18n = ["Babel (>=2.7)"]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "marshmallow"
-version = "3.12.1"
+version = "3.13.0"
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)"]
+dev = ["pytest", "pytz", "simplejson", "mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"]
+docs = ["sphinx (==4.1.1)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.6)"]
+lint = ["mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]]
@@ -421,11 +444,23 @@ pyparsing = ">=2.0.2"
[[package]]
name = "pathspec"
-version = "0.8.1"
+version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[[package]]
+name = "platformdirs"
+version = "2.2.0"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "pluggy"
@@ -441,6 +476,19 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
+[[package]]
+name = "poetry-dynamic-versioning"
+version = "0.13.0"
+description = "Plugin for Poetry to enable dynamic versioning based on VCS tags"
+category = "main"
+optional = false
+python-versions = ">=3.5,<4.0"
+
+[package.dependencies]
+dunamai = ">=1.5,<2.0"
+jinja2 = {version = ">=2.11.1,<4", markers = "python_version >= \"3.6\" and python_version < \"4.0\""}
+tomlkit = ">=0.4"
+
[[package]]
name = "pre-commit"
version = "2.13.0"
@@ -520,18 +568,6 @@ 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"
@@ -605,7 +641,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "regex"
-version = "2021.7.6"
+version = "2021.8.3"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
@@ -613,21 +649,21 @@ python-versions = "*"
[[package]]
name = "requests"
-version = "2.25.1"
+version = "2.26.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
-chardet = ">=3.0.2,<5"
-idna = ">=2.5,<3"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"
[package.extras]
-security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "responses"
@@ -779,9 +815,17 @@ category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+[[package]]
+name = "tomlkit"
+version = "0.7.2"
+description = "Style preserving TOML library"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
[[package]]
name = "tox"
-version = "3.23.1"
+version = "3.24.1"
description = "tox is a generic virtualenv management and test command line tool"
category = "dev"
optional = false
@@ -845,22 +889,23 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "virtualenv"
-version = "20.4.7"
+version = "20.7.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
-appdirs = ">=1.4.3,<2"
+"backports.entry-points-selectable" = ">=1.0.4"
distlib = ">=0.3.1,<1"
filelock = ">=3.0.0,<4"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+platformdirs = ">=2,<3"
six = ">=1.9.0,<2"
[package.extras]
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)"]
+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)"]
[[package]]
name = "watchdog"
@@ -901,7 +946,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
-content-hash = "44a12436d6071b62002beab90bd98605720f47f4c08c492d52f1b482efaeadbf"
+content-hash = "c6f0138bb35e8ebd809a9d3a66b240e1250eabf6767686df4bc0b85ec44904e7"
[metadata.files]
aiohttp = [
@@ -967,6 +1012,10 @@ babel = [
{file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
{file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
]
+"backports.entry-points-selectable" = [
+ {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
+ {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
+]
base58 = [
{file = "base58-2.1.0-py3-none-any.whl", hash = "sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48"},
{file = "base58-2.1.0.tar.gz", hash = "sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2"},
@@ -986,6 +1035,10 @@ chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
+charset-normalizer = [
+ {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"},
+ {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"},
+]
click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
@@ -1049,8 +1102,8 @@ coverage = [
{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"},
+ {file = "coveralls-3.2.0-py2.py3-none-any.whl", hash = "sha256:aedfcc5296b788ebaf8ace8029376e5f102f67c53d1373f2e821515c15b36527"},
+ {file = "coveralls-3.2.0.tar.gz", hash = "sha256:15a987d9df877fff44cd81948c5806ffb6eafb757b3443f737888358e96156ee"},
]
dataclasses-json = [
{file = "dataclasses-json-0.5.4.tar.gz", hash = "sha256:6c3976816fd3cdd8db3be2b516b64fc083acd46ac22c680d3dc24cb1d6ae3367"},
@@ -1067,9 +1120,9 @@ 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"},
+dunamai = [
+ {file = "dunamai-1.5.5-py3-none-any.whl", hash = "sha256:525ac30db6ca4f8e48b9f198c2e8fbc2a9ce3ea189768361c621ea635212ee49"},
+ {file = "dunamai-1.5.5.tar.gz", hash = "sha256:32f30db71e8fd1adeb42fac45c04433680e47a28298447cd30304e0bba95a7dd"},
]
filelock = [
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
@@ -1083,28 +1136,28 @@ fuuid = [
{file = "fuuid-0.1.0.tar.gz", hash = "sha256:ce8aec9ae81078941fa730dca0bf5ff7ca56675bea9327e938f1a04c8fad18b2"},
]
identify = [
- {file = "identify-2.2.10-py2.py3-none-any.whl", hash = "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421"},
- {file = "identify-2.2.10.tar.gz", hash = "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"},
+ {file = "identify-2.2.12-py2.py3-none-any.whl", hash = "sha256:a510cbe155f39665625c8a4c4b4f9360cbce539f51f23f47836ab7dd852db541"},
+ {file = "identify-2.2.12.tar.gz", hash = "sha256:242332b3bdd45a8af1752d5d5a3afb12bee26f8e67c4be06e394f82d05ef1a4d"},
]
idna = [
- {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
- {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
+ {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
]
imagesize = [
{file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
]
importlib-metadata = [
- {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"},
- {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"},
+ {file = "importlib_metadata-4.6.3-py3-none-any.whl", hash = "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b"},
+ {file = "importlib_metadata-4.6.3.tar.gz", hash = "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
- {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"},
- {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"},
+ {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
+ {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"},
]
jinja2 = [
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
@@ -1147,8 +1200,8 @@ markupsafe = [
{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"},
+ {file = "marshmallow-3.13.0-py2.py3-none-any.whl", hash = "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842"},
+ {file = "marshmallow-3.13.0.tar.gz", hash = "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e"},
]
marshmallow-enum = [
{file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"},
@@ -1210,13 +1263,21 @@ packaging = [
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
]
pathspec = [
- {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
- {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
+ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
+ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
+]
+platformdirs = [
+ {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"},
+ {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
+poetry-dynamic-versioning = [
+ {file = "poetry-dynamic-versioning-0.13.0.tar.gz", hash = "sha256:52e64165c811573e719b43310a09416c894afa6662a4035de5d888199ee49760"},
+ {file = "poetry_dynamic_versioning-0.13.0-py3-none-any.whl", hash = "sha256:46754061380ac772f49f60036471223804edc5a07b6aaee96e0a0793b1efae6a"},
+]
pre-commit = [
{file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"},
{file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"},
@@ -1245,9 +1306,6 @@ 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"},
@@ -1299,51 +1357,43 @@ pyyaml = [
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
regex = [
- {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"},
- {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"},
- {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"},
- {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"},
- {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"},
- {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"},
- {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"},
- {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"},
- {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"},
- {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"},
- {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"},
- {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"},
- {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"},
- {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"},
- {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"},
- {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"},
- {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"},
- {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"},
- {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"},
- {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"},
- {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"},
- {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"},
- {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"},
- {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"},
- {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"},
- {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"},
- {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"},
- {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"},
- {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"},
- {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"},
- {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"},
- {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"},
- {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"},
- {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"},
- {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"},
- {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"},
- {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"},
- {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"},
- {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"},
- {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"},
- {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"},
+ {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"},
+ {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"},
+ {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"},
+ {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"},
+ {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"},
+ {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"},
+ {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"},
+ {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"},
+ {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"},
+ {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"},
+ {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"},
+ {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"},
+ {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"},
+ {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"},
+ {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"},
+ {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"},
+ {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"},
+ {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"},
+ {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"},
+ {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"},
+ {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"},
+ {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"},
+ {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"},
+ {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"},
+ {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"},
+ {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"},
+ {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"},
+ {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"},
+ {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"},
+ {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"},
+ {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"},
+ {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"},
+ {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"},
]
requests = [
- {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
- {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
+ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
+ {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
responses = [
{file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"},
@@ -1392,9 +1442,13 @@ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
+tomlkit = [
+ {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"},
+ {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"},
+]
tox = [
- {file = "tox-3.23.1-py2.py3-none-any.whl", hash = "sha256:b0b5818049a1c1997599d42012a637a33f24c62ab8187223fdd318fa8522637b"},
- {file = "tox-3.23.1.tar.gz", hash = "sha256:307a81ddb82bd463971a273f33e9533a24ed22185f27db8ce3386bff27d324e3"},
+ {file = "tox-3.24.1-py2.py3-none-any.whl", hash = "sha256:60eda26fa47b7130e6fc1145620b1fd897963af521093c3685c3f63d1c394029"},
+ {file = "tox-3.24.1.tar.gz", hash = "sha256:9850daeb96d21b4abf049bc5f197426123039e383ebfed201764e9355fc5a880"},
]
typed-ast = [
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
@@ -1443,8 +1497,8 @@ urllib3 = [
{file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"},
]
virtualenv = [
- {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"},
- {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"},
+ {file = "virtualenv-20.7.0-py2.py3-none-any.whl", hash = "sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5"},
+ {file = "virtualenv-20.7.0.tar.gz", hash = "sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094"},
]
watchdog = [
{file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"},
diff --git a/pyproject.toml b/pyproject.toml
index 1270093..a7335cf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,21 @@
[tool.poetry]
-name = "shipengine_sdk"
-version = "0.0.1"
+name = "shipengine"
+version = "0.0.0"
description = "A Python library for ShipEngine."
authors = ["KaseyCantu "]
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/ShipEngine/shipengine-python"
+[tool.poetry-dynamic-versioning]
+enable = true
+vcs = "git"
+style = "semver"
+format = "{base}"
+
+[tool.poetry-dynamic-versioning.substitution]
+files = ["*.py", "*/__init__.py", "*/__version__.py", "*/_version.py"]
+
[tool.poetry.dependencies]
python = "^3.7"
aiohttp = "^3.7.4"
@@ -14,6 +23,7 @@ requests = "^2.25.1"
python-dotenv = "^0.15.0"
dataclasses-json = "^0.5.3"
fuuid = "^0.1.0"
+poetry-dynamic-versioning = "^0.13.0"
[tool.poetry.dev-dependencies]
pytest = ">=5.0"
@@ -25,11 +35,10 @@ Sphinx = "^3.5.2"
tox = "^3.23.0"
coverage = "^5.5"
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"
+responses = "^0.13.3"
[tool.black]
line-length = 100
@@ -46,5 +55,5 @@ relative_files = true
[tool.poetry.scripts]
[build-system]
-requires = ["poetry-core>=1.0.0"]
+requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry.core.masonry.api"
diff --git a/pytest.ini b/pytest.ini
index eff6106..5ea4619 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,5 +1,5 @@
[pytest]
-addopts = -ra -v --cov=shipengine_sdk
+addopts = -ra -v --cov=shipengine
testpaths = tests
filterwarnings =
ignore:"@coroutine" decorator is deprecated since Python 3.8, use "async def" instead:DeprecationWarning
diff --git a/requirements.txt b/requirements.txt
index ec44660..433e97f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,44 +1,48 @@
aiohttp==3.7.4.post0; python_version >= "3.6"
alabaster==0.7.12; python_version >= "3.5"
-appdirs==1.4.4; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+appdirs==1.4.4; python_version >= "3.6"
async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6"
atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" 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"
babel==2.9.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5"
+backports.entry-points-selectable==1.1.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "2.7"
base58==2.1.0; python_version >= "3.7" and python_version < "4.0"
black==20.8b1; python_version >= "3.6"
-certifi==2021.5.30; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5"
+certifi==2021.5.30; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5"
cfgv==3.3.0; python_full_version >= "3.6.1"
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"
+charset-normalizer==2.0.4; python_full_version >= "3.6.0" and python_version >= "3.5"
click==8.0.1; python_version >= "3.6"
colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4")
-coveralls==3.1.0; python_version >= "3.5"
+coveralls==3.2.0; python_version >= "3.5"
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"
+dunamai==1.5.5; python_version >= "3.5" and python_version < "4.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"
-identify==2.2.10; python_full_version >= "3.6.1"
-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"
+identify==2.2.12; python_full_version >= "3.6.1"
+idna==3.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
imagesize==1.2.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5"
-importlib-metadata==4.6.1; python_version < "3.8" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.6") and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.4.0" and python_version >= "3.6" and python_version < "3.8") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
+importlib-metadata==4.6.3; python_version < "3.8" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.6") and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.4.0" and python_version >= "3.6" and python_version < "3.8") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
iniconfig==1.1.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-isort==5.9.1; python_full_version >= "3.6.1" and python_version < "4.0"
-jinja2==3.0.1; python_version >= "3.6"
-markupsafe==2.0.1; python_version >= "3.6"
-marshmallow==3.12.1; python_version >= "3.6"
+isort==5.9.3; python_full_version >= "3.6.1" and python_version < "4.0"
+jinja2==3.0.1; python_version >= "3.6" and python_version < "4.0"
+markupsafe==2.0.1; python_version >= "3.6" and python_version < "4.0"
+marshmallow==3.13.0; python_version >= "3.6"
marshmallow-enum==1.5.1; python_version >= "3.6"
mccabe==0.6.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
multidict==5.1.0; python_version >= "3.6"
mypy-extensions==0.4.3; python_version >= "3.6"
nodeenv==1.6.0; python_full_version >= "3.6.1"
packaging==21.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-pathspec==0.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+pathspec==0.9.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+platformdirs==2.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"
pluggy==0.13.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+poetry-dynamic-versioning==0.13.0; python_version >= "3.5" and python_version < "4.0"
pre-commit==2.13.0; python_full_version >= "3.6.1"
py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
pycodestyle==2.7.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
@@ -46,15 +50,14 @@ 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"
-regex==2021.7.6; python_version >= "3.6"
-requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
+regex==2021.8.3; python_version >= "3.6"
+requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
responses==0.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
snowballstemmer==2.1.0; python_version >= "3.5"
@@ -67,12 +70,13 @@ sphinxcontrib-qthelp==1.0.3; python_version >= "3.5"
sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.5"
stringcase==1.2.0; python_version >= "3.6"
toml==0.10.2; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
-tox==3.23.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
+tomlkit==0.7.2; python_version >= "3.5" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.5" and python_version < "4.0" and python_full_version >= "3.5.0"
+tox==3.24.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
typed-ast==1.4.3; python_version >= "3.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"
+urllib3==1.26.6; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.5"
+virtualenv==20.7.0; 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/__init__.py b/shipengine/__init__.py
similarity index 91%
rename from shipengine_sdk/__init__.py
rename to shipengine/__init__.py
index 0bbbcc5..ebd4ff4 100644
--- a/shipengine_sdk/__init__.py
+++ b/shipengine/__init__.py
@@ -1,5 +1,5 @@
"""ShipEngine SDK."""
-__version__ = "0.0.1"
+__version__ = "0.0.0"
import logging
from logging import NullHandler
diff --git a/shipengine_sdk/models/enums/__init__.py b/shipengine/enums/__init__.py
similarity index 55%
rename from shipengine_sdk/models/enums/__init__.py
rename to shipengine/enums/__init__.py
index be38bf1..6627690 100644
--- a/shipengine_sdk/models/enums/__init__.py
+++ b/shipengine/enums/__init__.py
@@ -1,7 +1,6 @@
"""ShipEngine SDK Enumerations"""
from enum import Enum
-from .carriers import CarrierNames, Carriers
from .country import Country
from .error_code import ErrorCode
from .error_source import ErrorSource
@@ -9,33 +8,33 @@
from .regex_patterns import RegexPatterns
-class Constants(Enum):
- """Test API Key for use with Simengine."""
+class BaseURL(Enum):
+ """API Endpoint URI's used throughout the ShipEngine SDK."""
- STUB_API_KEY = "TEST_vMiVbICUjBz4BZjq0TRBLC/9MrxY4+yjvb1G1RMxlJs"
- CARRIER_ACCOUNT_ID_STUB = "car_41GrQHn5uouiPZc2TNE6PU29tZU9ud"
+ SHIPENGINE_RPC_URL = "https://api.shipengine.com/"
-class Endpoints(Enum):
- """API Endpoint URI's used throughout the ShipEngine SDK."""
+class Constants(Enum):
+ """Test API Key for use with Simengine."""
- SHIPENGINE_RPC_URL = "https://api.shipengine.com/jsonrpc"
+ STUB_API_KEY = "TEST_vMiVbICUjBz4BZjq0TRBLC/9MrxY4+yjvb1G1RMxlJs"
-class Events(Enum):
- """ShipEngine Events emitted by the SDK when a request is sent or when a response is received."""
+class HTTPVerbs(Enum):
+ """A collection of HTTP verbs used in requests to ShipEngine API."""
- ON_REQUEST_SENT = "on_request_sent"
- ON_RESPONSE_RECEIVED = "on_response_received"
+ GET = "GET"
+ DELETE = "DELETE"
+ POST = "POST"
+ PUT = "PUT"
-class RPCMethods(Enum):
+class Endpoints(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"
+ ADDRESSES_VALIDATE = "v1/addresses/validate"
+ GET_RATE_FROM_SHIPMENT = "v1/rates"
+ LIST_CARRIERS = "v1/carriers"
def does_member_value_exist(m: str, enum_to_search) -> bool:
@@ -46,9 +45,3 @@ def does_member_value_exist(m: str, enum_to_search) -> bool:
:param enum_to_search: The enumeration to check the member value against.
"""
return False if m not in (member.value for member in enum_to_search) else True
-
-
-def get_carrier_name_value(upper_carrier_code: str):
- for k in CarrierNames:
- if upper_carrier_code == k.name:
- return k.value
diff --git a/shipengine_sdk/models/enums/country.py b/shipengine/enums/country.py
similarity index 100%
rename from shipengine_sdk/models/enums/country.py
rename to shipengine/enums/country.py
diff --git a/shipengine_sdk/models/enums/error_code.py b/shipengine/enums/error_code.py
similarity index 100%
rename from shipengine_sdk/models/enums/error_code.py
rename to shipengine/enums/error_code.py
diff --git a/shipengine_sdk/models/enums/error_source.py b/shipengine/enums/error_source.py
similarity index 100%
rename from shipengine_sdk/models/enums/error_source.py
rename to shipengine/enums/error_source.py
diff --git a/shipengine_sdk/models/enums/error_type.py b/shipengine/enums/error_type.py
similarity index 100%
rename from shipengine_sdk/models/enums/error_type.py
rename to shipengine/enums/error_type.py
diff --git a/shipengine_sdk/models/enums/regex_patterns.py b/shipengine/enums/regex_patterns.py
similarity index 100%
rename from shipengine_sdk/models/enums/regex_patterns.py
rename to shipengine/enums/regex_patterns.py
diff --git a/shipengine_sdk/errors/__init__.py b/shipengine/errors/__init__.py
similarity index 84%
rename from shipengine_sdk/errors/__init__.py
rename to shipengine/errors/__init__.py
index d771528..a1e7968 100644
--- a/shipengine_sdk/errors/__init__.py
+++ b/shipengine/errors/__init__.py
@@ -2,7 +2,7 @@
import json
from typing import Optional
-from ..models.enums import ErrorCode, ErrorSource, ErrorType, does_member_value_exist
+from shipengine.enums import ErrorCode, ErrorSource, ErrorType, does_member_value_exist
class ShipEngineError(Exception):
@@ -10,7 +10,7 @@ def __init__(
self,
message: str,
request_id: Optional[str] = None,
- source: Optional[str] = None,
+ error_source: Optional[str] = None,
error_type: Optional[str] = None,
error_code: Optional[str] = None,
url: Optional[str] = None,
@@ -18,18 +18,18 @@ def __init__(
"""Base exception class that all other client errors will inherit from."""
self.message = message
self.request_id = request_id
- self.source = source
+ self.error_source = error_source
self.error_code = error_code
self.error_type = error_type
self.url = url
self._are_enums_valid()
def _are_enums_valid(self):
- if self.source is None:
+ if self.error_source is None:
pass # noqa
- elif not does_member_value_exist(self.source, ErrorSource):
+ elif not does_member_value_exist(self.error_source, ErrorSource):
raise ValueError(
- f"Error source must be a member of ErrorSource enum - [{self.source}] provided."
+ f"Error source must be a member of ErrorSource enum - [{self.error_source}] provided."
)
if self.error_type is None:
@@ -77,17 +77,17 @@ class ClientTimeoutError(ShipEngineError):
def __init__(
self,
retry_after: int,
- source: Optional[str] = None,
+ error_source: Optional[str] = None,
request_id: Optional[str] = None,
) -> None:
"""An exception that indicates the configured timeout has been reached for a given request."""
self.retry_after = retry_after
- self.source = source
+ self.error_source = error_source
self.request_id = request_id
super(ClientTimeoutError, self).__init__(
message=f"The request took longer than the {retry_after} seconds allowed.",
request_id=self.request_id,
- source=self.source,
+ error_source=self.error_source,
error_type=ErrorType.SYSTEM.value,
error_code=ErrorCode.TIMEOUT.value,
url="https://www.shipengine.com/docs/rate-limits",
@@ -95,15 +95,15 @@ def __init__(
class InvalidFieldValueError(ShipEngineError):
- def __init__(self, field_name: str, reason: str, field_value, source: str = None) -> None:
+ def __init__(self, field_name: str, reason: str, field_value, error_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
+ self.error_source = error_source
super(InvalidFieldValueError, self).__init__(
request_id=None,
message=f"{self.field_name} - {reason} {self.field_value} was provided.",
- source=self.source,
+ error_source=self.error_source,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.INVALID_FIELD_VALUE.value,
)
@@ -112,18 +112,18 @@ def __init__(self, field_name: str, reason: str, field_value, source: str = None
class RateLimitExceededError(ShipEngineError):
def __init__(
self,
- retry_after: int,
- source: Optional[str] = None,
+ retry_after: Optional[int] = None,
+ error_source: Optional[str] = None,
request_id: Optional[str] = None,
) -> None:
"""The amount of time (in SECONDS) to wait before retrying the request."""
self.retry_after = retry_after
- self.source = source
+ self.error_source = error_source
self.request_id = request_id
super(RateLimitExceededError, self).__init__(
message="You have exceeded the rate limit.",
request_id=self.request_id,
- source=self.source,
+ error_source=self.error_source,
error_type=ErrorType.SYSTEM.value,
error_code=ErrorCode.RATE_LIMIT_EXCEEDED.value,
url="https://www.shipengine.com/docs/rate-limits",
diff --git a/shipengine_sdk/http_client/__init__.py b/shipengine/http_client/__init__.py
similarity index 100%
rename from shipengine_sdk/http_client/__init__.py
rename to shipengine/http_client/__init__.py
diff --git a/shipengine/http_client/client.py b/shipengine/http_client/client.py
new file mode 100644
index 0000000..d938635
--- /dev/null
+++ b/shipengine/http_client/client.py
@@ -0,0 +1,184 @@
+"""A synchronous HTTP Client for the ShipEngine SDK."""
+import json
+import os
+import platform
+import time
+from typing import Any, Dict, Optional
+from urllib.parse import urljoin
+
+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 import __version__
+
+from ..enums import ErrorCode, ErrorSource, ErrorType, HTTPVerbs
+from ..errors import RateLimitExceededError, ShipEngineError
+from ..shipengine_config import ShipEngineConfig
+from ..util import check_response_for_errors
+
+
+def base_url(config) -> str:
+ return config.base_uri if os.getenv("CLIENT_BASE_URI") is None else os.getenv("CLIENT_BASE_URI")
+
+
+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,
+ }
+
+
+class ShipEngineAuth(AuthBase):
+ def __init__(self, api_key: str) -> None:
+ """Auth Base appends `Api-Key` header to all requests."""
+ self.api_key: str = api_key
+
+ def __call__(self, request: Request, *args, **kwargs) -> Request:
+ request.headers["Api-Key"] = self.api_key
+ return request
+
+
+class ShipEngineClient:
+ def __init__(self) -> None:
+ """A `JSON-RPC 2.0` HTTP client used to send all HTTP requests from the SDK."""
+ self.session = requests.session()
+
+ def get(self, endpoint: str, config: ShipEngineConfig) -> Dict[str, Any]:
+ """Send an HTTP GET request."""
+ return self._request_loop(
+ http_method=HTTPVerbs.GET.value, endpoint=endpoint, params=None, config=config
+ )
+
+ def post(
+ self, endpoint: str, config: ShipEngineConfig, params: Optional[Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
+ """Send an HTTP POST request."""
+ return self._request_loop(
+ http_method=HTTPVerbs.POST.value, endpoint=endpoint, params=params, config=config
+ )
+
+ def delete(self, endpoint: str, config: ShipEngineConfig):
+ """Send an HTTP DELETE request."""
+ return self._request_loop(
+ http_method=HTTPVerbs.DELETE.value, endpoint=endpoint, params=None, config=config
+ )
+
+ def put(self, endpoint: str, config: ShipEngineConfig, params: Optional[Dict[str, Any]] = None):
+ """Send an HTTP PUT request."""
+ return self._request_loop(
+ http_method=HTTPVerbs.PUT.value, endpoint=endpoint, params=params, config=config
+ )
+
+ def _request_loop(
+ self,
+ http_method: str,
+ endpoint: str,
+ params: Optional[Dict[str, Any]],
+ config: ShipEngineConfig,
+ ) -> Dict[str, Any]:
+ retry: int = 0
+ while retry <= config.retries:
+ try:
+ api_response = self._send_request(
+ http_method=http_method,
+ endpoint=endpoint,
+ body=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)
+ retry += 1
+ continue
+ else:
+ raise err
+ return api_response
+
+ def _send_request(
+ self,
+ http_method: str,
+ endpoint: str,
+ body: Optional[Dict[str, Any]],
+ retry: int,
+ config: ShipEngineConfig,
+ ) -> Dict[str, Any]:
+ """
+ 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.
+ """
+ base_uri = base_url(config=config)
+ client: Session = self._request_retry_session(retries=config.retries, url_base=base_uri)
+
+ req_headers = request_headers(user_agent=self._derive_user_agent(), api_key=config.api_key)
+ req: Request = Request(
+ method=http_method,
+ url=urljoin(base_uri, endpoint),
+ data=json.dumps(body),
+ headers=req_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 {http_method} API:\n {err.response}",
+ error_source=ErrorSource.SHIPENGINE.value,
+ error_type=ErrorType.SYSTEM.value,
+ error_code=ErrorCode.UNSPECIFIED.value,
+ )
+
+ resp_body: Dict[str, Any] = resp.json()
+ status_code: int = resp.status_code
+
+ check_response_for_errors(status_code=status_code, response_body=resp_body, config=config)
+ return resp_body
+
+ def _request_retry_session(
+ self,
+ url_base: str,
+ retries: int = 1,
+ backoff_factor=1,
+ status_force_list=(429, 500, 502, 503, 504),
+ ) -> Session:
+ """A requests `Session()` that has retries enforced."""
+ retry: Retry = Retry(
+ total=retries,
+ read=retries,
+ connect=retries,
+ backoff_factor=backoff_factor,
+ status_forcelist=status_force_list,
+ )
+ adapter: HTTPAdapter = HTTPAdapter(max_retries=retry)
+ self.session.mount("http://", adapter=adapter)
+ self.session.mount("https://", adapter=adapter)
+ self.session.url_base = url_base
+ return self.session
+
+ @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: str = f"shipengine-python/{__version__}"
+ platform_os = platform.system()
+ os_version = platform.release()
+ python_version: str = platform.python_version()
+ python_implementation: str = platform.python_implementation()
+
+ return f"shipengine-python/{sdk_version} {platform_os}/{os_version} {python_implementation}/{python_version}"
diff --git a/shipengine/shipengine.py b/shipengine/shipengine.py
new file mode 100644
index 0000000..23a89f2
--- /dev/null
+++ b/shipengine/shipengine.py
@@ -0,0 +1,160 @@
+"""The entrypoint to the ShipEngine API SDK."""
+from typing import Any, Dict, List, Union
+
+from shipengine.enums import Endpoints
+
+from .http_client import ShipEngineClient
+from .shipengine_config import ShipEngineConfig
+
+
+class ShipEngine:
+ config: ShipEngineConfig
+ """
+ Global configuration for the ShipEngine API client, such as timeouts,
+ retries, page size, etc. This configuration applies to all method calls,
+ unless specifically overridden when calling a method.
+ """
+
+ def __init__(self, config: Union[str, Dict[str, Any], ShipEngineConfig]) -> None:
+ """
+ Exposes the functionality of the ShipEngine API.
+
+ The `api_key` you pass in can be either a ShipEngine sandbox
+ or production API Key. (sandbox keys start with "TEST_")
+ """
+ self.client = ShipEngineClient()
+
+ if type(config) is str:
+ self.config = ShipEngineConfig({"api_key": config})
+ elif type(config) is dict:
+ self.config = ShipEngineConfig(config)
+
+ def create_label_from_rate(
+ self, rate_id: str, params: Dict[str, Any], config: Union[str, Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
+ """
+ When retrieving rates for shipments using the /rates endpoint, the returned information contains a rateId
+ property that can be used to generate a label without having to refill in the shipment information repeatedly.
+ See: https://shipengine.github.io/shipengine-openapi/#operation/create_label_from_rate
+
+ :param str rate_id: The rate_id you wish to create a shipping label for.
+ :param Dict[str, Any] params: A dictionary of label params that will dictate the label display and
+ level of verification.
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns Dict[str, Any]: A label that corresponds the to shipment details for the rate_id provided.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.post(endpoint=f"v1/labels/rates/{rate_id}", params=params, config=config)
+
+ def create_label_from_shipment(
+ self, params: Dict[str, Any], config: Union[str, Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
+ """
+ Purchase and print a shipping label for a given shipment.
+ See: https://shipengine.github.io/shipengine-openapi/#operation/create_label
+
+ :param Dict[str, Any] params: A dictionary of shipment details for the label creation.
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns Dict[str, Any]: A label that corresponds the to shipment details provided.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.post(endpoint="v1/labels", params=params, config=config)
+
+ def get_rates_from_shipment(
+ self, shipment: Dict[str, Any], config: Union[str, Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
+ """
+ Given some shipment details and rate options, this endpoint returns a list of rate quotes.
+ See: https://shipengine.github.io/shipengine-openapi/#operation/calculate_rates
+
+ :param Dict[str, Any] shipment: A dictionary of shipment details for the label creation.
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns Dict[str, Any]: A label that corresponds the to shipment details provided.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.post(
+ endpoint=Endpoints.GET_RATE_FROM_SHIPMENT.value, params=shipment, config=config
+ )
+
+ def list_carriers(self, config: Dict[str, Any] = None) -> Dict[str, Any]:
+ """
+ Fetch the carrier accounts connected to your ShipEngine Account.
+
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns Dict[str, Any]: The carrier accounts associated with a given ShipEngine Account.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.get(endpoint=Endpoints.LIST_CARRIERS.value, config=config)
+
+ def track_package_by_label_id(
+ self, label_id: str, config: Dict[str, Any] = None
+ ) -> Dict[str, Any]:
+ """
+ Retrieve a given shipping label's tracking information with a label_id.
+ See: https://shipengine.github.io/shipengine-openapi/#operation/get_tracking_log_from_label
+
+ :param str label_id: The label_id for a shipment you wish to get tracking information for.
+ (Best option if you create labels via ShipEngine API)
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns Dict[str, Any]: Tracking information corresponding to the label_id provided.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.get(endpoint=f"v1/labels/{label_id}/track", config=config)
+
+ def track_package_by_carrier_code_and_tracking_number(
+ self, carrier_code: str, tracking_number: str, config: Dict[str, Any] = None
+ ) -> Dict[str, Any]:
+ """
+ Retrieve the label's tracking information with Carrier Code and Tracking Number.
+ See: https://shipengine.github.io/shipengine-openapi/#operation/get_tracking_log
+
+ :param str carrier_code: The carrier_code for the carrier servicing the shipment.
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns Dict[str, Any]: Tracking information corresponding to the carrier_code and tracking_number provided.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.get(
+ endpoint=f"v1/tracking?carrier_code={carrier_code}&tracking_number={tracking_number}",
+ config=config,
+ )
+
+ def validate_addresses(
+ self, address: List[Dict[str, Any]], config: Union[str, Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
+ """
+ Address validation ensures accurate addresses and can lead to reduced shipping costs by preventing address
+ correction surcharges. ShipEngine cross references multiple databases to validate addresses and identify
+ potential deliverability issues.
+ See: https://shipengine.github.io/shipengine-openapi/#operation/validate_address
+
+ :param List[Dict[str, Any]] address: A list containing the address(es) to be validated.
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns: Dict[str, Any]: The response from ShipEngine API including the validated and normalized address.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.post(
+ endpoint=Endpoints.ADDRESSES_VALIDATE.value, params=address, config=config
+ )
+
+ def void_label_by_label_id(
+ self, label_id: str, config: Union[str, Dict[str, Any]]
+ ) -> Dict[str, Any]:
+ """
+ Void label with a Label Id.
+ See: https://shipengine.github.io/shipengine-openapi/#operation/void_label
+
+ :param str label_id: The label_id of the label you wish to void.
+ :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values
+ for properties of the global ShipEngineConfig object.
+ :returns Dict[str, Any]: The response from ShipEngine API confirming the label was successfully voided or
+ unable to be voided.
+ """
+ config = self.config.merge(new_config=config)
+ return self.client.put(endpoint=f"v1/labels/{label_id}/void", config=config)
diff --git a/shipengine_sdk/shipengine_config.py b/shipengine/shipengine_config.py
similarity index 84%
rename from shipengine_sdk/shipengine_config.py
rename to shipengine/shipengine_config.py
index 596bbc1..3bd6617 100644
--- a/shipengine_sdk/shipengine_config.py
+++ b/shipengine/shipengine_config.py
@@ -2,13 +2,12 @@
import json
from typing import Any, Dict, Optional
-from .events import ShipEngineEventListener
-from .models import Endpoints
+from .enums import BaseURL
from .util import is_api_key_valid, is_retries_valid, is_timeout_valid
class ShipEngineConfig:
- DEFAULT_BASE_URI: str = Endpoints.SHIPENGINE_RPC_URL.value
+ DEFAULT_BASE_URI: str = BaseURL.SHIPENGINE_RPC_URL.value
"""A ShipEngine API Key, sandbox API Keys start with `TEST_`."""
DEFAULT_PAGE_SIZE: int = 50
@@ -50,11 +49,6 @@ def __init__(self, config: Dict[str, Any]) -> None:
else:
self.retries: int = self.DEFAULT_RETRIES
- if "event_listener" in config:
- self.event_listener = config["event_listener"]
- else:
- self.event_listener = ShipEngineEventListener()
-
def merge(self, new_config: Optional[Dict[str, Any]] = None):
"""
The method allows the merging of a method-level configuration
@@ -85,12 +79,6 @@ def merge(self, new_config: Optional[Dict[str, Any]] = None):
{"timeout": new_config["timeout"]}
) if "timeout" in new_config else config.update({"timeout": self.timeout})
- config.update(
- {"event_listener": new_config["event_listener"]}
- ) if "event_listener" in new_config else config.update(
- {"event_listener": self.event_listener}
- )
-
return ShipEngineConfig(config)
def to_dict(self):
diff --git a/shipengine_sdk/util/__init__.py b/shipengine/util/__init__.py
similarity index 93%
rename from shipengine_sdk/util/__init__.py
rename to shipengine/util/__init__.py
index 638584c..bd4b611 100644
--- a/shipengine_sdk/util/__init__.py
+++ b/shipengine/util/__init__.py
@@ -1,5 +1,4 @@
"""Testing a string manipulation helper function."""
-from .iso_string import IsoString
from .sdk_assertions import * # noqa
diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine/util/sdk_assertions.py
similarity index 57%
rename from shipengine_sdk/util/sdk_assertions.py
rename to shipengine/util/sdk_assertions.py
index b45c2a0..791139e 100644
--- a/shipengine_sdk/util/sdk_assertions.py
+++ b/shipengine/util/sdk_assertions.py
@@ -2,15 +2,15 @@
import re
from typing import Any, Dict, List
+from shipengine.enums import Country, ErrorCode, ErrorSource, ErrorType
+
from ..errors import (
ClientSystemError,
- ClientTimeoutError,
InvalidFieldValueError,
RateLimitExceededError,
ShipEngineError,
ValidationError,
)
-from ..models.enums import Country, ErrorCode, ErrorSource, ErrorType
validation_message = "Invalid address. Either the postal code or the city/locality and state/province must be specified." # noqa
@@ -20,14 +20,14 @@ def is_street_valid(street: List[str]) -> None:
if len(street) == 0:
raise ValidationError(
message="Invalid address. At least one address line is required.",
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.FIELD_VALUE_REQUIRED.value,
)
elif len(street) > 3:
raise ValidationError(
message="Invalid address. No more than 3 street lines are allowed.",
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.INVALID_FIELD_VALUE.value,
)
@@ -43,7 +43,7 @@ def is_city_valid(city: str) -> None:
elif not latin_pattern.match(city) or city == "":
raise ValidationError(
message=validation_message,
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.FIELD_VALUE_REQUIRED.value,
)
@@ -59,7 +59,7 @@ def is_state_valid(state: str) -> None:
elif not latin_pattern.match(state) or state == "":
raise ValidationError(
message=validation_message,
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.FIELD_VALUE_REQUIRED.value,
)
@@ -72,7 +72,7 @@ def is_postal_code_valid(postal_code: str) -> None:
if not pattern.match(postal_code) or postal_code == "":
raise ValidationError(
message=validation_message,
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.FIELD_VALUE_REQUIRED.value,
)
@@ -83,7 +83,7 @@ def is_country_code_valid(country: str) -> None:
if country not in (member.value for member in Country):
raise ValidationError(
message=f"Invalid address: [{country}] is not a valid country code.",
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.FIELD_VALUE_REQUIRED.value,
)
@@ -101,7 +101,7 @@ def is_api_key_valid(config: Dict[str, Any]) -> None:
if "api_key" not in config or config["api_key"] == "":
raise ValidationError(
message=message,
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.FIELD_VALUE_REQUIRED.value,
)
@@ -109,7 +109,7 @@ def is_api_key_valid(config: Dict[str, Any]) -> None:
if re.match(r"\s", config["api_key"]):
raise ValidationError(
message=message,
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.FIELD_VALUE_REQUIRED.value,
)
@@ -128,7 +128,7 @@ def is_retries_valid(config: Dict[str, Any]) -> None:
field_name="retries",
reason="Retries must be zero or greater.",
field_value=config["retries"],
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
)
@@ -145,7 +145,7 @@ def is_timeout_valid(config: Dict[str, Any]) -> None:
field_name="timeout",
reason="Timeout must be zero or greater.",
field_value=config["timeout"],
- source=ErrorSource.SHIPENGINE.value,
+ error_source=ErrorSource.SHIPENGINE.value,
)
@@ -161,7 +161,7 @@ def api_key_validation_error_assertions(error) -> None:
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.error_source is ErrorSource.SHIPENGINE.value
assert error.message == "A ShipEngine API key must be specified."
@@ -171,117 +171,74 @@ def timeout_validation_error_assertions(error) -> None:
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
+ assert error.error_source is ErrorSource.SHIPENGINE.value
def check_response_for_errors(status_code: int, response_body: Dict[str, Any], config) -> None:
- """Checks response and status_code for 404, 429, and 500 error cases and raises an approved exception."""
+ """Checks response and status_code for 400, 404, 429, and 500 error cases and raises an approved exception."""
+ if status_code != 200:
+ error = response_body["errors"][0]
- # Check if status_code is 404 and raises an error if so.
- if "error" in response_body and status_code == 404:
- error = response_body["error"]
- error_data = error["data"]
- raise ClientSystemError(
+ if status_code == 400:
+ raise ShipEngineError(
message=error["message"],
- request_id=response_body["id"],
- source=error_data["source"],
- error_type=error_data["type"],
- error_code=error_data["code"],
+ error_source=ErrorSource.SHIPENGINE.value,
+ error_type=error["error_type"],
+ error_code=error["error_code"],
)
- elif status_code == 404:
+
+ if status_code == 404:
raise ShipEngineError(
- message=f"Resource not found, please check the base_uri you have set and try again. [{config.base_uri}] is currently set.", # noqa
- source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.SYSTEM.value,
- error_code=ErrorCode.NOT_FOUND.value,
+ message=error["message"],
+ error_source=ErrorSource.SHIPENGINE.value,
+ error_type=error["error_type"],
+ error_code=error["error_code"],
)
# 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"]
- error_data = error["data"]
- retry_after = error_data["details"]["retryAfter"]
- if retry_after > config.timeout:
- raise ClientTimeoutError(
- retry_after=config.timeout,
- source=ErrorSource.SHIPENGINE.value,
- request_id=response_body["id"],
- )
- else:
- raise RateLimitExceededError(
- retry_after=retry_after,
- source=ErrorSource.SHIPENGINE.value,
- request_id=response_body["id"],
- )
+ if status_code == 429:
+ # TODO: Need to access retry after in response headers and add back in the below code.
+ # retry_after = error["details"]["retryAfter"]
+ # if retry_after > config.timeout:
+ # raise ClientTimeoutError(
+ # retry_after=config.timeout,
+ # error_source=ErrorSource.SHIPENGINE.value,
+ # request_id=response_body["request_id"],
+ # )
+ # else:
+ raise RateLimitExceededError(
+ # retry_after=retry_after,
+ error_source=ErrorSource.SHIPENGINE.value,
+ request_id=response_body["request_id"],
+ )
# 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"],
- )
-
-
-def does_normalized_address_have_errors(result) -> None:
- """
- Assertions to check if the returned normalized address has Any errors. If errors
- are present an exception is thrown.
-
- :param AddressValidateResult result: The address validation response from ShipEngine API.
- """
- if len(result.errors) > 1:
- error_list = list()
- for err in result.errors:
- error_list.append(err["message"])
-
- str_errors = "\n".join(error_list)
-
- raise ShipEngineError(
- message=f"Invalid address.\n{str_errors}",
- request_id=result.request_id,
- source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.ERROR.value,
- error_code=ErrorCode.INVALID_ADDRESS.value,
- )
- elif len(result.errors) == 1:
- raise ShipEngineError(
- message=f"Invalid address. {result.errors[0]['message']}",
- request_id=result.request_id,
- source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.ERROR.value,
- error_code=result.errors[0]["code"],
- )
- elif result.is_valid is False:
- raise ShipEngineError(
- message="Invalid address - The address provided could not be normalized.",
- request_id=result.request_id,
- source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.ERROR.value,
- error_code=ErrorCode.INVALID_ADDRESS.value,
- )
-
-
-def is_package_id_valid(package_id: str) -> None:
- """Checks that package_id is valid."""
- pattern = re.compile(r"^pkg_[1-9a-zA-Z]+$")
-
- if not package_id.startswith("pkg_"):
- raise ValidationError(
- message=f"[{package_id[0:4]}] is not a valid package ID prefix.",
- source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.VALIDATION.value,
- error_code=ErrorCode.INVALID_IDENTIFIER.value,
- )
-
- if not pattern.match(package_id):
- raise ValidationError(
- message=f"[{package_id}] is not a valid package ID.",
- source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.VALIDATION.value,
- error_code=ErrorCode.INVALID_IDENTIFIER.value,
- )
+ request_id=response_body["request_id"],
+ error_source=error["error_source"],
+ error_type=error["error_type"],
+ error_code=error["error_code"],
+ )
+
+
+# def is_package_id_valid(package_id: str) -> None:
+# """Checks that package_id is valid."""
+# pattern = re.compile(r"^pkg_[1-9a-zA-Z]+$")
+#
+# if not package_id.startswith("pkg_"):
+# raise ValidationError(
+# message=f"[{package_id[0:4]}] is not a valid package ID prefix.",
+# error_source=ErrorSource.SHIPENGINE.value,
+# error_type=ErrorType.VALIDATION.value,
+# error_code=ErrorCode.INVALID_IDENTIFIER.value,
+# )
+#
+# if not pattern.match(package_id):
+# raise ValidationError(
+# message=f"[{package_id}] is not a valid package ID.",
+# error_source=ErrorSource.SHIPENGINE.value,
+# error_type=ErrorType.VALIDATION.value,
+# error_code=ErrorCode.INVALID_IDENTIFIER.value,
+# )
diff --git a/shipengine_sdk/async_http_client/__init__.py b/shipengine_sdk/async_http_client/__init__.py
deleted file mode 100644
index 0cb716b..0000000
--- a/shipengine_sdk/async_http_client/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Initial Docstring."""
diff --git a/shipengine_sdk/events/__init__.py b/shipengine_sdk/events/__init__.py
deleted file mode 100644
index d0a5cde..0000000
--- a/shipengine_sdk/events/__init__.py
+++ /dev/null
@@ -1,203 +0,0 @@
-"""
-ShipEngine event emission via Observer Pattern. The ShipEngine SDK emits when an
-HTTP request is sent and when an HTTP response is received for said request.
-"""
-import json
-from dataclasses import dataclass
-from datetime import datetime
-from typing import Any, Callable, Dict, List, Optional, Union
-
-from dataclasses_json import dataclass_json
-
-from ..errors import ShipEngineError
-from ..models.enums import Events
-
-
-class ShipEngineEvent:
- timestamp: datetime
-
- def __init__(self, event_type: str, message: str) -> None:
- self.timestamp = datetime.now()
- self.type = event_type
- self.message = message
-
- 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)
-
- @staticmethod
- def new_event_message(method: str, base_uri: str, message_type: str) -> str:
- """A method to dynamically create an event message based on the $messageType being passed in."""
- if message_type == "base_message":
- return f"Calling the ShipEngine {method} API at {base_uri}"
- elif message_type == "retry_message":
- return f"Retrying the ShipEngine {method} API at {base_uri}"
- else:
- raise ShipEngineError(f"Message type [{message_type}] is not a valid type of message.")
-
-
-class RequestSentEvent(ShipEngineEvent):
- REQUEST_SENT = "request_sent"
-
- def __init__(
- self,
- request_id: str,
- message: str,
- base_uri: str,
- headers: List[str],
- body: Dict[str, Any],
- retry: int,
- timeout: int,
- ) -> None:
- super().__init__(event_type=self.REQUEST_SENT, message=message)
- self.request_id = request_id
- self.base_uri = base_uri
- self.headers = headers
- self.body = body
- self.retry = retry
- self.timeout = timeout
-
-
-class ResponseReceivedEvent(ShipEngineEvent):
- RESPONSE_RECEIVED = "response_received"
-
- def __init__(
- self,
- message: str,
- request_id: str,
- base_uri: str,
- status_code: int,
- headers: List[str],
- body: Dict[str, Any],
- retry: int,
- elapsed: float,
- ) -> None:
- super().__init__(event_type=self.RESPONSE_RECEIVED, message=message)
- self.request_id = request_id
- self.base_uri = base_uri
- self.status_code = status_code
- self.headers = headers
- self.body = body
- self.retry = retry
- self.elapsed = elapsed
-
-
-class Dispatcher:
- def __init__(self, events: Optional[List[str]] = None) -> None:
- events_list = [Events.ON_REQUEST_SENT.value, Events.ON_RESPONSE_RECEIVED.value]
- if events:
- for i in events:
- events_list.append(i)
-
- self.events = {event: dict() for event in events_list}
-
- def get_subscribers(self, event: Optional[str] = None):
- return self.events[event]
-
- 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):
- del self.get_subscribers(event)[subscriber]
-
- def dispatch(self, event, event_name: str = None):
- for subscriber, callback in self.get_subscribers(event_name).items():
- callback(event)
-
- 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)
-
-
-class Subscriber:
- def __init__(self, name=None) -> None:
- if name is not None:
- self.name = name
- else:
- self.name = "Event Subscriber"
-
- @staticmethod
- 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)
-
- def to_json(self):
- return json.dumps(self, default=lambda o: o.__dict__, indent=2)
-
-
-class ShipEngineEventListener(Subscriber):
- def __init__(self, name=None) -> None:
- super().__init__(name=name)
-
- # You can add your own event consumption logic by adding/overriding the parent `update()` method below.
- @staticmethod
- def update(event: Union[RequestSentEvent, ResponseReceivedEvent]):
- # print(event.to_dict())
- return event
-
-
-def emit_event(emitted_event_type: str, event_data, dispatcher: Dispatcher):
- if emitted_event_type == RequestSentEvent.REQUEST_SENT:
- request_sent_event = RequestSentEvent(
- message=event_data.message,
- request_id=event_data.id,
- base_uri=event_data.base_uri,
- headers=event_data.request_headers,
- body=event_data.body,
- retry=event_data.retry,
- timeout=event_data.timeout,
- )
- dispatcher.dispatch(event=request_sent_event, event_name=Events.ON_REQUEST_SENT.value)
- return request_sent_event
- elif emitted_event_type == ResponseReceivedEvent.RESPONSE_RECEIVED:
- response_received_event = ResponseReceivedEvent(
- message=event_data.message,
- request_id=event_data.id,
- base_uri=event_data.base_uri,
- status_code=event_data.status_code,
- headers=event_data.response_headers,
- body=event_data.body,
- retry=event_data.retry,
- elapsed=event_data.elapsed,
- )
- dispatcher.dispatch(
- event=response_received_event, event_name=Events.ON_RESPONSE_RECEIVED.value
- )
- return response_received_event
- else:
- raise ShipEngineError(f"Event type [{emitted_event_type}] is not a valid type of event.")
-
-
-@dataclass_json
-@dataclass
-class EventOptions:
- """To be used as the main argument in the **emitEvent()** function."""
-
- message: Optional[str]
- id: Optional[str]
- base_uri: Optional[str]
- body: Optional[Dict[str, Any]]
- retry: Optional[int]
- status_code: Optional[int] = None
- request_headers: Optional[Dict[str, Any]] = None
- response_headers: Any = None
- timeout: Optional[int] = None
- elapsed: Optional[float] = None
diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py
deleted file mode 100644
index 7bc08ad..0000000
--- a/shipengine_sdk/http_client/client.py
+++ /dev/null
@@ -1,201 +0,0 @@
-"""A synchronous HTTP Client for the ShipEngine SDK."""
-import json
-import os
-import platform
-from datetime import datetime
-from typing import Any, Dict, Optional
-
-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 __version__
-
-from ..errors import ShipEngineError
-from ..events import (
- Dispatcher,
- EventOptions,
- RequestSentEvent,
- ResponseReceivedEvent,
- ShipEngineEvent,
- emit_event,
-)
-from ..jsonrpc.process_request import handle_response, wrap_request
-from ..models import ErrorCode, ErrorSource, ErrorType
-from ..models.enums import Events
-from ..shipengine_config import ShipEngineConfig
-from ..util.sdk_assertions import check_response_for_errors
-
-
-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,
- 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"
- )
- else:
- return ShipEngineEvent.new_event_message(
- method=method, base_uri=base_uri, message_type="retry_message"
- )
-
-
-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,
- }
-
-
-class ShipEngineAuth(AuthBase):
- def __init__(self, api_key: str) -> None:
- """Auth Base appends `Api-Key` header to all requests."""
- self.api_key: str = api_key
-
- def __call__(self, request: Request, *args, **kwargs) -> Request:
- request.headers["Api-Key"] = self.api_key
- return request
-
-
-class ShipEngineClient:
- _DISPATCHER: Dispatcher = Dispatcher()
-
- def __init__(self, config: ShipEngineConfig) -> None:
- """A `JSON-RPC 2.0` HTTP client used to send all HTTP requests from the SDK."""
- self._DISPATCHER.register(
- event=Events.ON_REQUEST_SENT.value, subscriber=config.event_listener
- )
- self._DISPATCHER.register(
- event=Events.ON_RESPONSE_RECEIVED.value, subscriber=config.event_listener
- )
- self.session = requests.session()
-
- def send_rpc_request(
- self, method: str, params: Optional[Dict[str, Any]], retry: int, config: ShipEngineConfig
- ) -> Dict[str, Any]:
- """
- 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.
- """
- client: Session = self._request_retry_session(retries=config.retries)
- base_uri = base_url(config=config)
-
- request_body: Dict[str, Any] = wrap_request(method=method, params=params)
- req_headers = request_headers(user_agent=self._derive_user_agent(), api_key=config.api_key)
- req: Request = Request(
- method="POST",
- url=base_uri,
- data=json.dumps(request_body),
- headers=req_headers,
- auth=ShipEngineAuth(config.api_key),
- )
- prepared_req: PreparedRequest = req.prepare()
-
- request_event_message = generate_event_message(
- retry=retry, method=method, base_uri=base_uri
- )
-
- request_event_data = EventOptions(
- message=request_event_message,
- id=request_body["id"],
- base_uri=base_uri,
- request_headers=req_headers,
- body=request_body,
- retry=retry,
- timeout=config.timeout,
- )
- request_sent_event = emit_event(
- emitted_event_type=RequestSentEvent.REQUEST_SENT,
- event_data=request_event_data,
- dispatcher=self._DISPATCHER,
- )
-
- 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: 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=response_received_message,
- id=request_body["id"],
- base_uri=base_uri,
- status_code=status_code,
- response_headers=resp.headers,
- body=request_body,
- retry=retry,
- elapsed=(request_sent_event.timestamp - datetime.now()).total_seconds(),
- )
-
- # Emit `ResponseReceivedEvent` to registered Subscribers.
- emit_event(
- emitted_event_type=ResponseReceivedEvent.RESPONSE_RECEIVED,
- event_data=response_event_data,
- dispatcher=self._DISPATCHER,
- )
-
- check_response_for_errors(status_code=status_code, response_body=resp_body, config=config)
- return 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 = Retry(
- total=retries,
- read=retries,
- connect=retries,
- backoff_factor=backoff_factor,
- status_forcelist=status_force_list,
- )
- adapter: HTTPAdapter = HTTPAdapter(max_retries=retry)
- self.session.mount("http://", adapter=adapter)
- self.session.mount("https://", adapter=adapter)
- return self.session
-
- @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: str = f"shipengine-python/{__version__}"
- python_version: str = platform.python_version()
- python_implementation: str = platform.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
deleted file mode 100644
index 7502077..0000000
--- a/shipengine_sdk/jsonrpc/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""
-A collection of methods that provide `JSON-RPC 2.0` HTTP client
-functionality for sending HTTP requests from the ShipEngine SDK.
-"""
-import time
-from typing import Any, Dict, Optional
-
-from ..errors import RateLimitExceededError
-from ..http_client import ShipEngineClient
-from ..shipengine_config import ShipEngineConfig
-from .process_request import handle_response, wrap_request
-
-
-def rpc_request(
- method: str, config: ShipEngineConfig, params: Optional[Dict[str, Any]] = None
-) -> Dict[str, Any]:
- """Create and send a `JSON-RPC 2.0` request over HTTP messages."""
- return rpc_request_loop(method, params, config)
-
-
-def rpc_request_loop(
- method: str, params: Optional[Dict[str, Any]], config: ShipEngineConfig
-) -> Dict[str, Any]:
- client: ShipEngineClient = ShipEngineClient(config=config)
- 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)
- retry += 1
- continue
- else:
- raise err
- return api_response
diff --git a/shipengine_sdk/jsonrpc/process_request.py b/shipengine_sdk/jsonrpc/process_request.py
deleted file mode 100644
index 05fa692..0000000
--- a/shipengine_sdk/jsonrpc/process_request.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""Functions that help with process requests and handle responses."""
-from typing import Any, Dict, Optional
-
-from fuuid import b58_fuuid
-
-from ..errors import (
- AccountStatusError,
- BusinessRuleError,
- ClientSecurityError,
- ClientSystemError,
- ShipEngineError,
- ValidationError,
-)
-from ..models import ErrorType
-
-
-def wrap_request(method: str, params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
- """
- 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_{b58_fuuid()}", jsonrpc="2.0", method=method)
- else:
- return dict(id=f"req_{b58_fuuid()}", jsonrpc="2.0", method=method, params=params)
-
-
-def handle_response(response_body: Dict[str, Any]) -> Dict[str, Any]:
- """Handles the response from ShipEngine API."""
- if "result" in response_body:
- return response_body
-
- error: Dict[str, Any] = response_body["error"]
- error_data: Dict[str, Any] = error["data"]
- error_type: str = 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
deleted file mode 100644
index a1ebfd0..0000000
--- a/shipengine_sdk/models/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""ShipEngine SDK Models & Enumerations"""
-from .address import Address, AddressValidateResult
-from .carriers import Carrier, CarrierAccount
-from .enums import (
- CarrierNames,
- Carriers,
- Country,
- Endpoints,
- ErrorCode,
- ErrorSource,
- ErrorType,
- RegexPatterns,
- RPCMethods,
- does_member_value_exist,
- get_carrier_name_value,
-)
-from .package import (
- Location,
- Package,
- Shipment,
- TrackingEvent,
- TrackingQuery,
- TrackPackageResult,
-)
diff --git a/shipengine_sdk/models/address/__init__.py b/shipengine_sdk/models/address/__init__.py
deleted file mode 100644
index a47a399..0000000
--- a/shipengine_sdk/models/address/__init__.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""Models to be used throughout the ShipEngine SDK."""
-import json
-from dataclasses import dataclass
-from typing import List, Optional
-
-from dataclasses_json import LetterCase, dataclass_json
-
-from ...util import (
- is_city_valid,
- is_country_code_valid,
- is_postal_code_valid,
- is_state_valid,
- is_street_valid,
-)
-
-
-@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] = False
- name: Optional[str] = ""
- phone: Optional[str] = ""
- company: Optional[str] = ""
-
- def __post_init__(self) -> None:
- is_street_valid(self.street)
- is_city_valid(self.city_locality)
- is_state_valid(self.state_province)
- is_postal_code_valid(self.postal_code)
- is_country_code_valid(self.country_code)
-
-
-class AddressValidateResult:
- is_valid: Optional[bool]
- request_id: str
- normalized_address: Optional[Address]
- info: Optional[List]
- warnings: Optional[List]
- errors: Optional[List]
-
- def __init__(
- self,
- is_valid: Optional[bool],
- request_id: str,
- normalized_address: Optional[Address],
- messages: List,
- info: Optional[List] = None,
- warnings: Optional[List] = None,
- errors: Optional[List] = None,
- ) -> None:
- self.is_valid = is_valid
- self.request_id = request_id
- self.normalized_address = normalized_address
- self.info = list() if info is None else info
- self.warnings = list() if warnings is None else warnings
- self.errors = list() if errors is None else errors
- self.__extract_messages(messages)
-
- def __extract_messages(self, messages):
- for message in messages:
- if message["type"] == "error":
- del message["type"]
- self.errors.append(message)
- elif message["type"] == "info":
- del message["type"]
- self.info.append(message)
- elif message["type"] == "warning":
- del message["type"]
- self.warnings.append(message)
-
- 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)
-
- def __repr__(self):
- return f"AddressValidateResult({self.is_valid}, {self.request_id}, {self.normalized_address}, {self.info}, {self.warnings}, {self.errors})" # noqa
diff --git a/shipengine_sdk/models/carriers/__init__.py b/shipengine_sdk/models/carriers/__init__.py
deleted file mode 100644
index a7c04fd..0000000
--- a/shipengine_sdk/models/carriers/__init__.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""CarrierAccount class object and immutable carrier object."""
-import json
-from typing import Any, Dict
-
-from ...errors import InvalidFieldValueError, ShipEngineError
-from ..enums import Carriers, does_member_value_exist, get_carrier_name_value
-
-
-class Carrier:
- def __init__(self, code: str) -> None:
- """This class represents a given account with a Carrier provider e.g. `FedEx`, `UPS`, `USPS`."""
- if not does_member_value_exist(code, Carriers):
- raise ShipEngineError(f"Carrier [{code}] not currently supported.")
- else:
- self.name = get_carrier_name_value(code.upper())
- self.code = code
-
- 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)
-
- def __repr__(self):
- return f"Carrier({self.code}, {self.name})"
-
-
-class CarrierAccount:
- carrier: Carrier
-
- def __init__(self, account_information: Dict[str, Any]) -> None:
- """This class represents a given account with a Carrier provider e.g. `FedEx`, `UPS`, `USPS`."""
- self._set_carrier(account_information["carrierCode"])
- self.account_id = account_information["accountId"]
- self.account_number = account_information["accountNumber"]
-
- def _set_carrier(self, carrier: str) -> None:
- if does_member_value_exist(carrier, Carriers):
- self.carrier = Carrier(code=carrier).to_dict()
- self.name = self.carrier["name"]
- else:
- InvalidFieldValueError(
- field_name="carrier",
- reason=f"Carrier [{carrier}] is currently not supported.",
- field_value=carrier,
- )
-
- 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)
-
- def __repr__(self):
- return (
- f"CarrierAccount({self.account_id}, {self.account_number}, {self.carrier}, {self.name})"
- )
diff --git a/shipengine_sdk/models/enums/carriers/__init__.py b/shipengine_sdk/models/enums/carriers/__init__.py
deleted file mode 100644
index ec6f94d..0000000
--- a/shipengine_sdk/models/enums/carriers/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Carrier enumerations used throughout the ShipEngine SDK."""
-from .carrier_names import CarrierNames
-from .carriers import Carriers
diff --git a/shipengine_sdk/models/enums/carriers/carrier_names.py b/shipengine_sdk/models/enums/carriers/carrier_names.py
deleted file mode 100644
index fd8c8fd..0000000
--- a/shipengine_sdk/models/enums/carriers/carrier_names.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Enumeration of valid carrier names."""
-from enum import Enum
-
-
-class CarrierNames(Enum):
- """An enumeration valid carrier names."""
-
- FEDEX = "FedEx"
- """FedEx - Federal Express"""
-
- UPS = "United Parcel Service"
- """UPS - United Parcel Service"""
-
- USPS = "U.S. Postal Service"
- """USPS - United State Postal Service"""
-
- STAMPS_COM = "Stamps.com"
- """USPS services via Stamps.com"""
-
- DHL_EXPRESS = "DHL Express"
- """DHL Express"""
-
- DHL_GLOBAL_MAIL = "DHL ECommerce"
- """DHL ECommerce"""
-
- CANADA_POST = "Canada Post"
- """Canada Post"""
-
- AUSTRALIA_POST = "Australia Post"
- """Australia Post"""
-
- FIRSTMILE = "First Mile"
- """First Mile"""
-
- ASENDIA = "Asendia"
- """Asendia"""
-
- ONTRAC = "OnTrac"
- """OnTrac"""
-
- APC = "APC"
- """APC"""
-
- NEWGISTICS = "Newgistics"
- """Newgistics"""
-
- GLOBEGISTICS = "Globegistics"
- """Globegistics"""
-
- RR_DONNELLEY = "RR Donnelley"
- """RR Donnell"""
-
- IMEX = "IMEX"
- """IMEX"""
-
- ACCESS_WORLDWIDE = "Access Worldwide"
- """Access Worldwide"""
-
- PUROLATOR_CA = "Purolator Canada"
- """Purolator Canada"""
-
- SENDLE = "Sendle"
- """Sendle"""
diff --git a/shipengine_sdk/models/enums/carriers/carriers.py b/shipengine_sdk/models/enums/carriers/carriers.py
deleted file mode 100644
index a79342c..0000000
--- a/shipengine_sdk/models/enums/carriers/carriers.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Enumeration of valid carrier providers."""
-from enum import Enum
-
-
-class Carriers(Enum):
- """An enumeration of valid carrier providers."""
-
- FEDEX = "fedex"
- """FedEx - Federal Express"""
-
- UPS = "ups"
- """UPS - United Parcel Service"""
-
- USPS = "usps"
- """USPS - United State Postal Service"""
-
- STAMPS_COM = "stamps_com"
- """USPS services via Stamps.com"""
-
- DHL_EXPRESS = "dhl_express"
- """DHL Express"""
-
- DHL_GLOBAL_MAIL = "dhl_global_mail"
- """DHL ECommerce"""
-
- CANADA_POST = "canada_post"
- """Canada Post"""
-
- AUSTRALIA_POST = "australia_post"
- """Australia Post"""
-
- FIRSTMILE = "firstmile"
- """First Mile"""
-
- ASENDIA = "asendia"
- """Asendia"""
-
- ONTRAC = "ontrac"
- """OnTrac"""
-
- APC = "apc"
- """APC"""
-
- NEWGISTICS = "newgistics"
- """Newgistics"""
-
- GLOBEGISTICS = "globegistics"
- """Globegistics"""
-
- RR_DONNELLEY = "rr_donnelley"
- """RR Donnell"""
-
- IMEX = "imex"
- """IMEX"""
-
- ACCESS_WORLDWIDE = "access_worldwide"
- """Access Worldwide"""
-
- PUROLATOR_CA = "purolator_ca"
- """Purolator Canada"""
-
- SENDLE = "sendle"
- """Sendle"""
diff --git a/shipengine_sdk/models/package/__init__.py b/shipengine_sdk/models/package/__init__.py
deleted file mode 100644
index daeeff0..0000000
--- a/shipengine_sdk/models/package/__init__.py
+++ /dev/null
@@ -1,264 +0,0 @@
-"""Data objects to be used in the `track_package` and `track` methods."""
-import json
-from dataclasses import dataclass
-from typing import Any, Dict, List, Optional, Union
-
-from dataclasses_json import LetterCase, dataclass_json
-
-from ...errors import ShipEngineError
-from ...services.get_carrier_accounts import GetCarrierAccounts
-from ...shipengine_config import ShipEngineConfig
-from ...util.iso_string import IsoString
-from .. import Carrier, CarrierAccount
-
-
-class Shipment:
- config: ShipEngineConfig
- shipment_id: Optional[str] = None
- account_id: Optional[str] = None
- carrier_account: Optional[CarrierAccount] = None
- carrier: Optional[Carrier] = None
- estimated_delivery_date: Union[IsoString, str]
- actual_delivery_date: Union[IsoString, str]
-
- def __init__(
- self, shipment: Dict[str, Any], actual_delivery_date: IsoString, config: ShipEngineConfig
- ) -> None:
- """This object represents a given Shipment."""
- self.config = config
- self.shipment_id = shipment["shipmentId"] if "shipmentId" in shipment else None
- self.account_id = shipment["carrierAccountId"] if "carrierAccountId" in shipment else None
-
- if self.account_id is not None:
- self.carrier_account = self._get_carrier_account(
- carrier=shipment["carrierCode"], account_id=self.account_id
- )
-
- if self.carrier_account is not None:
- self.carrier = self.carrier_account.carrier
- else:
- self.carrier = (
- Carrier(shipment["carrierCode"])
- if "carrierCode" in shipment
- else ShipEngineError("The carrierCode field was null from api response.")
- )
-
- self.estimated_delivery_date = IsoString(iso_string=shipment["estimatedDelivery"])
- self.actual_delivery_date = actual_delivery_date
-
- def _get_carrier_account(self, carrier: str, account_id: str) -> CarrierAccount:
- get_accounts: GetCarrierAccounts = GetCarrierAccounts()
- target_carrier: List[CarrierAccount] = list()
- carrier_accounts: List[CarrierAccount] = get_accounts.fetch_cached_carrier_accounts(
- carrier_code=carrier, config=self.config
- )
-
- for account in carrier_accounts:
- if account_id == account.account_id:
- target_carrier.append(account)
- return target_carrier[0]
-
- raise ShipEngineError(
- message=f"accountId [{account_id}] doesn't match any of the accounts connected to your ShipEngine Account." # noqa
- )
-
- def to_dict(self) -> Dict[str, Any]:
- if hasattr(self, "config"):
- del self.config
- else:
- pass # noqa
- return (lambda o: o.__dict__)(self)
-
- def to_json(self) -> str:
- if hasattr(self, "config"):
- del self.config
- else:
- pass # noqa
- return json.dumps(self, default=lambda o: o.__dict__, indent=2)
-
- def __repr__(self):
- return f"Shipment({self.shipment_id}, {self.account_id})"
-
-
-class Package:
- """This object contains package information for a given shipment."""
-
- package_id: Optional[str]
- weight: Optional[Dict[str, Any]]
- dimensions: Optional[Dict[str, Any]]
- tracking_number: Optional[str]
- tracking_url: Optional[str]
-
- def __init__(self, package: Dict[str, Any]) -> None:
- self.package_id = package["packageId"] if "packageId" in package else None
- self.weight = package["weight"] if "weight" in package else None
- self.dimensions = package["dimensions"] if "dimensions" in package else None
- self.tracking_number = package["trackingNumber"] if "trackingNumber" in package else None
- self.tracking_url = package["trackingUrl"] if "trackingUrl" in package else None
-
- def to_dict(self) -> Dict[str, Any]:
- return (lambda o: o.__dict__)(self)
-
- def to_json(self) -> str:
- return json.dumps(self, default=lambda o: o.__dict__, indent=2)
-
- def __repr__(self):
- return f"Package({self.package_id}, {self.weight}, {self.dimensions}, {self.tracking_number}, {self.tracking_url})" # noqa
-
-
-@dataclass_json(letter_case=LetterCase.CAMEL)
-@dataclass
-class TrackingQuery:
- """This object is used as an argument in the `track_package` and `track` methods."""
-
- carrier_code: str
- tracking_number: str
-
-
-class Location:
- city_locality: Optional[str]
- state_province: Optional[str]
- postal_code: Optional[str]
- country_code: Optional[str]
- latitude: Optional[float] = None
- longitude: Optional[float] = None
-
- def __init__(self, location_data: Dict[str, Any]) -> None:
- self.city_locality = (
- location_data["cityLocality"]
- if "cityLocality" in location_data and location_data is not None
- else None
- )
- self.state_province = (
- location_data["stateProvince"]
- if "stateProvince" in location_data and location_data is not None
- else None
- )
- self.postal_code = (
- location_data["postalCode"]
- if "postalCode" in location_data and location_data is not None
- else None
- )
- self.country_code = (
- location_data["countryCode"]
- if "countryCode" in location_data and location_data is not None
- else None
- )
-
- if (
- "coordinates" in location_data
- and location_data is not None
- and location_data["coordinates"] is not None
- ):
- self.latitude = location_data["coordinates"]["latitude"]
- self.longitude = location_data["coordinates"]["longitude"]
-
- 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)
-
- def __repr__(self):
- return f"Location({self.city_locality}, {self.state_province}, {self.postal_code}, {self.country_code}, {self.latitude}, {self.longitude})" # noqa
-
-
-class TrackingEvent:
- date_time: Union[IsoString, str]
- carrier_date_time: Union[IsoString, str]
- status: str
- description: Optional[str]
- carrier_status_code: Optional[str]
- carrier_detail_code: Optional[str]
- signer: Optional[str]
- location: Optional[Location]
-
- def __init__(self, event: Dict[str, Any]) -> None:
- """Tracking event object."""
- self.date_time = IsoString(iso_string=event["timestamp"])
-
- self.carrier_date_time = IsoString(iso_string=event["carrierTimestamp"])
-
- self.status = event["status"]
- self.description = event["description"] if "description" in event else 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"])
- if "location" in event and event["location"] is not None
- else None
- )
-
- 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)
-
- def __repr__(self):
- return f"TrackingEvent({self.date_time.to_string()}, {self.date_time.to_string()}, {self.status}, {self.description}, {self.carrier_status_code}, {self.carrier_detail_code}, {self.signer}, {self.location})" # noqa
-
-
-class TrackPackageResult:
- shipment: Optional[Shipment]
- package: Optional[Package]
- events: Optional[List[TrackingEvent]] = list()
-
- def __init__(self, api_response: Dict[str, Any], config: ShipEngineConfig) -> None:
- """This object is used as the return type for the `track_package` and `track` methods."""
- self.events = list()
- result = api_response["result"]
- for event in result["events"]:
- self.events.append(TrackingEvent(event=event))
-
- self.shipment = (
- Shipment(
- shipment=result["shipment"],
- actual_delivery_date=self.get_latest_event().date_time,
- config=config,
- )
- if "shipment" in result
- else None
- )
- self.package = Package(result["package"]) if "package" in result else None
-
- def get_errors(self) -> List[TrackingEvent]:
- """Returns **only** the exception events."""
- errors: List[TrackingEvent] = list()
- for event in self.events:
- if event.status == "exception":
- errors.append(event)
- return errors
-
- def get_latest_event(self) -> TrackingEvent:
- """Returns the latest event to have occurred in the `events` list."""
- return self.events[-1]
-
- def has_errors(self) -> bool:
- """Returns `true` if there are any exception events."""
- for event in self.events:
- if event.status == "exception":
- return True
- return False
-
- def to_dict(self):
- if hasattr(self.shipment, "config"):
- del self.shipment.config
- else:
- pass # noqa
- return (lambda o: o.__dict__)(self)
-
- def to_json(self):
- if hasattr(self.shipment, "config"):
- del self.shipment.config
- else:
- pass # noqa
- return json.dumps(self, default=lambda o: o.__dict__, indent=2)
-
- def __repr__(self):
- return f"TrackPackageResult({self.shipment}, {self.package}, {self.events})"
diff --git a/shipengine_sdk/services/__init__.py b/shipengine_sdk/services/__init__.py
deleted file mode 100644
index a677db9..0000000
--- a/shipengine_sdk/services/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""ShipEngine SDK service objects."""
diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py
deleted file mode 100644
index 31b4552..0000000
--- a/shipengine_sdk/services/address_validation.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Validate a single address or multiple addresses."""
-from typing import Any, Dict
-
-from ..jsonrpc import rpc_request
-from ..models.address import Address, AddressValidateResult
-from ..models.enums import RPCMethods
-from ..shipengine_config import ShipEngineConfig
-from ..util import does_normalized_address_have_errors
-
-
-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: Dict[str, Any] = rpc_request(
- method=RPCMethods.ADDRESS_VALIDATE.value,
- config=config,
- params={"address": address.to_dict()},
- )
- result: Dict[str, Any] = api_response["result"]
- return AddressValidateResult(
- is_valid=result["isValid"],
- request_id=api_response["id"],
- normalized_address=Address.from_dict(result["normalizedAddress"])
- if "normalizedAddress" in result and result["normalizedAddress"] is not None
- else None,
- messages=result["messages"],
- )
-
-
-def normalize(address: Address, config: ShipEngineConfig) -> Address:
- """
- Normalize a given address into a standardized format.
-
- :param Address address: The address to be validate.
- :param ShipEngineConfig config: The global ShipEngine configuration object.
- :returns: :class:`Address`: The normalized address returned from ShipEngine API.
- """
- validation_result: AddressValidateResult = validate(address=address, config=config)
- does_normalized_address_have_errors(result=validation_result)
- return validation_result.normalized_address
diff --git a/shipengine_sdk/services/get_carrier_accounts.py b/shipengine_sdk/services/get_carrier_accounts.py
deleted file mode 100644
index c5da08e..0000000
--- a/shipengine_sdk/services/get_carrier_accounts.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""
-Fetch the carrier account connected to a given ShipEngine Account
-based on the API Key passed into the ShipEngine SDK.
-"""
-from typing import List, Optional
-
-from ..jsonrpc import rpc_request
-from ..models import CarrierAccount, RPCMethods
-from ..shipengine_config import ShipEngineConfig
-
-cached_accounts: List = list()
-
-
-class GetCarrierAccounts:
- @staticmethod
- def fetch_carrier_accounts(
- config: ShipEngineConfig, carrier_code: Optional[str] = None
- ) -> List[CarrierAccount]:
- global cached_accounts
- if carrier_code is not None:
- api_response = rpc_request(
- method=RPCMethods.LIST_CARRIERS.value,
- config=config,
- params={"carrierCode": carrier_code},
- )
- else:
- api_response = rpc_request(
- method=RPCMethods.LIST_CARRIERS.value,
- config=config,
- )
-
- accounts = api_response["result"]["carrierAccounts"]
- cached_accounts = list()
- for account in accounts:
- carrier_account = CarrierAccount(account)
- cached_accounts.append(carrier_account)
-
- return cached_accounts
-
- def fetch_cached_carrier_accounts(
- self, config: ShipEngineConfig, carrier_code: Optional[str]
- ) -> List[CarrierAccount]:
- global cached_accounts
- accounts = cached_accounts
- return (
- accounts
- if len(cached_accounts) > 0
- else self.fetch_carrier_accounts(config=config, carrier_code=carrier_code)
- )
-
- @staticmethod
- def get_cached_accounts_by_carrier_code(carrier_code: Optional[str]) -> List[CarrierAccount]:
- global cached_accounts
- accounts = list()
- if carrier_code is None:
- return cached_accounts
- else:
- for account in cached_accounts:
- if account.carrier["code"] == carrier_code:
- accounts.append(account)
- return accounts
diff --git a/shipengine_sdk/services/track_package.py b/shipengine_sdk/services/track_package.py
deleted file mode 100644
index 764d6c2..0000000
--- a/shipengine_sdk/services/track_package.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Track a given package to obtain status updates on it's progression through the fulfillment cycle."""
-from typing import Union
-
-from ..jsonrpc import rpc_request
-from ..models import RPCMethods, TrackingQuery, TrackPackageResult
-from ..shipengine_config import ShipEngineConfig
-from ..util import is_package_id_valid
-
-
-def track(tracking_data: Union[str, TrackingQuery], config: ShipEngineConfig) -> TrackPackageResult:
- if type(tracking_data) is str:
- is_package_id_valid(tracking_data)
-
- api_response = rpc_request(
- method=RPCMethods.TRACK_PACKAGE.value,
- config=config,
- params={"packageId": tracking_data},
- )
-
- return TrackPackageResult(api_response, config)
-
- if type(tracking_data) is TrackingQuery:
- api_response = rpc_request(
- method=RPCMethods.TRACK_PACKAGE.value, config=config, params=tracking_data.to_dict() # type: ignore
- )
-
- return TrackPackageResult(api_response, config)
diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py
deleted file mode 100644
index 0f76639..0000000
--- a/shipengine_sdk/shipengine.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""The entrypoint to the ShipEngine API SDK."""
-from typing import Any, Dict, List, Optional, Union
-
-from .models import CarrierAccount, TrackingQuery, TrackPackageResult
-from .models.address import Address, AddressValidateResult
-from .services.address_validation import normalize, validate
-from .services.get_carrier_accounts import GetCarrierAccounts
-from .services.track_package import track
-from .shipengine_config import ShipEngineConfig
-
-
-class ShipEngine:
- config: ShipEngineConfig
- """
- Global configuration for the ShipEngine API client, such as timeouts,
- retries, page size, etc. This configuration applies to all method calls,
- unless specifically overridden when calling a method.
- """
-
- def __init__(self, config: Union[str, Dict[str, Any], ShipEngineConfig]) -> None:
- """
- Exposes the functionality of the ShipEngine API.
-
- The `api_key` you pass in can be either a ShipEngine sandbox
- or production API Key. (sandbox keys start with "TEST_")
- """
-
- if type(config) is str:
- self.config = ShipEngineConfig({"api_key": config})
- elif type(config) is dict:
- self.config = ShipEngineConfig(config)
-
- def validate_address(
- self, address: Address, config: Optional[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=address, config=config)
-
- def normalize_address(
- self, address: Address, config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None
- ) -> Address:
- """Normalize a given address into a standardized format used by carriers."""
- config = self.config.merge(new_config=config)
- return normalize(address=address, config=config)
-
- def get_carrier_accounts(
- self,
- carrier_code: Optional[str] = None,
- config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None,
- ) -> List[CarrierAccount]:
- """Fetch a list of the carrier accounts connected to your ShipEngine Account."""
- config = self.config.merge(new_config=config)
- get_accounts = GetCarrierAccounts()
- return get_accounts.fetch_carrier_accounts(config=config, carrier_code=carrier_code)
-
- def track_package(
- self,
- tracking_data: Union[str, TrackingQuery],
- config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None,
- ) -> TrackPackageResult:
- """
- Track a package by `tracking_number` and `carrier_code` via the **TrackingQuery** object, by using just the
- **package_id**.
- """
- config = self.config.merge(new_config=config)
- return track(tracking_data=tracking_data, config=config)
diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py
deleted file mode 100644
index cdd02e3..0000000
--- a/shipengine_sdk/util/iso_string.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""Initial Docstring"""
-import re
-from datetime import datetime
-
-from ..models.enums import RegexPatterns
-
-
-class IsoString:
- 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
-
- def __str__(self) -> str:
- return f"{self.iso_string}"
-
- def to_string(self) -> str:
- return self.iso_string
-
- 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")
- else:
- return datetime.fromisoformat(iso_string)
-
- def has_timezone(self) -> bool:
- 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_with_tz(iso_str: str):
- pattern = re.compile(RegexPatterns.VALID_ISO_STRING.value)
- if pattern.match(iso_str):
- return True
- else:
- return False
-
- @staticmethod
- 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
- else:
- return False
-
- @staticmethod
- def _maybe_add_microseconds(iso_str: str):
- if "." not in iso_str:
- if "Z" not in iso_str:
- return iso_str + ".0"
- else:
- return iso_str[:-1] + ".0Z"
- else:
- return iso_str
diff --git a/tests/errors/test_errors.py b/tests/errors/test_errors.py
index e748dec..dea3afc 100644
--- a/tests/errors/test_errors.py
+++ b/tests/errors/test_errors.py
@@ -1,7 +1,7 @@
"""Tests for the ShipEngine SDK Errors"""
import pytest
-from shipengine_sdk.errors import (
+from shipengine.errors import (
AccountStatusError,
BusinessRuleError,
ClientSecurityError,
@@ -23,7 +23,7 @@ def shipengine_error():
raise ShipEngineError(
request_id="req_a523b1b19bd54054b7eb953f000e7f15",
message="The is a test exception",
- source="shipengine",
+ error_source="shipengine",
error_type="validation",
error_code="invalid_address",
url="https://google.com",
@@ -35,7 +35,7 @@ def shipengine_error_with_no_error_type() -> ShipEngineError:
raise ShipEngineError(
request_id="req_a523b1b19bd54054b7eb953f000e7f15",
message="The is a test exception",
- source="shipengine",
+ error_source="shipengine",
error_type=None,
error_code="invalid_address",
)
@@ -46,7 +46,7 @@ def shipengine_error_with_bad_error_type() -> ShipEngineError:
raise ShipEngineError(
request_id="req_a523b1b19bd54054b7eb953f000e7f15",
message="The is a test exception",
- source="shipengine",
+ error_source="shipengine",
error_type="tracking",
error_code="invalid_address",
)
@@ -57,7 +57,7 @@ def shipengine_error_with_bad_error_source() -> ShipEngineError:
raise ShipEngineError(
request_id="req_a523b1b19bd54054b7eb953f000e7f15",
message="The is a test exception",
- source="wayne_enterprises",
+ error_source="wayne_enterprises",
error_type="validation",
error_code="invalid_address",
)
@@ -68,7 +68,7 @@ def shipengine_error_with_bad_error_code() -> ShipEngineError:
raise ShipEngineError(
request_id="req_a523b1b19bd54054b7eb953f000e7f15",
message="The is a test exception",
- source="shipengine",
+ error_source="shipengine",
error_type="validation",
error_code="failure",
)
diff --git a/tests/events/__init__.py b/tests/events/__init__.py
deleted file mode 100644
index 39fe0c3..0000000
--- a/tests/events/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests around events emitted from ShipEngine SDK."""
diff --git a/tests/events/test_emitted_events.py b/tests/events/test_emitted_events.py
deleted file mode 100644
index 790ea0c..0000000
--- a/tests/events/test_emitted_events.py
+++ /dev/null
@@ -1,238 +0,0 @@
-"""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("-")) <= 6
- )
diff --git a/tests/http_client/test_http_client.py b/tests/http_client/test_http_client.py
deleted file mode 100644
index b41776e..0000000
--- a/tests/http_client/test_http_client.py
+++ /dev/null
@@ -1,97 +0,0 @@
-"""Testing basic ShipEngineClient functionality."""
-import pytest
-import responses
-
-from shipengine_sdk import ShipEngine
-from shipengine_sdk.errors import ClientSystemError
-from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType
-from shipengine_sdk.models.address import Address
-from shipengine_sdk.models.enums import Endpoints
-
-
-def validate_address(address):
- shipengine = ShipEngine(
- dict(
- api_key="baz",
- page_size=50,
- retries=2,
- timeout=10,
- )
- )
- return shipengine.validate_address(address)
-
-
-def valid_residential_address() -> Address:
- return Address(
- street=["4 Jersey St"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def get_500_server_error() -> Address:
- return Address(
- street=["500 Server Error"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-class TestShipEngineClient:
- @responses.activate
- def test_500_server_response(self):
- responses.add(
- responses.POST,
- Endpoints.SHIPENGINE_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,
- )
- try:
- validate_address(get_500_server_error())
- except ClientSystemError as e:
- assert e.message == "Unable to connect to the database"
- assert e.request_id is not None
- assert e.source == ErrorSource.SHIPENGINE.value
- assert e.error_type == ErrorType.SYSTEM.value
- assert e.error_code == ErrorCode.UNSPECIFIED.value
- with pytest.raises(ClientSystemError):
- validate_address(get_500_server_error())
-
- @responses.activate
- def test_404_server_response(self):
- responses.add(
- responses.POST,
- Endpoints.SHIPENGINE_RPC_URL.value,
- json={
- "jsonrpc": "2.0",
- "id": "req_DezVNUvRkAP819f3JeqiuS",
- "error": {
- "code": "-32603",
- "message": "Content not found.",
- "data": {"source": "shipengine", "type": "system", "code": "not_found"},
- },
- },
- status=404,
- )
- try:
- validate_address(valid_residential_address())
- except ClientSystemError as e:
- assert e.message == "Content not found."
- assert e.request_id is not None
- assert e.source == ErrorSource.SHIPENGINE.value
- assert e.error_type == ErrorType.SYSTEM.value
- assert e.error_code == ErrorCode.NOT_FOUND.value
- with pytest.raises(ClientSystemError):
- validate_address(valid_residential_address())
diff --git a/tests/jsonrpc/test_process_request.py b/tests/jsonrpc/test_process_request.py
deleted file mode 100644
index 5d8dce8..0000000
--- a/tests/jsonrpc/test_process_request.py
+++ /dev/null
@@ -1,90 +0,0 @@
-"""Testing the process request and response functions."""
-import pytest
-
-from shipengine_sdk.errors import (
- AccountStatusError,
- BusinessRuleError,
- ClientSecurityError,
- ClientSystemError,
- ShipEngineError,
- ValidationError,
-)
-from shipengine_sdk.jsonrpc import handle_response, wrap_request
-from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType, RPCMethods
-
-
-def handle_response_errors(error_source: str, error_code: str, error_type: str):
- return handle_response(
- {
- "jsonrpc": "2.0",
- "id": "req_0938jf40398j4f09s8hfd",
- "error": {
- "code": 12345,
- "message": "Test message from the test suite.",
- "data": {"source": error_source, "type": error_type, "code": error_code},
- },
- }
- )
-
-
-class TestProcessRequest:
- """Test the handle request and response functionality."""
-
- def test_wrap_request_with_no_params(self) -> None:
- """Unit test for the `wrap_request` method used by the client."""
- request_body = wrap_request(method=RPCMethods.ADDRESS_VALIDATE.value, params=None)
- assert "params" not in request_body
-
- def test_account_status_handling(self) -> None:
- """Unit test for the `handle_response` method account status error case."""
- with pytest.raises(AccountStatusError):
- handle_response_errors(
- error_source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.ACCOUNT_STATUS.value,
- error_code=ErrorCode.TERMS_NOT_ACCEPTED.value,
- )
-
- def test_security_error_case(self) -> None:
- """Unit test for the `handle_response` method security error case."""
- with pytest.raises(ClientSecurityError):
- handle_response_errors(
- error_source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.SECURITY.value,
- error_code=ErrorCode.UNAUTHORIZED.value,
- )
-
- def test_validation_error_case(self) -> None:
- """Unit test for the `handle_response` method validation error case."""
- with pytest.raises(ValidationError):
- handle_response_errors(
- error_source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.VALIDATION.value,
- error_code=ErrorCode.INVALID_FIELD_VALUE.value,
- )
-
- def test_business_rule_error_case(self) -> None:
- """Unit test for the `handle_response` method business rule error case."""
- with pytest.raises(BusinessRuleError):
- handle_response_errors(
- error_source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.BUSINESS_RULES.value,
- error_code=ErrorCode.TRACKING_NOT_SUPPORTED.value,
- )
-
- def test_system_error_case(self) -> None:
- """Unit test for the `handle_response` method system error case."""
- with pytest.raises(ClientSystemError):
- handle_response_errors(
- error_source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.SYSTEM.value,
- error_code=ErrorCode.UNSPECIFIED.value,
- )
-
- def test_shipengine_error_case(self) -> None:
- """Unit test for the `handle_response` method shipengine error case."""
- with pytest.raises(ShipEngineError):
- handle_response_errors(
- error_source=ErrorSource.SHIPENGINE.value,
- error_type=ErrorType.AUTHORIZATION.value,
- error_code=ErrorCode.TERMS_NOT_ACCEPTED.value,
- )
diff --git a/tests/models/__init__.py b/tests/models/__init__.py
deleted file mode 100644
index 44cbe32..0000000
--- a/tests/models/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests on the models used throughout the ShipEngine SDK."""
diff --git a/tests/models/address/test_address.py b/tests/models/address/test_address.py
deleted file mode 100644
index 150c6f7..0000000
--- a/tests/models/address/test_address.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Test the instantiation of the Address object and it's validations."""
-import pytest
-
-from shipengine_sdk.errors import ValidationError
-from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType
-from tests.util import address_with_too_many_lines, empty_address_lines
-
-
-def address_line_assertions(err: ValidationError, variant: str) -> None:
- """"""
- assert type(err) is ValidationError
- assert err.request_id is None
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.VALIDATION.value
-
- if variant == "empty_address_lines":
- assert err.message == "Invalid address. At least one address line is required."
- assert err.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value
- elif variant == "too_many_address_lines":
- assert err.message == "Invalid address. No more than 3 street lines are allowed."
- assert err.error_code is ErrorCode.INVALID_FIELD_VALUE.value
-
-
-class TestAddress:
- def test_no_address_lines(self):
- """DX-1033/DX-1051 - No address lines in the street list."""
- try:
- empty_address_lines()
- except ValidationError as err:
- address_line_assertions(err, "empty_address_lines")
- with pytest.raises(ValidationError):
- empty_address_lines()
-
- def test_address_with_too_many_lines(self):
- """DX-1034/DX-1052 - Too many address lines."""
- try:
- address_with_too_many_lines()
- except ValidationError as err:
- address_line_assertions(err, "too_many_address_lines")
diff --git a/tests/models/carriers/test_carrier.py b/tests/models/carriers/test_carrier.py
deleted file mode 100644
index c920047..0000000
--- a/tests/models/carriers/test_carrier.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Test the Carrier class object."""
-from shipengine_sdk.errors import ShipEngineError
-from shipengine_sdk.models import Carrier
-
-
-class TestCarrier:
- def test_invalid_carrier(self) -> None:
- try:
- Carrier(code="royal_mail")
- except ShipEngineError as err:
- assert err.message == "Carrier [royal_mail] not currently supported."
-
- def test_to_json(self) -> None:
- carrier = Carrier(code="fedex")
- assert type(carrier.to_json()) is str
diff --git a/tests/models/carriers/test_carrier_account.py b/tests/models/carriers/test_carrier_account.py
deleted file mode 100644
index 4fd8ab0..0000000
--- a/tests/models/carriers/test_carrier_account.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Test the CarrierAccount class object."""
-from typing import Any, Dict
-
-from shipengine_sdk.errors import InvalidFieldValueError
-from shipengine_sdk.models import CarrierAccount
-
-
-def stub_carrier_account_object() -> Dict[str, Any]:
- """
- Return a dictionary that mimics the data this object would be passed
- from the returned ShipEngine API response.
- """
- return {
- "accountId": "car_1knseddGBrseWTiw",
- "accountNumber": "1169350",
- "carrierCode": "royal_mail",
- "name": "United Parcel Service",
- }
-
-
-class TestCarrierAccount:
- def test_carrier_account_with_invalid_carrier(self) -> None:
- k = stub_carrier_account_object()
- try:
- CarrierAccount(account_information=k)
- except InvalidFieldValueError as err:
- assert err.message == f"Carrier [{k['carrierCode']}] is currently not supported."
-
- def test_carrier_account_to_dict(self) -> None:
- k = stub_carrier_account_object()
- carrier_account = CarrierAccount(account_information=k)
-
- assert type(carrier_account.to_dict()) is dict
-
- def test_carrier_account_to_json(self) -> None:
- k = stub_carrier_account_object()
- carrier_account = CarrierAccount(account_information=k)
-
- assert type(carrier_account.to_json()) is str
diff --git a/tests/models/track_package/__init__.py b/tests/models/track_package/__init__.py
deleted file mode 100644
index 6491bdf..0000000
--- a/tests/models/track_package/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Test for the models used in the `track_package` method of the ShipEngine SDK."""
diff --git a/tests/models/track_package/test_package.py b/tests/models/track_package/test_package.py
deleted file mode 100644
index b21d5db..0000000
--- a/tests/models/track_package/test_package.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Testing the Package class object."""
-from typing import Any, Dict
-
-from shipengine_sdk.models import Package
-
-
-def stub_package_data() -> Dict[str, Any]:
- return {
- "packageId": "pkg_1FedExAccepted",
- "trackingNumber": "5fSkgyuh3GkfUjTZSEAQ8gHeTU29tZ",
- "trackingUrl": "https://www.fedex.com/track/5fSkgyuh3GkfUjTZSEAQ8gHeTU29tZ",
- "weight": {"value": 76, "unit": "kilogram"},
- "dimensions": {"length": 36, "width": 36, "height": 23, "unit": "inch"},
- }
-
-
-def stub_package() -> Package:
- """Return a valid stub Package object."""
- return Package(stub_package_data())
-
-
-class TestPackage:
- def test_package_to_dict(self) -> None:
- package = stub_package()
- assert type(package.to_dict()) is dict
-
- def test_package_to_json(self) -> None:
- package = stub_package()
- assert type(package.to_json()) is str
diff --git a/tests/models/track_package/test_shipment.py b/tests/models/track_package/test_shipment.py
deleted file mode 100644
index ba30ac7..0000000
--- a/tests/models/track_package/test_shipment.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""Testing the Shipment class object."""
-from typing import Any, Dict
-
-import pytest
-
-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
-
-
-def stub_valid_shipment_data() -> Dict[str, Any]:
- """
- Return a dictionary that mimics the Shipment data that would
- be returned by ShipEngine API.
- """
- return {
- "carrierCode": "fedex",
- "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value,
- "shipmentId": "shp_yuh3GkfUjTZSEAQ",
- "estimatedDelivery": "2021-06-15T21:00:00.000Z",
- }
-
-
-def stub_invalid_shipment_data() -> Dict[str, Any]:
- """
- Return a dictionary that mimics the Shipment data that would
- be returned by ShipEngine API, where the `carrierAccountId` is invalid.
- """
- return {
- "carrierCode": "fedex",
- "carrierAccountId": "car_kfUoSHIPENGINEQ8gHeT",
- "shipmentId": "shp_yuh3GkfUjTZSEAQ",
- "estimatedDelivery": "2021-06-15T21:00:00.000Z",
- }
-
-
-def stub_invalid_account_id_shipment_instantiation() -> Shipment:
- """Return a test Shipment object that has an invalid `carrierAccountId`.."""
- return Shipment(
- shipment=stub_invalid_shipment_data(),
- actual_delivery_date=IsoString("2021-06-10T21:00:00.000"),
- config=stub_shipengine_config(),
- )
-
-
-def stub_valid_shipment_instantiation() -> Shipment:
- """Return a valid test Shipment object."""
- return Shipment(
- shipment=stub_valid_shipment_data(),
- actual_delivery_date=IsoString("2021-06-10T21:00:00.000"),
- config=stub_shipengine_config(),
- )
-
-
-class TestShipment:
- def test_get_carrier_account_failure_via_invalid_account_id(self) -> None:
- with pytest.raises(ShipEngineError):
- stub_invalid_account_id_shipment_instantiation()
-
- def test_shipment_to_dict(self) -> None:
- shipment = stub_valid_shipment_instantiation()
- assert type(shipment.to_dict()) is dict
-
- def test_shipment_to_json(self) -> None:
- shipment = stub_valid_shipment_instantiation()
- assert type(shipment.to_json()) is str
diff --git a/tests/models/track_package/test_track_package_result.py b/tests/models/track_package/test_track_package_result.py
deleted file mode 100644
index f7541a0..0000000
--- a/tests/models/track_package/test_track_package_result.py
+++ /dev/null
@@ -1,148 +0,0 @@
-"""Testing the TrackPackageResult class object."""
-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
-
-
-def stub_track_package_data() -> Dict[str, Any]:
- """
- Return a dictionary that mimics the track_package response
- from ShipEngine API.
- """
- return {
- "jsonrpc": "2.0",
- "id": "req_1de9ca85b8544c1c91cd17abc43cbb5e",
- "result": {
- "shipment": {
- "carrierCode": "fedex",
- "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value,
- "shipmentId": "shp_tJUaQJz3Twz57iL",
- "estimatedDelivery": "2021-06-15T21:00:00.000Z",
- },
- "package": {
- "packageId": "pkg_1FedexDeLiveredException",
- "trackingNumber": "2A4g3tJUaQJz3Twz57iLWBciD7wZWH",
- "trackingUrl": "https://www.fedex.com/track/2A4g3tJUaQJz3Twz57iLWBciD7wZWH",
- "weight": {"value": 76, "unit": "kilogram"},
- "dimensions": {"length": 36, "width": 36, "height": 23, "unit": "inch"},
- },
- "events": [
- {
- "timestamp": "2021-06-10T19:00:00.000Z",
- "carrierTimestamp": "2021-06-11T01:00:00",
- "status": "accepted",
- "description": "Dropped-off at shipping center",
- "carrierStatusCode": "ACPT-2",
- "carrierDetailCode": "PU7W",
- },
- {
- "timestamp": "2021-06-11T01:00:00.000Z",
- "carrierTimestamp": "2021-06-11T07:00:00",
- "status": "in_transit",
- "description": "En-route to distribution center hub",
- "carrierStatusCode": "ER00P",
- },
- {
- "timestamp": "2021-06-11T20:00:00.000Z",
- "carrierTimestamp": "2021-06-12T02:00:00",
- "status": "unknown",
- "description": "Mechanically sorted",
- "carrierStatusCode": "MMSa",
- "carrierDetailCode": "00004134918400045",
- },
- {
- "timestamp": "2021-06-12T10:00:00.000Z",
- "carrierTimestamp": "2021-06-12T16:00:00",
- "status": "in_transit",
- "description": "On vehicle for delivery",
- "carrierStatusCode": "OFD-22",
- "carrierDetailCode": "91R-159E",
- },
- {
- "timestamp": "2021-06-12T17:00:00.000Z",
- "carrierTimestamp": "2021-06-12T23:00:00",
- "status": "exception",
- "description": "Weather delay",
- "carrierStatusCode": "EX026",
- "carrierDetailCode": "XX00016",
- "location": {
- "cityLocality": "Pittsburgh",
- "stateProvince": "PA",
- "postalCode": "15218",
- "countryCode": "US",
- },
- },
- {
- "timestamp": "2021-06-13T02:00:00.000Z",
- "carrierTimestamp": "2021-06-13T08:00:00",
- "status": "exception",
- "description": "Equipment failure",
- "carrierStatusCode": "EX038",
- "carrierDetailCode": "XX00184",
- "location": {
- "cityLocality": "Pittsburgh",
- "stateProvince": "PA",
- "postalCode": "15218",
- "countryCode": "US",
- },
- },
- {
- "timestamp": "2021-06-13T10:00:00.000Z",
- "carrierTimestamp": "2021-06-13T16:00:00",
- "status": "in_transit",
- "description": "On vehicle for delivery",
- "carrierStatusCode": "OFD-22",
- "carrierDetailCode": "91R-159E",
- },
- {
- "timestamp": "2021-06-13T20:00:00.000Z",
- "carrierTimestamp": "2021-06-14T02:00:00",
- "status": "delivered",
- "description": "Delivered",
- "carrierStatusCode": "DV99-0004",
- "signer": "John P. Doe",
- "location": {
- "cityLocality": "Pittsburgh",
- "stateProvince": "PA",
- "postalCode": "15218",
- "countryCode": "US",
- "coordinates": {"latitude": 40.4504687, "longitude": -79.9352761},
- },
- },
- ],
- },
- }
-
-
-def stub_track_package_result() -> TrackPackageResult:
- """Return a valid stub TrackPackageResult object."""
- return TrackPackageResult(
- api_response=stub_track_package_data(), config=stub_shipengine_config()
- )
-
-
-class TestTrackPackageResult:
- def test_get_errors(self) -> None:
- result = stub_track_package_result()
- err = result.get_errors()
- assert type(err) is list
- assert len(err) == 2
-
- def test_has_errors(self) -> None:
- result = stub_track_package_result()
- assert result.has_errors() is True
-
- def test_get_latest_event(self) -> None:
- result = stub_track_package_result()
- assert type(result.get_latest_event()) is TrackingEvent
-
- def test_track_package_result_to_dict(self) -> None:
- result = stub_track_package_result()
- assert type(result.to_dict()) is dict
-
- def test_track_package_result_to_json(self) -> None:
- result = stub_track_package_result()
- assert type(result.to_json()) is str
diff --git a/tests/models/track_package/test_tracking_event.py b/tests/models/track_package/test_tracking_event.py
deleted file mode 100644
index 4e37682..0000000
--- a/tests/models/track_package/test_tracking_event.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Testing the TrackingEvent class object."""
-from typing import Any, Dict
-
-from shipengine_sdk.models import TrackingEvent
-
-
-def stub_event_data() -> Dict[str, Any]:
- """Return a dictionary that mimics a tracking event object in a response from ShipEngine API."""
- return {
- "timestamp": "2021-06-13T13:00:00.000Z",
- "carrierTimestamp": "2021-06-13T19:00:00",
- "status": "accepted",
- "description": "Dropped-off at shipping center",
- "carrierStatusCode": "ACPT-2",
- }
-
-
-def stub_tracking_event() -> TrackingEvent:
- """Return a valid stub TrackingEvent object."""
- return TrackingEvent(stub_event_data())
-
-
-class TestTrackingEvent:
- def test_tracking_event_to_dict(self) -> None:
- tracking_event = stub_tracking_event()
- assert type(tracking_event.to_dict()) is dict
-
- def test_tracking_event_to_json(self) -> None:
- tracking_event = stub_tracking_event()
- assert type(tracking_event.to_json()) is str
diff --git a/tests/services/__init__.py b/tests/services/__init__.py
index b5bcf10..bd4a435 100644
--- a/tests/services/__init__.py
+++ b/tests/services/__init__.py
@@ -1 +1 @@
-"""Initial Docstring"""
+"""Tests for the core services in the ShipEngine SDK."""
diff --git a/tests/services/test_address_validation.py b/tests/services/test_address_validation.py
deleted file mode 100644
index bab1cfb..0000000
--- a/tests/services/test_address_validation.py
+++ /dev/null
@@ -1,263 +0,0 @@
-"""Test the validate address method of the ShipEngine SDK."""
-import re
-
-from shipengine_sdk.errors import ClientSystemError, ShipEngineError, ValidationError
-from shipengine_sdk.models import (
- Address,
- AddressValidateResult,
- ErrorCode,
- ErrorSource,
- ErrorType,
-)
-
-from ..util.test_helpers import (
- address_missing_required_fields,
- address_with_all_fields,
- address_with_errors,
- address_with_invalid_country,
- address_with_invalid_postal_code,
- address_with_invalid_state,
- address_with_warnings,
- canada_valid_avs_assertions,
- get_server_side_error,
- multi_line_address,
- non_latin_address,
- unknown_address,
- valid_address_assertions,
- valid_canadian_address,
- valid_commercial_address,
- valid_residential_address,
- validate_an_address,
-)
-
-
-class TestValidateAddress:
- TEST_METHOD: str = "validate"
-
- def test_valid_residential_address(self) -> None:
- """DX-1024 - Valid residential address."""
- residential_address = valid_residential_address()
- validated_address = validate_an_address(residential_address)
- address = validated_address.normalized_address
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=residential_address,
- returned_address=validated_address,
- expected_residential_indicator=True,
- )
- assert (
- address.street[0]
- == (residential_address.street[0] + " " + residential_address.street[1])
- .replace(".", "")
- .upper()
- )
-
- def test_valid_commercial_address(self) -> None:
- """DX-1025 - Valid commercial address."""
- commercial_address = valid_commercial_address()
- validated_address = validate_an_address(commercial_address)
- address = validated_address.normalized_address
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=commercial_address,
- returned_address=validated_address,
- expected_residential_indicator=False,
- )
- assert (
- address.street[0]
- == (commercial_address.street[0] + " " + commercial_address.street[1])
- .replace(".", "")
- .upper()
- )
-
- def test_multi_line_address(self) -> None:
- """DX-1027 - Validate multiline address."""
- valid_multi_line_address = multi_line_address()
- validated_address = validate_an_address(valid_multi_line_address)
- address = validated_address.normalized_address
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=valid_multi_line_address,
- returned_address=validated_address,
- expected_residential_indicator=False,
- )
- assert (
- address.street[0]
- == (valid_multi_line_address.street[0] + " " + valid_multi_line_address.street[1])
- .replace(".", "")
- .upper()
- )
- assert address.street[1] == valid_multi_line_address.street[2].upper()
-
- def test_numeric_postal_code(self) -> None:
- """DX-1028 - Validate numeric postal code."""
- residential_address = valid_residential_address()
- validated_address = validate_an_address(residential_address)
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=residential_address,
- returned_address=validated_address,
- expected_residential_indicator=True,
- )
- assert re.match(r"\d", validated_address.normalized_address.postal_code)
-
- def test_alpha_postal_code(self) -> None:
- """DX-1029 - Alpha postal code."""
- canadian_address = valid_canadian_address()
- validated_address = validate_an_address(canadian_address)
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="international",
- original_address=canadian_address,
- returned_address=validated_address,
- expected_residential_indicator=False,
- )
-
- def test_unknown_address(self) -> None:
- """DX-1026 - Validate address of unknown address."""
- address = unknown_address()
- validated_address = validate_an_address(address)
- canada_valid_avs_assertions(
- original_address=address,
- validated_address=validated_address,
- expected_residential_indicator=None,
- )
-
- def test_address_with_non_latin_chars(self) -> None:
- """DX-1030 - non-latin characters."""
- non_latin = non_latin_address()
- validated_address = validate_an_address(non_latin)
- address = validated_address.normalized_address
-
- assert validated_address.is_valid is True
- assert address is not None
- assert type(address) is Address
- assert address.street[0] == "68 Kamitobatsunodacho"
- assert address.city_locality == "Kyoto-Shi Minami-Ku"
- assert address.state_province == "Kyoto"
- assert address.postal_code == non_latin.postal_code
- assert address.country_code == non_latin.country_code
- assert address.is_residential is False
- assert len(address.street) == 1
-
- def test_address_with_warnings(self) -> None:
- """DX-1031 - validate with warnings."""
- warnings_address = address_with_warnings()
- validated_address = validate_an_address(warnings_address)
- address = validated_address.normalized_address
-
- assert type(validated_address) is AddressValidateResult
- assert validated_address.is_valid is True
- assert type(address) is Address
- assert len(validated_address.info) == 0
- assert len(validated_address.warnings) != 0
- assert (
- validated_address.warnings[0]["code"]
- == ErrorCode.PARTIALLY_VERIFIED_TO_PREMISE_LEVEL.value
- )
- assert (
- validated_address.warnings[0]["message"]
- == "This address has been verified down to the house/building level (highest possible accuracy with the provided data)" # noqa
- )
- assert len(validated_address.errors) == 0
- assert address.city_locality == warnings_address.city_locality
- assert address.state_province == warnings_address.state_province.title()
- assert address.postal_code == "M6K 3C3"
- assert address.country_code == warnings_address.country_code.upper()
- assert address.is_residential is True
-
- def test_address_with_errors(self) -> None:
- """DX-1032 - Validate with error messages."""
- error_address = address_with_errors()
- validated_address = validate_an_address(error_address)
- address = validated_address.normalized_address
-
- assert type(validated_address) is AddressValidateResult
- assert validated_address.is_valid is False
- assert address is None
- assert len(validated_address.info) == 0
- assert len(validated_address.warnings) != 0
- assert validated_address.warnings[0]["message"] == "Address not found"
- assert len(validated_address.errors) != 0
- assert validated_address.errors[0]["code"] == ErrorCode.ADDRESS_NOT_FOUND.value
- assert validated_address.errors[0]["message"] == "Invalid City, State, or Zip"
- assert validated_address.errors[1]["code"] == ErrorCode.ADDRESS_NOT_FOUND.value
- assert validated_address.errors[1]["message"] == "Insufficient or Incorrect Address Data"
-
- def test_missing_city_state_and_postal_code(self) -> None:
- """DX-1035 & DX-1036 - Missing city, state, and postal code."""
- try:
- address_missing_required_fields()
- except ValidationError as err:
- assert err.request_id is None
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.VALIDATION.value
- assert err.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value
- assert (
- err.message
- == "Invalid address. Either the postal code or the city/locality and state/province must be specified." # noqa
- )
-
- def test_invalid_country_code(self) -> None:
- """DX-1037 - Invalid country code."""
- try:
- address_with_invalid_country()
- except ValidationError as err:
- assert err.request_id is None
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.VALIDATION.value
- assert err.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value
- assert err.message == "Invalid address: [RZ] is not a valid country code."
-
- def test_server_side_error(self) -> None:
- """DX-1038 - Server-side error."""
- try:
- get_server_side_error()
- except ClientSystemError as err:
- assert err.request_id is not None
- assert err.request_id.startswith("req_") is True
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.SYSTEM.value
- assert err.error_code is ErrorCode.UNSPECIFIED.value
-
- def test_address_with_name_company_phone(self) -> None:
- """DX-1393 - Validate address with name, company, and phone."""
- address = address_with_all_fields()
- validated_address = validate_an_address(address=address)
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=address,
- returned_address=validated_address,
- expected_residential_indicator=True,
- )
-
- def test_address_with_invalid_state(self) -> None:
- """Test validate_address when an invalid state is passed in."""
- try:
- address_with_invalid_state()
- except ShipEngineError as err:
- assert type(err) is ValidationError
- assert (
- err.message
- == "Invalid address. Either the postal code or the city/locality and state/province must be specified."
- ) # noqa
-
- def test_address_with_invalid_postal_code(self) -> None:
- """Test validate_address when an invalid postal code is passed in."""
- try:
- address_with_invalid_postal_code()
- except ShipEngineError as err:
- assert type(err) is ValidationError
- assert (
- err.message
- == "Invalid address. Either the postal code or the city/locality and state/province must be specified."
- ) # noqa
diff --git a/tests/services/test_get_carrier_accounts.py b/tests/services/test_get_carrier_accounts.py
deleted file mode 100644
index 1a1eabe..0000000
--- a/tests/services/test_get_carrier_accounts.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""Tests for the GetCarrierAccounts service in the ShipEngine SDK."""
-from shipengine_sdk.errors import ClientSystemError
-from shipengine_sdk.models import Carriers, ErrorCode, ErrorSource, ErrorType
-from shipengine_sdk.services.get_carrier_accounts import GetCarrierAccounts
-
-from ..util.test_helpers import stub_get_carrier_accounts
-
-
-class TestGetCarrierAccounts:
- def test_no_accounts_setup(self) -> None:
- """DX-1075 - No accounts setup yet."""
- accounts = stub_get_carrier_accounts(carrier_code="sendle")
-
- assert type(accounts) is list
- assert len(accounts) == 0
-
- def test_multiple_accounts(self) -> None:
- """DX-1076 - Multiple carrier accounts."""
- accounts = stub_get_carrier_accounts()
-
- assert len(accounts) == 5
- assert accounts[0].carrier["code"] == Carriers.UPS.value
- assert accounts[1].carrier["code"] == Carriers.FEDEX.value
- assert accounts[2].carrier["code"] == Carriers.FEDEX.value
- assert accounts[3].carrier["code"] == Carriers.USPS.value
- assert accounts[4].carrier["code"] == Carriers.STAMPS_COM.value
-
- for account in accounts:
- assert account.account_id.startswith("car_")
- assert account.name is not None
- assert account.account_number is not None
- assert account.carrier is not None
- assert account.carrier["code"] is not None
- assert type(account.carrier["code"]) == str
- assert account.carrier["name"] is not None
- assert type(account.carrier["name"]) == str
-
- def test_multiple_accounts_of_same_carrier(self):
- """DX-1077 - Multiple accounts of the same carrier."""
- accounts = stub_get_carrier_accounts()
-
- assert len(accounts) == 5
- assert accounts[0].carrier["code"] == Carriers.UPS.value
- assert accounts[0].account_id != accounts[1].account_id
- assert accounts[1].carrier["code"] == Carriers.FEDEX.value
- assert accounts[2].carrier["code"] == Carriers.FEDEX.value
- assert accounts[3].carrier["code"] == Carriers.USPS.value
- assert accounts[4].carrier["code"] == Carriers.STAMPS_COM.value
-
- for account in accounts:
- assert account.account_id.startswith("car_")
- assert account.name is not None
-
- def test_server_side_error(self) -> None:
- """DX-1078 - Get carrier accounts server-side error."""
- try:
- stub_get_carrier_accounts("access_worldwide")
- except ClientSystemError as err:
- assert err.request_id is not None
- assert err.request_id.startswith("req_") is True
- assert err.source == ErrorSource.SHIPENGINE.value
- assert err.error_type == ErrorType.SYSTEM.value
- assert err.error_code == ErrorCode.UNSPECIFIED.value
-
- def test_get_cached_accounts_by_carrier(self) -> None:
- get_accounts = GetCarrierAccounts()
- stub_get_carrier_accounts() # fill the cache
- accounts = get_accounts.get_cached_accounts_by_carrier_code(carrier_code="fedex")
-
- assert len(accounts) == 2
- assert accounts[0].carrier["code"] == "fedex"
- assert accounts[1].carrier["code"] == "fedex"
diff --git a/tests/services/test_get_rate_from_shipment.py b/tests/services/test_get_rate_from_shipment.py
new file mode 100644
index 0000000..9789475
--- /dev/null
+++ b/tests/services/test_get_rate_from_shipment.py
@@ -0,0 +1,211 @@
+"""Testing the get_rate_fro_shipment functionality in the ShipEngine SDK."""
+import json
+import unittest
+import urllib.parse as urlparse
+
+import responses
+
+from shipengine.enums import BaseURL, Endpoints
+from tests.util import stub_shipengine_instance
+
+
+class TestGetRateFromShipment(unittest.TestCase):
+ @responses.activate
+ def test_get_rate_from_shipment(self) -> None:
+ """Test get_rate_from_shipment functionality."""
+ responses.add(
+ **{
+ "method": responses.POST,
+ "url": urlparse.urljoin(
+ BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.GET_RATE_FROM_SHIPMENT.value
+ ),
+ "body": json.dumps(
+ {
+ "shipmentId": "se-141694059",
+ "carrierId": "se-161650",
+ "serviceCode": "usps_first_class_mail",
+ "externalOrderId": None,
+ "items": [],
+ "taxIdentifiers": None,
+ "externalShipmentId": None,
+ "shipDate": "2021-07-28T00:00:00Z",
+ "createdAt": "2021-07-28T16:56:40.257Z",
+ "modifiedAt": "2021-07-28T16:56:40.223Z",
+ "shipmentStatus": "pending",
+ "shipTo": {
+ "name": "James Atkinson",
+ "phone": None,
+ "companyName": None,
+ "addressLine1": "28793 Fox Fire Lane",
+ "addressLine2": None,
+ "addressLine3": None,
+ "cityLocality": "Shell Knob",
+ "stateProvince": "MO",
+ "postalCode": "65747",
+ "countryCode": "US",
+ "addressResidentialIndicator": "yes",
+ },
+ "shipFrom": {
+ "name": "Medals of America",
+ "phone": "800-308-0849",
+ "companyName": None,
+ "addressLine1": "114 Southchase Blvd",
+ "addressLine2": None,
+ "addressLine3": None,
+ "cityLocality": "Fountain Inn",
+ "stateProvince": "SC",
+ "postalCode": "29644",
+ "countryCode": "US",
+ "addressResidentialIndicator": "unknown",
+ },
+ "warehouseId": None,
+ "returnTo": {
+ "name": "Medals of America",
+ "phone": "800-308-0849",
+ "companyName": None,
+ "addressLine1": "114 Southchase Blvd",
+ "addressLine2": None,
+ "addressLine3": None,
+ "cityLocality": "Fountain Inn",
+ "stateProvince": "SC",
+ "postalCode": "29644",
+ "countryCode": "US",
+ "addressResidentialIndicator": "unknown",
+ },
+ "confirmation": "none",
+ "customs": {
+ "contents": "merchandise",
+ "nonDelivery": "return_to_sender",
+ "customsItems": [],
+ },
+ "advancedOptions": {
+ "billToAccount": None,
+ "billToCountryCode": None,
+ "billToParty": None,
+ "billToPostalCode": None,
+ "containsAlcohol": None,
+ "deliveryDutyPaid": None,
+ "dryIce": None,
+ "dryIceWeight": None,
+ "nonMachinable": None,
+ "saturdayDelivery": None,
+ "useUPSGroundFreightPricing": None,
+ "freightClass": None,
+ "customField1": None,
+ "customField2": None,
+ "customField3": None,
+ "originType": None,
+ "shipperRelease": None,
+ "collectOnDelivery": None,
+ },
+ "originType": None,
+ "insuranceProvider": "none",
+ "tags": [],
+ "orderSourceCode": None,
+ "packages": [
+ {
+ "packageCode": "package",
+ "weight": {"value": 2.9, "unit": "ounce"},
+ "dimensions": {
+ "unit": "inch",
+ "length": 0,
+ "width": 0,
+ "height": 0,
+ },
+ "insuredValue": {"currency": "usd", "amount": 0},
+ "trackingNumber": None,
+ "labelMessages": {
+ "reference1": "4051492",
+ "reference2": None,
+ "reference3": None,
+ },
+ "externalPackageId": None,
+ }
+ ],
+ "totalWeight": {"value": 2.9, "unit": "ounce"},
+ "rateResponse": {
+ "rates": [
+ {
+ "rateId": "se-784001113",
+ "rateType": "shipment",
+ "carrierId": "se-161650",
+ "shippingAmount": {"currency": "usd", "amount": 3.12},
+ "insuranceAmount": {"currency": "usd", "amount": 0},
+ "confirmationAmount": {"currency": "usd", "amount": 0},
+ "otherAmount": {"currency": "usd", "amount": 0},
+ "taxAmount": None,
+ "zone": 5,
+ "packageType": "package",
+ "deliveryDays": 3,
+ "guaranteedService": False,
+ "estimatedDeliveryDate": "2021-07-31T00:00:00Z",
+ "carrierDeliveryDays": "3",
+ "shipDate": "2021-07-28T00:00:00Z",
+ "negotiatedRate": False,
+ "serviceType": "USPS First Class Mail",
+ "serviceCode": "usps_first_class_mail",
+ "trackable": True,
+ "carrierCode": "usps",
+ "carrierNickname": "USPS",
+ "carrierFriendlyName": "USPS",
+ "validationStatus": "valid",
+ "warningMessages": [],
+ "errorMessages": [],
+ }
+ ],
+ "invalidRates": [],
+ "rateRequestId": "se-85117731",
+ "shipmentId": "se-141694059",
+ "createdAt": "2021-07-28T16:56:40.6148892Z",
+ "status": "completed",
+ "errors": [],
+ },
+ }
+ ),
+ "status": 200,
+ "content_type": "application/json",
+ }
+ )
+
+ shipengine = stub_shipengine_instance()
+ result = shipengine.get_rates_from_shipment(
+ shipment={
+ "rate_options": {
+ "carrier_ids": ["se-161650"],
+ "service_codes": ["usps_first_class_mail"],
+ "package_types": ["package"],
+ },
+ "shipment": {
+ "service_code": "",
+ "ship_to": {
+ "name": "James Atkinson",
+ "phone": None,
+ "address_line1": "28793 Fox Fire Lane",
+ "city_locality": "Shell Knob",
+ "state_province": "MO",
+ "postal_code": "65747",
+ "country_code": "US",
+ "address_residential_indicator": "yes",
+ },
+ "ship_from": {
+ "name": "Medals of America",
+ "phone": "800-308-0849",
+ "company_name": None,
+ "address_line1": "114 Southchase Blvd",
+ "address_line2": "",
+ "city_locality": "Fountain Inn",
+ "state_province": "SC",
+ "postal_code": "29644",
+ "country_code": "US",
+ "address_residential_indicator": "no",
+ },
+ "packages": [
+ {
+ "weight": {"value": 2.9, "unit": "ounce"},
+ "label_messages": {"reference1": "4051492"},
+ }
+ ],
+ },
+ }
+ )
+ self.assertEqual(result["rateResponse"]["rates"][0]["rateId"], "se-784001113")
diff --git a/tests/services/test_list_carriers.py b/tests/services/test_list_carriers.py
new file mode 100644
index 0000000..cd3bd35
--- /dev/null
+++ b/tests/services/test_list_carriers.py
@@ -0,0 +1,857 @@
+"""Testing the list_carriers functionality in the ShipEngine SDK."""
+import json
+import unittest
+import urllib.parse as urlparse
+
+import responses
+
+from shipengine.enums import BaseURL, Endpoints
+from tests.util import stub_shipengine_instance
+
+
+class TestListCarriers(unittest.TestCase):
+ def test_something(self):
+ self.assertEqual(False, False)
+
+ @responses.activate
+ def test_list_carriers(self) -> None:
+ """
+ Tests that the list_carriers method properly interacts with the ShipEngine API. It ensures
+ that this method returns a successful response from ShipEngine API containing all carriers
+ associated with a given ShipEngine account.
+ """
+ responses.add(
+ **{
+ "method": responses.GET,
+ "url": urlparse.urljoin(
+ BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.LIST_CARRIERS.value
+ ),
+ "body": json.dumps(
+ {
+ "carriers": [
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "account_number": "test_account_656171",
+ "requires_funded_amount": True,
+ "balance": 8452.04,
+ "nickname": "ShipEngine Test Account - Stamps.com",
+ "friendly_name": "Stamps.com",
+ "primary": False,
+ "has_multi_package_supporting_services": False,
+ "supports_label_messages": True,
+ "services": [
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_first_class_mail",
+ "name": "USPS First Class Mail",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": False,
+ },
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_media_mail",
+ "name": "USPS Media Mail",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": False,
+ },
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_parcel_select",
+ "name": "USPS Parcel Select Ground",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": False,
+ },
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_priority_mail",
+ "name": "USPS Priority Mail",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": False,
+ },
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_priority_mail_express",
+ "name": "USPS Priority Mail Express",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": False,
+ },
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_first_class_mail_international",
+ "name": "USPS First Class Mail Intl",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": False,
+ },
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_priority_mail_international",
+ "name": "USPS Priority Mail Intl",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": False,
+ },
+ {
+ "carrier_id": "se-656171",
+ "carrier_code": "stamps_com",
+ "service_code": "usps_priority_mail_express_international",
+ "name": "USPS Priority Mail Express Intl",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": False,
+ },
+ ],
+ "packages": [
+ {
+ "package_id": "None",
+ "package_code": "cubic",
+ "name": "Cubic",
+ "description": "Cubic",
+ },
+ {
+ "package_id": "None",
+ "package_code": "flat_rate_envelope",
+ "name": "Flat Rate Envelope",
+ "description": 'USPS flat rate envelope. A special cardboard envelope provided by the USPS that clearly indicates "Flat Rate".', # noqa
+ },
+ {
+ "package_id": "None",
+ "package_code": "flat_rate_legal_envelope",
+ "name": "Flat Rate Legal Envelope",
+ "description": "Flat Rate Legal Envelope",
+ },
+ {
+ "package_id": "None",
+ "package_code": "flat_rate_padded_envelope",
+ "name": "Flat Rate Padded Envelope",
+ "description": "Flat Rate Padded Envelope",
+ },
+ {
+ "package_id": "None",
+ "package_code": "large_envelope_or_flat",
+ "name": "Large Envelope or Flat",
+ "description": 'Large envelope or flat. Has one dimension that is between 11 1/2" and 15" long, 6 1/18" and 12" high, or 1/4" and 3/4" thick.', # noqa
+ },
+ {
+ "package_id": "None",
+ "package_code": "large_flat_rate_box",
+ "name": "Large Flat Rate Box",
+ "description": "Large Flat Rate Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "large_package",
+ "name": 'Large Package (any side \u003e 12")',
+ "description": 'Large package. Longest side plus the distance around the thickest part is over 84" and less than or equal to 108".', # noqa
+ },
+ {
+ "package_id": "None",
+ "package_code": "letter",
+ "name": "Letter",
+ "description": "Letter",
+ },
+ {
+ "package_id": "None",
+ "package_code": "medium_flat_rate_box",
+ "name": "Medium Flat Rate Box",
+ "description": 'USPS flat rate box. A special 11" x 8 1/2" x 5 1/2" or 14" x 3.5" x 12" USPS box that clearly indicates "Flat Rate Box"', # noqa
+ },
+ {
+ "package_id": "None",
+ "package_code": "non_rectangular",
+ "name": "Non Rectangular Package",
+ "description": "Non-Rectangular package type that is cylindrical in shape.",
+ },
+ {
+ "package_id": "None",
+ "package_code": "package",
+ "name": "Package",
+ "description": 'Package. Longest side plus the distance around the thickest part is less than or equal to 84"', # noqa
+ },
+ {
+ "package_id": "None",
+ "package_code": "regional_rate_box_a",
+ "name": "Regional Rate Box A",
+ "description": "Regional Rate Box A",
+ },
+ {
+ "package_id": "None",
+ "package_code": "regional_rate_box_b",
+ "name": "Regional Rate Box B",
+ "description": "Regional Rate Box B",
+ },
+ {
+ "package_id": "None",
+ "package_code": "small_flat_rate_box",
+ "name": "Small Flat Rate Box",
+ "description": "Small Flat Rate Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "thick_envelope",
+ "name": "Thick Envelope",
+ "description": 'Thick envelope. Envelopes or flats greater than 3/4" at the thickest point.', # noqa
+ },
+ ],
+ "options": [
+ {
+ "name": "non_machinable",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "bill_to_account",
+ "default_value": "None",
+ "description": "Bill To Account",
+ },
+ {
+ "name": "bill_to_party",
+ "default_value": "None",
+ "description": "Bill To Party",
+ },
+ {
+ "name": "bill_to_postal_code",
+ "default_value": "None",
+ "description": "Bill To Postal Code",
+ },
+ {
+ "name": "bill_to_country_code",
+ "default_value": "None",
+ "description": "Bill To Country Code",
+ },
+ ],
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "account_number": "test_account_656172",
+ "requires_funded_amount": False,
+ "balance": 0.0,
+ "nickname": "ShipEngine Test Account - UPS",
+ "friendly_name": "UPS",
+ "primary": False,
+ "has_multi_package_supporting_services": True,
+ "supports_label_messages": True,
+ "services": [
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_standard_international",
+ "name": "UPS Standard®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_next_day_air_early_am",
+ "name": "UPS Next Day Air® Early",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_worldwide_express",
+ "name": "UPS Worldwide Express®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_next_day_air",
+ "name": "UPS Next Day Air®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_ground_international",
+ "name": "UPS Ground® (International)",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_worldwide_express_plus",
+ "name": "UPS Worldwide Express Plus®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_next_day_air_saver",
+ "name": "UPS Next Day Air Saver®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_worldwide_expedited",
+ "name": "UPS Worldwide Expedited®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_2nd_day_air_am",
+ "name": "UPS 2nd Day Air AM®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_2nd_day_air",
+ "name": "UPS 2nd Day Air®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_worldwide_saver",
+ "name": "UPS Worldwide Saver®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_2nd_day_air_international",
+ "name": "UPS 2nd Day Air® (International)",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_3_day_select",
+ "name": "UPS 3 Day Select®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_ground",
+ "name": "UPS® Ground",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656172",
+ "carrier_code": "ups",
+ "service_code": "ups_next_day_air_international",
+ "name": "UPS Next Day Air® (International)",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ ],
+ "packages": [
+ {
+ "package_id": "None",
+ "package_code": "package",
+ "name": "Package",
+ "description": 'Package. Longest side plus the distance around the thickest part is less than or equal to 84"', # noqa
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups__express_box_large",
+ "name": "UPS Express® Box - Large",
+ "description": "Express Box - Large",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_10_kg_box",
+ "name": "UPS 10 KG Box®",
+ "description": "10 KG Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_25_kg_box",
+ "name": "UPS 25 KG Box®",
+ "description": "25 KG Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_express_box",
+ "name": "UPS Express® Box",
+ "description": "Express Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_express_box_medium",
+ "name": "UPS Express® Box - Medium",
+ "description": "Express Box - Medium",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_express_box_small",
+ "name": "UPS Express® Box - Small",
+ "description": "Express Box - Small",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_express_pak",
+ "name": "UPS Express® Pak",
+ "description": "Pak",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_letter",
+ "name": "UPS Letter",
+ "description": "Letter",
+ },
+ {
+ "package_id": "None",
+ "package_code": "ups_tube",
+ "name": "UPS Tube",
+ "description": "Tube",
+ },
+ ],
+ "options": [
+ {
+ "name": "bill_to_account",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "bill_to_country_code",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "bill_to_party",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "bill_to_postal_code",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "collect_on_delivery",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "contains_alcohol",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "delivered_duty_paid",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "dry_ice",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "dry_ice_weight",
+ "default_value": "0",
+ "description": "",
+ },
+ {
+ "name": "freight_class",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "non_machinable",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "saturday_delivery",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "shipper_release",
+ "default_value": "False",
+ "description": "Driver may release package without signature",
+ },
+ ],
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "account_number": "test_account_656173",
+ "requires_funded_amount": False,
+ "balance": 0.0,
+ "nickname": "ShipEngine Test Account - FedEx",
+ "friendly_name": "FedEx",
+ "primary": False,
+ "has_multi_package_supporting_services": True,
+ "supports_label_messages": True,
+ "services": [
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_ground",
+ "name": "FedEx Ground®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_home_delivery",
+ "name": "FedEx Home Delivery®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_2day",
+ "name": "FedEx 2Day®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_2day_am",
+ "name": "FedEx 2Day® A.M.",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_express_saver",
+ "name": "FedEx Express Saver®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_standard_overnight",
+ "name": "FedEx Standard Overnight®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_priority_overnight",
+ "name": "FedEx Priority Overnight®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_first_overnight",
+ "name": "FedEx First Overnight®",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_1_day_freight",
+ "name": "FedEx 1Day® Freight",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_2_day_freight",
+ "name": "FedEx 2Day® Freight",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_3_day_freight",
+ "name": "FedEx 3Day® Freight",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_first_overnight_freight",
+ "name": "FedEx First Overnight® Freight",
+ "domestic": True,
+ "international": False,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_ground_international",
+ "name": "FedEx International Ground®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_international_economy",
+ "name": "FedEx International Economy®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_international_priority",
+ "name": "FedEx International Priority®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_international_first",
+ "name": "FedEx International First®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_international_economy_freight",
+ "name": "FedEx International Economy® Freight",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_international_priority_freight",
+ "name": "FedEx International Priority® Freight",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": True,
+ },
+ {
+ "carrier_id": "se-656173",
+ "carrier_code": "fedex",
+ "service_code": "fedex_international_connect_plus",
+ "name": "FedEx International Connect Plus®",
+ "domestic": False,
+ "international": True,
+ "is_multi_package_supported": False,
+ },
+ ],
+ "packages": [
+ {
+ "package_id": "None",
+ "package_code": "fedex_envelope_onerate",
+ "name": "FedEx One Rate® Envelope",
+ "description": "FedEx® Envelope",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_extra_large_box_onerate",
+ "name": "FedEx One Rate® Extra Large Box",
+ "description": "FedEx® Extra Large Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_large_box_onerate",
+ "name": "FedEx One Rate® Large Box",
+ "description": "FedEx® Large Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_medium_box_onerate",
+ "name": "FedEx One Rate® Medium Box",
+ "description": "FedEx® Medium Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_pak_onerate",
+ "name": "FedEx One Rate® Pak",
+ "description": "FedEx® Pak",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_small_box_onerate",
+ "name": "FedEx One Rate® Small Box",
+ "description": "FedEx® Small Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_tube_onerate",
+ "name": "FedEx One Rate® Tube",
+ "description": "FedEx® Tube",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_10kg_box",
+ "name": "FedEx® 10kg Box",
+ "description": "FedEx® 10kg Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_25kg_box",
+ "name": "FedEx® 25kg Box",
+ "description": "FedEx® 25kg Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_box",
+ "name": "FedEx® Box",
+ "description": "FedEx® Box",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_envelope",
+ "name": "FedEx® Envelope",
+ "description": "FedEx® Envelope",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_pak",
+ "name": "FedEx® Pak",
+ "description": "FedEx® Pak",
+ },
+ {
+ "package_id": "None",
+ "package_code": "fedex_tube",
+ "name": "FedEx® Tube",
+ "description": "FedEx® Tube",
+ },
+ {
+ "package_id": "None",
+ "package_code": "package",
+ "name": "Package",
+ "description": 'Package. Longest side plus the distance around the thickest part is less than or equal to 84"', # noqa
+ },
+ ],
+ "options": [
+ {
+ "name": "bill_to_account",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "bill_to_country_code",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "bill_to_party",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "bill_to_postal_code",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "collect_on_delivery",
+ "default_value": "",
+ "description": "",
+ },
+ {
+ "name": "contains_alcohol",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "delivered_duty_paid",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "dry_ice",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "dry_ice_weight",
+ "default_value": "0",
+ "description": "",
+ },
+ {
+ "name": "non_machinable",
+ "default_value": "False",
+ "description": "",
+ },
+ {
+ "name": "saturday_delivery",
+ "default_value": "False",
+ "description": "",
+ },
+ ],
+ },
+ ],
+ "request_id": "6420e68b-b724-4d55-8180-a1828a3ca054",
+ "errors": [],
+ }
+ ),
+ "status": 200,
+ "content_type": "application/json",
+ }
+ )
+
+ shipengine = stub_shipengine_instance()
+ result = shipengine.list_carriers()
+ self.assertEqual(type(result["carriers"]), list)
+ self.assertGreater(len(result["carriers"]), 0)
+ self.assertEqual(len(result["carriers"]), 3)
diff --git a/tests/services/test_normalize_address.py b/tests/services/test_normalize_address.py
deleted file mode 100644
index 7c2454a..0000000
--- a/tests/services/test_normalize_address.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""Test the normalize address method of the ShipEngine SDK."""
-import re
-
-from shipengine_sdk.errors import ClientSystemError, ShipEngineError, ValidationError
-from shipengine_sdk.models import Address, ErrorCode, ErrorSource, ErrorType
-
-from ..util.test_helpers import (
- address_missing_required_fields,
- address_with_errors,
- address_with_single_error,
- address_with_warnings,
- get_server_side_error,
- multi_line_address,
- non_latin_address,
- normalize_an_address,
- unknown_address,
- valid_address_assertions,
- valid_canadian_address,
- valid_commercial_address,
- valid_residential_address,
-)
-
-
-class TestNormalizeAddress:
- TEST_METHOD: str = "normalize"
-
- def test_normalize_valid_residential_address(self) -> None:
- """DX-1041 - Normalize valid residential address."""
- residential_address = valid_residential_address()
- normalized = normalize_an_address(residential_address)
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=residential_address,
- returned_address=normalized,
- expected_residential_indicator=True,
- )
-
- def test_normalize_valid_commercial_address(self) -> None:
- """DX-1042 - Normalize valid commercial address."""
- commercial_address = valid_commercial_address()
- normalized = normalize_an_address(commercial_address)
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=commercial_address,
- returned_address=normalized,
- expected_residential_indicator=False,
- )
-
- def test_normalize_unknown_address(self) -> None:
- """DX-1043 - Normalize unknown address."""
- address = unknown_address()
- normalized = normalize_an_address(address)
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="international",
- original_address=address,
- returned_address=normalized,
- expected_residential_indicator=None,
- )
-
- def test_normalize_multi_line_address(self) -> None:
- """DX-1044 - Normalize multi-line address."""
- multi_line = multi_line_address()
- normalized = normalize_an_address(multi_line)
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=multi_line,
- returned_address=normalized,
- expected_residential_indicator=False,
- )
- assert (
- normalized.street[0]
- == (multi_line.street[0] + " " + multi_line.street[1]).replace(".", "").upper()
- )
- assert normalized.street[1] == multi_line.street[2].upper()
-
- def test_normalize_numeric_postal_code(self) -> None:
- """DX-1045 - Normalize address with numeric postal code."""
- address = valid_residential_address()
- normalized = normalize_an_address(address)
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="domestic",
- original_address=address,
- returned_address=normalized,
- expected_residential_indicator=True,
- )
- assert re.match(r"\d", normalized.postal_code)
-
- def test_normalize_alpha_postal_code(self) -> None:
- """DX-1046 - Normalize address with alpha-numeric postal code."""
- address = valid_canadian_address()
- normalized = normalize_an_address(address)
-
- valid_address_assertions(
- test_method=self.TEST_METHOD,
- locale="international",
- original_address=address,
- returned_address=normalized,
- expected_residential_indicator=False,
- )
-
- def test_normalize_non_latin_chars(self) -> None:
- """DX-1047 - Normalize address with non-latin characters."""
- non_latin = non_latin_address()
- normalized = normalize_an_address(non_latin)
-
- assert type(normalized) is Address
- assert normalized.street[0] == "68 Kamitobatsunodacho"
- assert normalized.city_locality == "Kyoto-Shi Minami-Ku"
- assert normalized.state_province == "Kyoto"
- assert normalized.postal_code == non_latin.postal_code
- assert normalized.country_code == non_latin.country_code
- assert normalized.is_residential is False
- assert len(normalized.street) == 1
-
- def test_normalize_with_warnings(self) -> None:
- """DX-1048 - Normalize address with warnings."""
- warning_address = address_with_warnings()
- normalized = normalize_an_address(warning_address)
-
- assert type(normalized) is Address
- assert normalized is not None
- assert normalized.city_locality == warning_address.city_locality
- assert normalized.state_province == warning_address.state_province.title()
- assert normalized.postal_code == "M6K 3C3"
- assert normalized.country_code == warning_address.country_code.upper()
- assert normalized.is_residential is True
-
- def test_normalize_with_single_error_message(self) -> None:
- """DX-1049 - Normalize address with single error message."""
- single_error = address_with_single_error()
- try:
- normalize_an_address(single_error)
- except ShipEngineError as err:
- assert err.request_id is not None
- assert err.request_id.startswith("req_") is True
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.ERROR.value
- assert err.error_code == ErrorCode.MINIMUM_POSTAL_CODE_VERIFICATION_FAILED.value
- assert err.message == "Invalid address. Insufficient or inaccurate postal code"
-
- def test_normalize_with_multiple_errors(self) -> None:
- """DX-1050 - Normalize address with multiple error messages."""
- errors_address = address_with_errors()
- try:
- normalize_an_address(errors_address)
- except ShipEngineError as err:
- assert err.request_id is not None
- assert err.request_id.startswith("req_") is True
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.ERROR.value
- assert err.error_code is ErrorCode.INVALID_ADDRESS.value
- assert (
- err.message
- == "Invalid address.\nInvalid City, State, or Zip\nInsufficient or Incorrect Address Data"
- )
-
- def test_normalize_missing_city_state_and_postal_code(self) -> None:
- """DX-1053 & DX-1054 - Missing city, state, and postal code."""
- try:
- address_missing_required_fields()
- except ValidationError as err:
- assert err.request_id is None
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.VALIDATION.value
- assert err.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value
- assert (
- err.message
- == "Invalid address. Either the postal code or the city/locality and state/province must be specified." # noqa
- )
-
- def test_normalize_server_side_error(self) -> None:
- """DX-1055 - Server-side error."""
- try:
- get_server_side_error()
- except ClientSystemError as err:
- assert err.request_id is not None
- assert err.request_id.startswith("req_") is True
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.SYSTEM.value
- assert err.error_code is ErrorCode.UNSPECIFIED.value
diff --git a/tests/services/test_track_package.py b/tests/services/test_track_package.py
deleted file mode 100644
index 49fd566..0000000
--- a/tests/services/test_track_package.py
+++ /dev/null
@@ -1,318 +0,0 @@
-"""Testing the `track_package` method of the ShipEngine SDK."""
-from typing import List
-
-from shipengine_sdk.errors import ClientSystemError, ShipEngineError, ValidationError
-from shipengine_sdk.models import (
- ErrorCode,
- ErrorSource,
- ErrorType,
- TrackingEvent,
- TrackingQuery,
- TrackPackageResult,
-)
-
-from ..util import configurable_stub_shipengine_instance, stub_config
-
-
-def assertions_on_delivered_after_exception_or_multiple_attempts(
- tracking_result: TrackPackageResult,
-) -> None:
- track_package_assertions(tracking_result=tracking_result)
- does_delivery_date_match(tracking_result)
- assert_events_in_order(tracking_result.events)
- assert len(tracking_result.events) == 8
- 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 == "unknown"
- assert tracking_result.events[4].status == "exception"
- 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"
-
-
-def does_delivery_date_match(tracking_result: TrackPackageResult) -> None:
- """Check that the delivery dates for a given tracking response match."""
- assert (
- tracking_result.shipment.actual_delivery_date.to_datetime_object()
- == tracking_result.events[-1].date_time.to_datetime_object()
- )
-
-
-def assert_events_in_order(events: List) -> None:
- """
- Checks that the order of events is correct in that they should be ordered with
- the newest event at the bottom of the list.
- """
- previous_date_time = events[0].date_time
- for event in events:
- assert event.date_time.to_datetime_object() >= previous_date_time.to_datetime_object()
- previous_date_time = event.date_time
-
-
-def track_package_assertions(tracking_result: TrackPackageResult) -> None:
- """Common `track_package` assertions."""
- carrier_account_carrier_code = tracking_result.shipment.carrier_account.carrier["code"]
- carrier_code = tracking_result.shipment.carrier["code"]
- estimated_delivery = tracking_result.shipment.estimated_delivery_date
-
- assert carrier_account_carrier_code is not None
- assert type(carrier_account_carrier_code) is str
- assert carrier_code is not None
- assert type(carrier_code) is str
- assert estimated_delivery.has_timezone() is True
-
-
-def date_time_assertions(event: TrackingEvent) -> None:
- """Check that date_time has a timezone."""
- assert event.date_time is not None
- assert event.carrier_date_time is not None
- assert event.date_time.has_timezone() is True
- assert event.carrier_date_time.has_timezone() is False
-
-
-class TestTrackPackage:
- _PACKAGE_ID_FEDEX_ACCEPTED: str = "pkg_1FedExAccepted"
- _PACKAGE_ID_FEDEX_DELIVERED: str = "pkg_1FedExDeLivered"
- _PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION: str = "pkg_1FedexDeLiveredException"
-
- def test_track_by_tracking_number_and_carrier_code(self) -> None:
- """DX-1084 - Test track by tracking number and carrier code."""
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_data = TrackingQuery(carrier_code="fedex", tracking_number="abcFedExDelivered")
- tracking_result = shipengine.track_package(tracking_data=tracking_data)
-
- assert tracking_data.carrier_code == tracking_result.shipment.carrier["code"]
- assert tracking_data.tracking_number == tracking_result.package.tracking_number
- assert tracking_result.package.tracking_url is not None
- assert type(tracking_result.package.tracking_url) is str
-
- def test_track_by_package_id(self) -> None:
- """DX-1086 - Test track by package ID."""
- package_id = self._PACKAGE_ID_FEDEX_ACCEPTED
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- assert tracking_result.package.package_id == package_id
- assert tracking_result.package.tracking_number is not None
- assert tracking_result.package.tracking_url is not None
- assert tracking_result.shipment.shipment_id is not None
- assert tracking_result.shipment.account_id is not None
-
- def test_initial_scan_tracking_event(self) -> None:
- """DX-1088 - Test initial scan tracking event."""
- package_id = self._PACKAGE_ID_FEDEX_ACCEPTED
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- assert len(tracking_result.events) == 1
- assert tracking_result.events[0].status == "accepted"
-
- def test_out_for_delivery_tracking_event(self) -> None:
- """DX-1089 - Test out for delivery tracking event."""
- package_id = "pkg_1FedExAttempted"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- assert len(tracking_result.events) == 5
- assert tracking_result.events[0].status == "accepted"
- assert tracking_result.events[1].status == "in_transit"
-
- def test_multiple_delivery_attempts(self) -> None:
- """DX-1090 - Test multiple delivery attempt events."""
- package_id = "pkg_1FedexDeLiveredAttempted"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- 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 == "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."""
- package_id = self._PACKAGE_ID_FEDEX_DELIVERED
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- assert (
- tracking_result.shipment.actual_delivery_date.to_datetime_object()
- == tracking_result.events[4].date_time.to_datetime_object()
- )
- does_delivery_date_match(tracking_result)
- assert_events_in_order(tracking_result.events)
- assert len(tracking_result.events) == 5
- assert tracking_result.events[0].status == "accepted"
- assert tracking_result.events[1].status == "in_transit"
- assert tracking_result.events[4].status == "delivered"
- assert tracking_result.events[-1].status == "delivered"
-
- def test_delivered_with_signature(self) -> None:
- """DX-1092 - Test track delivered with signature event."""
- package_id = self._PACKAGE_ID_FEDEX_DELIVERED
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- does_delivery_date_match(tracking_result)
- assert_events_in_order(tracking_result.events)
- assert len(tracking_result.events) == 5
- assert tracking_result.events[0].status == "accepted"
- assert tracking_result.events[1].status == "in_transit"
- assert tracking_result.events[3].status == "in_transit"
- assert tracking_result.events[4].status == "delivered"
- assert tracking_result.events[-1].status == "delivered"
- assert tracking_result.events[-1].signer is not None
- assert type(tracking_result.events[-1].signer) is str
-
- def test_delivered_after_multiple_attempts(self) -> None:
- """DX-1093 - Test delivered after multiple attempts tracking event."""
- package_id = self._PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
- assertions_on_delivered_after_exception_or_multiple_attempts(tracking_result)
-
- def test_delivered_after_exception(self) -> None:
- """DX-1094 - Test delivered after exception tracking event."""
- package_id = self._PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
- assertions_on_delivered_after_exception_or_multiple_attempts(tracking_result)
-
- def test_single_exception_tracking_event(self) -> None:
- """DX-1095 - Test single exception tracking event."""
- package_id = "pkg_1FedexException"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- assert_events_in_order(tracking_result.events)
- assert len(tracking_result.events) == 3
- assert tracking_result.events[0].status == "accepted"
- assert tracking_result.events[1].status == "in_transit"
- assert tracking_result.events[2].status == "exception"
-
- def test_track_with_multiple_exceptions(self) -> None:
- """DX-1096 - Test track with multiple exceptions."""
- package_id = self._PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- assert_events_in_order(tracking_result.events)
- 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 == "exception"
- assert tracking_result.events[7].status == "delivered"
- assert tracking_result.events[-1].status == "delivered"
-
- def test_multiple_locations_in_tracking_event(self) -> None:
- """DX-1097 - Test track package with multiple locations in tracking event."""
- package_id = "pkg_Attempted"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- assert_events_in_order(tracking_result.events)
- assert tracking_result.events[0].location is None
- assert tracking_result.events[1].location.latitude is 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
-
- def test_carrier_date_time_without_timezone(self) -> None:
- """DX-1098 - Test track package where carrierDateTime has no timezone."""
- package_id = "pkg_Attempted"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- tracking_result = shipengine.track_package(tracking_data=package_id)
-
- track_package_assertions(tracking_result=tracking_result)
- assert_events_in_order(tracking_result.events)
- assert len(tracking_result.events) == 5
- for event in tracking_result.events:
- date_time_assertions(event=event)
-
- def test_invalid_tracking_number(self) -> None:
- """DX-1099 - Test track package with an invalid tracking number."""
- tracking_data = TrackingQuery(carrier_code="fedex", tracking_number="abc123")
- shipengine = configurable_stub_shipengine_instance(stub_config())
- try:
- shipengine.track_package(tracking_data=tracking_data)
- except ShipEngineError as err:
- assert type(err) is ClientSystemError
- assert err.request_id is not None
- assert err.request_id.startswith("req_")
- assert err.source == ErrorSource.CARRIER.value
- assert err.error_type == ErrorType.BUSINESS_RULES.value
- assert err.error_code == ErrorCode.INVALID_IDENTIFIER.value
- assert (
- err.message
- == f"{tracking_data.tracking_number} is not a valid fedex tracking number."
- )
-
- def test_invalid_package_id_prefix(self) -> None:
- """DX-1100 - Test track package with invalid package_id prefix."""
- package_id = "car_1FedExAccepted"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- try:
- shipengine.track_package(tracking_data=package_id)
- except ShipEngineError as err:
- assert type(err) is ValidationError
- assert err.request_id is None
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.VALIDATION.value
- assert err.error_code is ErrorCode.INVALID_IDENTIFIER.value
- assert err.message == f"[{package_id[0:4]}] is not a valid package ID prefix."
-
- def test_invalid_package_id(self) -> None:
- """DX-1101 - Test track package with invalid package_id."""
- package_id = "pkg_12!@3a s567"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- try:
- shipengine.track_package(tracking_data=package_id)
- except ShipEngineError as err:
- assert type(err) is ValidationError
- assert err.request_id is None
- assert err.source is ErrorSource.SHIPENGINE.value
- assert err.error_type is ErrorType.VALIDATION.value
- assert err.error_code is ErrorCode.INVALID_IDENTIFIER.value
- assert err.message == f"[{package_id}] is not a valid package ID."
-
- def test_package_id_not_found(self) -> None:
- """DX-1102 - Test track package where package ID cannot be found."""
- package_id = "pkg_123"
- shipengine = configurable_stub_shipengine_instance(stub_config())
- try:
- shipengine.track_package(tracking_data=package_id)
- except ShipEngineError as err:
- assert type(err) is ClientSystemError
- assert err.request_id is not None
- assert err.request_id.startswith("req_")
- assert err.source == ErrorSource.SHIPENGINE.value
- assert err.error_type == ErrorType.VALIDATION.value
- assert err.error_code == ErrorCode.INVALID_IDENTIFIER.value
- assert err.message == f"Package ID {package_id} does not exist."
-
- def test_server_side_error(self) -> None:
- """DX-1103 - Test track package server-side error."""
- tracking_data = TrackingQuery(carrier_code="fedex", tracking_number="500 Server Error")
- shipengine = configurable_stub_shipengine_instance(stub_config())
- try:
- shipengine.track_package(tracking_data=tracking_data)
- except ShipEngineError as err:
- assert type(err) is ClientSystemError
- assert err.request_id is not None
- assert err.request_id.startswith("req_")
- assert err.source == ErrorSource.SHIPENGINE.value
- assert err.error_type == ErrorType.SYSTEM.value
- assert err.error_code == ErrorCode.UNSPECIFIED.value
- assert err.message == "Unable to process this request. A downstream API error occurred."
diff --git a/tests/services/test_validate_addresses.py b/tests/services/test_validate_addresses.py
new file mode 100644
index 0000000..2483700
--- /dev/null
+++ b/tests/services/test_validate_addresses.py
@@ -0,0 +1,67 @@
+"""Testing the validate_addresses functionality in the ShipEngine SDK."""
+import json
+import unittest
+import urllib.parse as urlparse
+
+import responses
+
+from shipengine.enums import BaseURL, Endpoints
+
+from ..util import stub_shipengine_instance, valid_commercial_address
+
+
+class TestValidateAddresses(unittest.TestCase):
+ @responses.activate
+ def test_validate_addresses(self) -> None:
+ """
+ Tests that the validate_addresses method properly interacts with ShipEngine API,
+ and returns a successful response from ShipEngine API.
+ """
+ responses.add(
+ **{
+ "method": responses.POST,
+ "url": urlparse.urljoin(
+ BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.ADDRESSES_VALIDATE.value
+ ),
+ "body": json.dumps(
+ [
+ {
+ "status": "verified",
+ "original_address": {
+ "name": "ShipEngine",
+ "phone": "1-123-123-1234",
+ "company_name": "None",
+ "address_line1": "3800 N Lamar Blvd",
+ "address_line2": "ste 220",
+ "address_line3": "None",
+ "city_locality": "Austin",
+ "state_province": "TX",
+ "postal_code": "78756",
+ "country_code": "US",
+ "address_residential_indicator": "unknown",
+ },
+ "matched_address": {
+ "name": "SHIPENGINE",
+ "phone": "1-123-123-1234",
+ "company_name": "None",
+ "address_line1": "3800 N LAMAR BLVD STE 220",
+ "address_line2": "",
+ "address_line3": "None",
+ "city_locality": "AUSTIN",
+ "state_province": "TX",
+ "postal_code": "78756-0003",
+ "country_code": "US",
+ "address_residential_indicator": "no",
+ },
+ "messages": [],
+ }
+ ]
+ ),
+ "status": 200,
+ "content_type": "application/json",
+ }
+ )
+
+ shipengine = stub_shipengine_instance()
+ result = shipengine.validate_addresses(valid_commercial_address())
+ self.assertEqual(result[0]["status"], "verified")
diff --git a/tests/test___init__.py b/tests/test___init__.py
deleted file mode 100644
index b2f5f60..0000000
--- a/tests/test___init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""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"
diff --git a/tests/test_shipengine.py b/tests/test_shipengine.py
index 67f6b2a..462d267 100644
--- a/tests/test_shipengine.py
+++ b/tests/test_shipengine.py
@@ -1,31 +1,20 @@
"""Testing the ShipEngine object."""
import pytest
-from shipengine_sdk import ShipEngine, __version__
-from shipengine_sdk.errors import ValidationError
-from shipengine_sdk.util.sdk_assertions import api_key_validation_error_assertions
+from shipengine import ShipEngine
+from shipengine.errors import ValidationError
+from shipengine.util import api_key_validation_error_assertions
-def shipengine_no_api_key() -> ShipEngine:
- """Return an error from no API Key."""
- return ShipEngine(dict(retries=2))
+def shipengine_empty_api_key():
+ return ShipEngine("")
-def shipengine_empty_api_key() -> ShipEngine:
- """Return an error from empty API Key."""
- return ShipEngine(config="")
-
-
-def shipengine_whitespace_in_api_key() -> ShipEngine:
- """Return an error from whitespace in API Key."""
- return ShipEngine(config=" ")
+def shipengine_no_api_key():
+ return ShipEngine({"retries": 3})
class TestShipEngine:
- def test_version(self) -> None:
- """Test the package version of the ShipEngine SDK."""
- assert __version__ == "0.0.1"
-
def test_no_api_key_provided(self) -> None:
"""DX-1440 - No API Key at instantiation."""
try:
diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py
index 81b392f..7d784b2 100644
--- a/tests/test_shipengine_config.py
+++ b/tests/test_shipengine_config.py
@@ -1,12 +1,11 @@
"""Testing the ShipEngineConfig object."""
import pytest
-from shipengine_sdk import ShipEngine, ShipEngineConfig
-from shipengine_sdk.errors import InvalidFieldValueError, ValidationError
-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
+from shipengine import ShipEngine, ShipEngineConfig
+from shipengine.enums import BaseURL, Constants
+from shipengine.errors import InvalidFieldValueError, ValidationError
+from shipengine.util import api_key_validation_error_assertions
+from shipengine.util.sdk_assertions import timeout_validation_error_assertions
def stub_config() -> dict:
@@ -17,20 +16,6 @@ def stub_config() -> dict:
return dict(api_key="baz_sim", 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:
"""Return an error from no API Key."""
return ShipEngineConfig(dict(retries=2))
@@ -81,7 +66,7 @@ def complete_valid_config() -> ShipEngineConfig:
"""
return ShipEngineConfig(
dict(
- api_key="baz_sim",
+ api_key=Constants.STUB_API_KEY.value,
page_size=50,
retries=2,
timeout=10,
@@ -89,6 +74,23 @@ def complete_valid_config() -> ShipEngineConfig:
)
+def valid_commercial_address():
+ return [
+ {
+ "name": "ShipEngine",
+ "company": "Auctane",
+ "phone": "1-123-123-1234",
+ "address_line1": "3800 N Lamar Blvd",
+ "address_line2": "ste 220",
+ "city_locality": "Austin",
+ "state_province": "TX",
+ "postal_code": "78756",
+ "country_code": "US",
+ "address_residential_indicator": "unknown",
+ }
+ ]
+
+
class TestShipEngineConfig:
def test_valid_custom_config(self):
"""
@@ -96,8 +98,8 @@ def test_valid_custom_config(self):
valid values for each attribute.
"""
valid_config: ShipEngineConfig = complete_valid_config()
- assert valid_config.api_key == "baz_sim"
- assert valid_config.base_uri is Endpoints.SHIPENGINE_RPC_URL.value
+ assert valid_config.api_key.startswith("TEST_")
+ assert valid_config.base_uri is BaseURL.SHIPENGINE_RPC_URL.value
assert valid_config.page_size == 50
assert valid_config.retries == 2
assert valid_config.timeout == 10
@@ -120,14 +122,14 @@ def test_empty_api_key_provided(self) -> None:
with pytest.raises(ValidationError):
config_with_empty_api_key()
- def test_valid_retries(self):
+ def test_valid_retries(self) -> None:
"""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_sim"
assert valid_retries.retries == retries
- def test_invalid_retries_provided(self):
+ def test_invalid_retries_provided(self) -> None:
"""DX-1442 - Invalid retries at instantiation."""
retries = -3
try:
@@ -140,7 +142,7 @@ def test_invalid_retries_provided(self):
with pytest.raises(InvalidFieldValueError):
set_config_retries(retries)
- def test_invalid_timeout_provided(self):
+ def test_invalid_timeout_provided(self) -> None:
"""DX-1443 - Invalid timeout at instantiation."""
timeout = -5
try:
@@ -151,13 +153,13 @@ def test_invalid_timeout_provided(self):
e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided."
)
- def test_invalid_timeout_in_method_call(self):
+ def test_invalid_timeout_in_method_call(self) -> None:
"""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)
+ shipengine.validate_addresses(
+ address=valid_commercial_address(), config=dict(timeout=timeout)
)
except InvalidFieldValueError as e:
timeout_validation_error_assertions(e)
@@ -165,13 +167,13 @@ def test_invalid_timeout_in_method_call(self):
e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided."
)
- def test_invalid_retries_in_method_call(self):
+ def test_invalid_retries_in_method_call(self) -> None:
"""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)
+ shipengine.validate_addresses(
+ address=valid_commercial_address(), config=dict(retries=retries)
)
except InvalidFieldValueError as e:
timeout_validation_error_assertions(e)
@@ -179,13 +181,13 @@ def test_invalid_retries_in_method_call(self):
e.message == f"retries - Retries must be zero or greater. {retries} was provided."
)
- def test_invalid_api_key_in_method_call(self):
+ def test_invalid_api_key_in_method_call(self) -> None:
"""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)
+ shipengine.validate_addresses(
+ address=valid_commercial_address(), config=dict(api_key=api_key)
)
except Exception as e:
api_key_validation_error_assertions(e)
@@ -197,7 +199,7 @@ def test_config_defaults(self) -> None:
assert config.retries == 1
assert config.page_size == 50
assert config.timeout == 5
- assert config.base_uri is Endpoints.SHIPENGINE_RPC_URL.value
+ assert config.base_uri is BaseURL.SHIPENGINE_RPC_URL.value
def test_to_dict_method(self) -> None:
"""Test the to_dict convenience method."""
diff --git a/tests/test_snake_case_converter.py b/tests/test_snake_case_converter.py
new file mode 100644
index 0000000..fbd8f78
--- /dev/null
+++ b/tests/test_snake_case_converter.py
@@ -0,0 +1,9 @@
+"""Test test conversion helper function. snake_case -> camelCase."""
+from shipengine.util import snake_to_camel
+
+
+class TestSnakeToCamelCase:
+ def test_snake_to_camel(self):
+ """Test conversion of snake_case to camelCase."""
+ camel_case = snake_to_camel("python_is_awesome")
+ assert camel_case == "pythonIsAwesome"
diff --git a/tests/util/test_helpers.py b/tests/util/test_helpers.py
index 706155a..74baf81 100644
--- a/tests/util/test_helpers.py
+++ b/tests/util/test_helpers.py
@@ -1,22 +1,13 @@
"""Test data as functions and common assertion helper functions."""
-from typing import Dict, Optional, Union
+from typing import Any, Dict, List
-from shipengine_sdk import ShipEngine, ShipEngineConfig
-from shipengine_sdk.errors import ShipEngineError
-from shipengine_sdk.models import (
- Address,
- AddressValidateResult,
- ErrorCode,
- ErrorSource,
- ErrorType,
- TrackingQuery,
-)
-from shipengine_sdk.models.enums import Constants
+from shipengine import ShipEngine, ShipEngineConfig
+from shipengine.enums import Constants
def stub_config(
retries: int = 1,
-) -> Dict[str, any]:
+) -> Dict[str, Any]:
"""
Return a test configuration dictionary to be used
when instantiating the ShipEngine object.
@@ -44,378 +35,18 @@ def stub_shipengine_instance() -> ShipEngine:
return ShipEngine(config=stub_config())
-def address_with_all_fields() -> Address:
- """Return an address with all fields populated."""
- return Address(
- name="ShipEngine",
- company="Auctane",
- phone="123456789",
- street=["4 Jersey St", "Apt. 2b"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-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 valid_commercial_address() -> Address:
- """
- Return a test Address object with valid commercial
- address information.
- """
- return Address(
- street=["4 Jersey St", "ste 200"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def address_with_warnings() -> Address:
- """Return a test Address object that will cause the server to return warning messages."""
- return Address(
- street=["170 Warning Blvd", "Apartment 32-B"],
- city_locality="Toronto",
- state_province="On",
- postal_code="M6K 3C3",
- country_code="CA",
- )
-
-
-def address_with_single_error() -> Address:
- """Return a test Address object that will cause the server to return a single error message."""
- return Address(
- street=["170 Error Blvd"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def address_with_errors() -> Address:
- """Return a test Address object that will cause the server to return an error message."""
- return Address(
- street=["4 Invalid St"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def valid_canadian_address() -> Address:
- """Return an Address object with a valid canadian address."""
- return Address(
- street=["170 Princes Blvd", "Ste 200"],
- city_locality="Toronto",
- state_province="On",
- postal_code="M6K 3C3",
- country_code="CA",
- )
-
-
-def empty_address_lines() -> Address:
- """Returns an invalid address with empty street list."""
- return Address(
- street=list(),
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def address_with_too_many_lines() -> Address:
- """Return an address with too many address lines in the street list."""
- return Address(
- street=["4 Jersey St", "ste 200", "1st Floor", "Room B"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def multi_line_address() -> Address:
- """Returns a valid multiline address."""
- return Address(
- street=["4 Jersey St", "ste 200", "1st Floor"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def non_latin_address() -> Address:
- """Return an address with non-latin characters."""
- return Address(
- street=["上鳥羽角田町68"],
- city_locality="南区",
- state_province="京都",
- postal_code="601-8104",
- country_code="JP",
- )
-
-
-def unknown_address() -> Address:
- """
- Return an address that will make the server respond with an
- address with an unknown residential flag.
- """
- return Address(
- street=["4 Unknown St"],
- city_locality="Toronto",
- state_province="On",
- postal_code="M6K 3C3",
- country_code="CA",
- )
-
-
-def address_missing_required_fields() -> Address:
- """Return an address that is missing a state, city, and postal_code to return a ValidationError.."""
- return Address(
- street=["4 Jersey St"],
- city_locality="",
- state_province="",
- postal_code="",
- country_code="US",
- )
-
-
-def address_missing_country() -> Address:
- """Return an address that is only missing the country_code."""
- return Address(
- street=["4 Jersey St", "Apt. 2b"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="",
- )
-
-
-def address_with_invalid_country() -> Address:
- """Return an address that has an invalid country_code specified."""
- return Address(
- street=["4 Jersey St", "Apt. 2b"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="RZ",
- )
-
-
-def address_with_invalid_state() -> Address:
- """Return an address with an invalid state value."""
- return Address(
- street=["4 Jersey St", "Apt. 2b"],
- city_locality="Boston",
- state_province="&$",
- postal_code="02215",
- country_code="US",
- )
-
-
-def address_with_invalid_postal_code() -> Address:
- """Return an address with an invalid postal code."""
- return Address(
- street=["4 Jersey St", "Apt. 2b"],
- city_locality="Boston",
- state_province="MA",
- postal_code="2$1*5",
- country_code="US",
- )
-
-
-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(
- street=["500 Server Error"],
- city_locality="Boston",
- state_province="MA",
- postal_code="02215",
- country_code="US",
- )
-
-
-def validate_an_address(address: Address) -> AddressValidateResult:
- """
- Helper function that passes a config dictionary into the ShipEngine object to instantiate
- it and calls the `validate_address` method, providing it the `address` that is passed into
- this function.
- """
- return stub_shipengine_instance().validate_address(address=address)
-
-
-def normalize_an_address(address: Address) -> Address:
- """
- Helper function that passes a config dictionary into the ShipEngine object to instantiate
- it and calls the `normalize_address` method, providing it the `address` that is passed into
- this function.
- """
- return stub_shipengine_instance().normalize_address(address=address)
-
-
-def track_a_package(tracking_data: Union[str, Dict[str, any], TrackingQuery]):
- """"""
- return stub_shipengine_instance().track_package(tracking_data=tracking_data)
-
-
-def stub_get_carrier_accounts(carrier_code: Optional[str] = None):
- """Helper function that passes the `get_carrier_accounts` method a given carrier_code or None."""
- return stub_shipengine_instance().get_carrier_accounts(carrier_code=carrier_code)
-
-
-# Assertion helper functions
-
-
-def valid_address_assertions(
- test_method: str,
- locale: str,
- original_address: Address,
- returned_address: Union[Address, AddressValidateResult],
- expected_residential_indicator,
-) -> None:
- """
- A set of common assertions that are regularly made on the commercial US address
- used for testing the `validate_address` or `normalize_address` methods, based on
- the `test_method` that is passed in. It also makes different sets of assertions
- depending on what `locale` is passed in.
- """
- address = (
- returned_address.normalized_address
- if type(returned_address) is AddressValidateResult
- else returned_address
- )
- if locale == "domestic":
- if test_method == "validate":
- assert type(returned_address) is AddressValidateResult
- assert returned_address.is_valid is True
- assert type(address) is Address
- assert len(returned_address.info) == 0
- assert len(returned_address.warnings) == 0
- assert len(returned_address.errors) == 0
- assert address is not None
- assert address.city_locality == original_address.city_locality.upper()
- assert address.state_province == original_address.state_province.upper()
- assert address.postal_code == original_address.postal_code
- assert address.country_code == original_address.country_code.upper()
- assert address.is_residential is expected_residential_indicator
- elif test_method == "normalize":
- assert type(returned_address) is Address
- assert returned_address.city_locality == original_address.city_locality.upper()
- assert returned_address.state_province == original_address.state_province.upper()
- assert returned_address.postal_code == original_address.postal_code
- assert returned_address.country_code == original_address.country_code.upper()
- assert returned_address.is_residential is expected_residential_indicator
- elif locale == "international":
- if test_method == "validate":
- canada_valid_avs_assertions(
- original_address=original_address,
- validated_address=returned_address,
- expected_residential_indicator=expected_residential_indicator,
- )
- if test_method == "normalize":
- canada_valid_normalize_assertions(
- original_address=original_address,
- normalized_address=returned_address,
- expected_residential_indicator=expected_residential_indicator,
- )
-
-
-def canada_valid_avs_assertions(
- original_address: Address,
- validated_address: AddressValidateResult,
- expected_residential_indicator,
-) -> None:
- """
- A set of common assertions that are regularly made on the canadian_address
- used for testing `validate_address`.
- """
- address = validated_address.normalized_address
- assert type(validated_address) is AddressValidateResult
- assert validated_address.is_valid is True
- assert type(address) is Address
- assert len(validated_address.info) == 0
- assert len(validated_address.warnings) == 0
- assert len(validated_address.errors) == 0
- assert address is not None
- assert address.city_locality == original_address.city_locality
- assert address.state_province == original_address.state_province.title()
- assert address.postal_code == "M6K 3C3"
- assert address.country_code == original_address.country_code.upper()
- assert address.is_residential is expected_residential_indicator
-
-
-def us_valid_normalize_assertions(
- original_address: Address,
- normalized_address: Address,
- expected_residential_indicator,
-) -> None:
- """
- A set of common assertions that are regularly made on the commercial US address
- used for `normalized_address` testing.
- """
- assert type(normalized_address) is Address
- assert normalized_address.city_locality == original_address.city_locality.upper()
- assert normalized_address.state_province == original_address.state_province.upper()
- assert normalized_address.postal_code == original_address.postal_code
- assert normalized_address.country_code == original_address.country_code.upper()
- assert normalized_address.is_residential is expected_residential_indicator
-
-
-def canada_valid_normalize_assertions(
- original_address: Address,
- normalized_address: Address,
- expected_residential_indicator,
-) -> None:
- """
- A set of common assertions that are regularly made on the canadian_address
- used for testing `validate_address`.
- """
- assert type(normalized_address) is Address
- assert normalized_address.city_locality == original_address.city_locality
- assert normalized_address.state_province == original_address.state_province.title()
- 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"
+def valid_commercial_address() -> List[Dict[str, Any]]:
+ return [
+ {
+ "name": "ShipEngine",
+ "company": "Auctane",
+ "phone": "1-123-123-1234",
+ "address_line1": "3800 N Lamar Blvd",
+ "address_line2": "ste 220",
+ "city_locality": "Austin",
+ "state_province": "TX",
+ "postal_code": "78756",
+ "country_code": "US",
+ "address_residential_indicator": "unknown",
+ }
+ ]
diff --git a/tests/util/test_iso_string.py b/tests/util/test_iso_string.py
deleted file mode 100644
index b204828..0000000
--- a/tests/util/test_iso_string.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Testing the IsoString class object."""
-import datetime
-
-from shipengine_sdk.util.iso_string import IsoString
-
-
-class TestIsoString:
- _test_iso_string_no_tz: str = "2021-06-10T21:00:00.000"
-
- 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_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_with_tz("2021-06-10T21:00:00.000K") is False
diff --git a/tox.ini b/tox.ini
index f69bfac..31b51a3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,12 +5,13 @@
[tox]
envlist = linting,
- py37,
- py38,
- py39
-minversion = 3.6.0
-isolated_build = True
-skip_missing_interpreters = True
+ python3.7,
+ python3.8,
+ python3.9
+minversion = 3.7.0
+isolated_build = true
+skip_missing_interpreters = true
+skipsdist = true
[tox:.package]
basepython = python3.7
@@ -41,12 +42,12 @@ commands =
; coveralls --submit={toxworkdir}/.coverage.{envname}
[testenv:lint]
-skip_install = True
+skip_install = true
deps = pre-commit>=2.9.3
commands = pre-commit run --show-diff-on-failure {posargs:}
[coverage:run]
-relative_files = True
+relative_files = true
[flake8]
max-line-length = 100