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