diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed89b7b..504eafe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ on: - main jobs: - linting: + lint_and_pytest: runs-on: ubuntu-latest strategy: matrix: @@ -47,12 +47,15 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics + - name: Pytest + run: | + pytest - name: Run linting environment and pre-commit hooks run: | tox -e lint - tox-coveralls: + coveralls: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -61,26 +64,12 @@ jobs: with: python-version: 3.7 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Set up Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install python dependencies run: | python -m pip install --upgrade pip python -m pip install tox pytest pytest-cov coverage responses if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Tox testenv - run: | - tox - - name: Pytest run: | pytest @@ -92,7 +81,7 @@ jobs: flag-name: Python Test Suite coveralls_finish: - needs: tox-coveralls + needs: coveralls runs-on: ubuntu-latest steps: - name: Coveralls Finished diff --git a/README.md b/README.md index 699ecd4..016cc5e 100644 --- a/README.md +++ b/README.md @@ -76,37 +76,30 @@ on `osx / linux / bashonwindows`: curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - ``` -- Once you have **Poetry** installed you need to install the projects dependencies with this command: +- Once you have **Poetry** installed you need to install the projects dependencies with this command from the project root: ```bash -poetry install +bin/setup ``` +- This script will install all dependencies specified in `pyproject.toml` via `Poetry` and install the `pre-commit` hooks +this project uses. -- After you have installed **Poetry**, you need to start the python environment managed by Poetry by - running `poetry shell` in your terminal. - +## Adding dependencies to the project +If your changes require you to install a python package/module using `poetry add ` or +`poetry add -D` for a dev dependency. You will also need to run the following command to +regenerate a `requirements.txt` file that includes the newly added dependencies: ```bash -poetry shell - ``` +poetry export -f requirements.txt --output requirements.txt --without-hashes --dev +``` -### Pre-Commit Hooks +## Pre-Commit Hooks We are using [Pre-Commit](https://pre-commit.com/) to enforce formatting, lint rules, and code analysis so that this repo is always in good health. - - If you choose not to globally install `pre-commit`, then you can skip installing via `pip` or `homebrew` directly. - You can simply run either `pip install -r requirements.txt` or `poetry install` -To be able to push a PR to the repo after making changes locally, you will need to install `pre-commit` which -is a tool that runs linting, formatting, and code analysis on your changes. -```bash -pip install pre-commit # Install via pip +- `Pre-Commit` is installed and initialized when you run `bin/setup` from the project root as outlined above. -OR - -brew install pre-commit # Install via homebrew -``` -- After you have run either `pip install -r requirements.txt`, `poetry install`, or globally installed - [Pre-Commit](https://pre-commit.com/) using the above commands you need to run the following command - in the project directory locally. This allows the pre-commit hooks to run when you are looking to commit - and push code to this repository. +- If you choose not to use `Poetry` and prefer `pip` you can simply run `pip install -r requirements.txt` +To be able to commit & push a PR to the repo after making changes locally, you will need to install `pre-commit` which +is a tool that runs tests, linting, formatting, and code analysis on your changes. ```bash pre-commit install ``` diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..1ef36e2 --- /dev/null +++ b/bin/setup @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +poetry install +pre-commit install +poetry shell diff --git a/poetry.lock b/poetry.lock index 91ab14e..4e38846 100644 --- a/poetry.lock +++ b/poetry.lock @@ -74,6 +74,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pytz = ">=2015.7" +[[package]] +name = "base58" +version = "2.1.0" +description = "Base58 and Base58Check implementation." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +tests = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "PyHamcrest (>=2.0.2)", "coveralls", "pytest-benchmark"] + [[package]] name = "black" version = "20.8b1" @@ -208,6 +219,17 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + [[package]] name = "filelock" version = "3.0.12" @@ -230,6 +252,17 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "fuuid" +version = "0.1.0" +description = "Functional UUIDs for Python." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +base58 = ">=2.1.0,<3.0.0" + [[package]] name = "identify" version = "2.2.10" @@ -259,7 +292,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.0" +version = "4.6.1" description = "Read metadata from Python packages" category = "dev" optional = false @@ -274,6 +307,14 @@ docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "isort" version = "5.9.1" @@ -343,14 +384,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "more-itertools" -version = "8.8.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "multidict" version = "5.1.0" @@ -377,11 +410,11 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.9" +version = "21.0" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" @@ -467,26 +500,37 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "4.6.11" +version = "6.2.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] -atomicwrites = ">=1.0" -attrs = ">=17.4.0" -colorama = {version = "*", markers = "sys_platform == \"win32\" and python_version != \"3.4\""} +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = {version = ">=4.0.0", markers = "python_version > \"2.7\""} +iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -six = ">=1.10.0" -wcwidth = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] +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" @@ -504,6 +548,34 @@ toml = "*" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-mock" +version = "3.6.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] +name = "pytest-watch" +version = "4.2.0" +description = "Local continuous test runner with pytest and watchdog." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = ">=0.3.3" +docopt = ">=0.4.0" +pytest = ">=2.6.4" +watchdog = ">=0.6.0" + [[package]] name = "python-dotenv" version = "0.15.0" @@ -533,7 +605,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" -version = "2021.4.4" +version = "2021.7.6" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -791,12 +863,15 @@ docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sp testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] [[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" +name = "watchdog" +version = "2.1.3" +description = "Filesystem events monitoring" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] [[package]] name = "yarl" @@ -813,7 +888,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.4.1" +version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false @@ -821,12 +896,12 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "2f4c29be3ad34cf982b28e4a892b6289c196d5d7c492b287afe67abaa68df6d1" +content-hash = "44a12436d6071b62002beab90bd98605720f47f4c08c492d52f1b482efaeadbf" [metadata.files] aiohttp = [ @@ -892,6 +967,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"}, ] +base58 = [ + {file = "base58-2.1.0-py3-none-any.whl", hash = "sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48"}, + {file = "base58-2.1.0.tar.gz", hash = "sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2"}, +] black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] @@ -988,6 +1067,10 @@ docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] +execnet = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, @@ -996,6 +1079,9 @@ flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] +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"}, @@ -1009,8 +1095,12 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.0-py3-none-any.whl", hash = "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33"}, - {file = "importlib_metadata-4.6.0.tar.gz", hash = "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb"}, + {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, + {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, +] +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"}, @@ -1068,10 +1158,6 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -more-itertools = [ - {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"}, - {file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"}, -] multidict = [ {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, @@ -1120,8 +1206,8 @@ nodeenv = [ {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, @@ -1156,13 +1242,23 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-4.6.11-py2.py3-none-any.whl", hash = "sha256:a00a7d79cbbdfa9d21e7d0298392a8dd4123316bfac545075e6f8f24c94d8c97"}, - {file = "pytest-4.6.11.tar.gz", hash = "sha256:50fa82392f2120cc3ec2ca0a75ee615be4c479e66669789771f1758332be4353"}, + {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"}, ] +pytest-mock = [ + {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, + {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, +] +pytest-watch = [ + {file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"}, +] python-dotenv = [ {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, @@ -1203,47 +1299,47 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] regex = [ - {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, - {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, - {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, - {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, - {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, - {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, - {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, - {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, - {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, - {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, - {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, - {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, - {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, + {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"}, ] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, @@ -1350,9 +1446,28 @@ virtualenv = [ {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, ] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +watchdog = [ + {file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"}, + {file = "watchdog-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acc4e2d5be6f140f02ee8590e51c002829e2c33ee199036fcd61311d558d89f4"}, + {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b851237cf3533fabbc034ffcd84d0fa52014b3121454e5f8b86974b531560c"}, + {file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a12539ecf2478a94e4ba4d13476bb2c7a2e0a2080af2bb37df84d88b1b01358a"}, + {file = "watchdog-2.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6fe9c8533e955c6589cfea6f3f0a1a95fb16867a211125236c82e1815932b5d7"}, + {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d9456f0433845e7153b102fffeb767bde2406b76042f2216838af3b21707894e"}, + {file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd8c595d5a93abd441ee7c5bb3ff0d7170e79031520d113d6f401d0cf49d7c8f"}, + {file = "watchdog-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0bcfe904c7d404eb6905f7106c54873503b442e8e918cc226e1828f498bdc0ca"}, + {file = "watchdog-2.1.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf84bd94cbaad8f6b9cbaeef43080920f4cb0e61ad90af7106b3de402f5fe127"}, + {file = "watchdog-2.1.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b8ddb2c9f92e0c686ea77341dcb58216fa5ff7d5f992c7278ee8a392a06e86bb"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8805a5f468862daf1e4f4447b0ccf3acaff626eaa57fbb46d7960d1cf09f2e6d"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:3e305ea2757f81d8ebd8559d1a944ed83e3ab1bdf68bcf16ec851b97c08dc035"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_i686.whl", hash = "sha256:431a3ea70b20962e6dee65f0eeecd768cd3085ea613ccb9b53c8969de9f6ebd2"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:e4929ac2aaa2e4f1a30a36751160be391911da463a8799460340901517298b13"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:201cadf0b8c11922f54ec97482f95b2aafca429c4c3a4bb869a14f3c20c32686"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:3a7d242a7963174684206093846537220ee37ba9986b824a326a8bb4ef329a33"}, + {file = "watchdog-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:54e057727dd18bd01a3060dbf5104eb5a495ca26316487e0f32a394fd5fe725a"}, + {file = "watchdog-2.1.3-py3-none-win32.whl", hash = "sha256:b5fc5c127bad6983eecf1ad117ab3418949f18af9c8758bd10158be3647298a9"}, + {file = "watchdog-2.1.3-py3-none-win_amd64.whl", hash = "sha256:44acad6f642996a2b50bb9ce4fb3730dde08f23e79e20cd3d8e2a2076b730381"}, + {file = "watchdog-2.1.3-py3-none-win_ia64.whl", hash = "sha256:0bcdf7b99b56a3ae069866c33d247c9994ffde91b620eaf0306b27e099bd1ae0"}, + {file = "watchdog-2.1.3.tar.gz", hash = "sha256:e5236a8e8602ab6db4b873664c2d356c365ab3cac96fbdec4970ad616415dd45"}, ] yarl = [ {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, @@ -1394,6 +1509,6 @@ yarl = [ {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, ] zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, ] diff --git a/pyproject.toml b/pyproject.toml index c554a19..1270093 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,19 +13,23 @@ aiohttp = "^3.7.4" requests = "^2.25.1" python-dotenv = "^0.15.0" dataclasses-json = "^0.5.3" +fuuid = "^0.1.0" [tool.poetry.dev-dependencies] -pytest = "^4.6" +pytest = ">=5.0" +pytest-cov = "^2.11.1" +pytest-mock = "^3.6.1" black = "^20.8b1" flake8 = "^3.8.4" Sphinx = "^3.5.2" tox = "^3.23.0" coverage = "^5.5" -pytest-cov = "^2.11.1" isort = "^5.8.0" responses = "^0.13.3" coveralls = "^3.1.0" pre-commit = "2.13.0" +pytest-cache = "^1.0" +pytest-watch = "^4.2.0" [tool.black] line-length = 100 diff --git a/requirements.txt b/requirements.txt index 5763db5..ec44660 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,52 +2,58 @@ 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" async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" -atomicwrites==1.4.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +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" +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" 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" 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 (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") and platform_system == "Windows" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0") or sys_platform == "win32" and python_version >= "3.6" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") and python_full_version >= "3.5.0" and platform_system == "Windows" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0") +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" dataclasses-json==0.5.4; python_version >= "3.6" distlib==0.3.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" docopt==0.6.2; python_version >= "3.5" docutils==0.16; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" +execnet==1.9.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" filelock==3.0.12; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +fuuid==0.1.0; python_version >= "3.7" and python_version < "4.0" 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" 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.0; python_full_version >= "3.6.1" and 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.4.0" and 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") +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") +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" 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" -more-itertools==8.8.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" 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==20.9; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" +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" -pluggy==0.13.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +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" pre-commit==2.13.0; python_full_version >= "3.6.1" -py==1.10.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +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" pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pygments==2.9.0; python_version >= "3.5" -pyparsing==2.4.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" -pytest==4.6.11; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") +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.4.4; python_version >= "3.6" +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") 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" @@ -60,13 +66,13 @@ sphinxcontrib-jsmath==1.0.1; python_version >= "3.5" 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" +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") typed-ast==1.4.3; python_version >= "3.6" -typing-extensions==3.10.0.0; 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.4.0" and python_version < "3.8" and 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" -wcwidth==0.2.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +watchdog==2.1.3; python_version >= "3.6" yarl==1.6.3; python_version >= "3.6" -zipp==3.4.1; 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.8" and python_version >= "3.6" +zipp==3.5.0; python_version < "3.8" and python_version >= "3.6" diff --git a/shipengine_sdk/events/__init__.py b/shipengine_sdk/events/__init__.py new file mode 100644 index 0000000..d0a5cde --- /dev/null +++ b/shipengine_sdk/events/__init__.py @@ -0,0 +1,203 @@ +""" +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 index 2a13aa0..ae2bf2a 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -2,6 +2,7 @@ import json import os import platform +from datetime import datetime from typing import Any, Dict, Optional import requests @@ -13,10 +14,55 @@ 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 is_response_404, is_response_429, is_response_500 +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): @@ -30,10 +76,16 @@ def __call__(self, request: Request, *args, **kwargs) -> Request: class ShipEngineClient: - _BASE_URI: str = "" + _DISPATCHER: Dispatcher = Dispatcher() - def __init__(self) -> None: + 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( @@ -44,29 +96,38 @@ def send_rpc_request( * is successful, the result is returned. Otherwise, an error is thrown. """ client: Session = self._request_retry_session(retries=config.retries) - base_uri: Optional[str] = ( - config.base_uri - if os.getenv("CLIENT_BASE_URI") is None - else os.getenv("CLIENT_BASE_URI") - ) - - request_headers: Dict[str, Any] = { - "User-Agent": self._derive_user_agent(), - "Content-Type": "application/json", - "Accept": "application/json", - } + 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=request_headers, + 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: @@ -80,10 +141,32 @@ def send_rpc_request( resp_body: Dict[str, Any] = resp.json() status_code: int = resp.status_code - is_response_404(status_code=status_code, response_body=resp_body, config=config) - is_response_429(status_code=status_code, response_body=resp_body, config=config) - is_response_500(status_code=status_code, response_body=resp_body) + 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( @@ -112,8 +195,7 @@ def _derive_user_agent() -> str: :rtype: str """ sdk_version: str = f"shipengine-python/{__version__}" - os_kernel: str = platform.platform(terse=True) python_version: str = platform.python_version() python_implementation: str = platform.python_implementation() - return f"{sdk_version} {os_kernel} {python_version} {python_implementation}" + return f"{sdk_version} {python_implementation}-v{python_version}" diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index d25f3b4..7502077 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -21,8 +21,7 @@ def rpc_request( def rpc_request_loop( method: str, params: Optional[Dict[str, Any]], config: ShipEngineConfig ) -> Dict[str, Any]: - client: ShipEngineClient = ShipEngineClient() - api_response: Optional[Dict[str, Any]] = None + client: ShipEngineClient = ShipEngineClient(config=config) retry: int = 0 while retry <= config.retries: try: @@ -36,7 +35,8 @@ def rpc_request_loop( and err.retry_after < config.timeout ): time.sleep(err.retry_after) + retry += 1 + continue else: raise err - retry += 1 return api_response diff --git a/shipengine_sdk/jsonrpc/process_request.py b/shipengine_sdk/jsonrpc/process_request.py index a7d59df..05fa692 100644 --- a/shipengine_sdk/jsonrpc/process_request.py +++ b/shipengine_sdk/jsonrpc/process_request.py @@ -1,6 +1,7 @@ """Functions that help with process requests and handle responses.""" from typing import Any, Dict, Optional -from uuid import uuid4 + +from fuuid import b58_fuuid from ..errors import ( AccountStatusError, @@ -24,11 +25,9 @@ def wrap_request(method: str, params: Optional[Dict[str, Any]]) -> Dict[str, Any :type params: Optional[Dict[str, Any]] """ if params is None: - return dict(id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method) + return dict(id=f"req_{b58_fuuid()}", jsonrpc="2.0", method=method) else: - return dict( - id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method, params=params - ) + 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]: diff --git a/shipengine_sdk/models/address/__init__.py b/shipengine_sdk/models/address/__init__.py index 7061a10..a47a399 100644 --- a/shipengine_sdk/models/address/__init__.py +++ b/shipengine_sdk/models/address/__init__.py @@ -22,10 +22,10 @@ class Address: state_province: str postal_code: str country_code: str - is_residential: Optional[bool] = None - name: str = "" - phone: str = "" - company: 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) diff --git a/shipengine_sdk/models/carriers/__init__.py b/shipengine_sdk/models/carriers/__init__.py index 90f958e..a7c04fd 100644 --- a/shipengine_sdk/models/carriers/__init__.py +++ b/shipengine_sdk/models/carriers/__init__.py @@ -31,7 +31,7 @@ class CarrierAccount: 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_id = account_information["accountId"] self.account_number = account_information["accountNumber"] def _set_carrier(self, carrier: str) -> None: diff --git a/shipengine_sdk/models/enums/__init__.py b/shipengine_sdk/models/enums/__init__.py index 9badc5a..be38bf1 100644 --- a/shipengine_sdk/models/enums/__init__.py +++ b/shipengine_sdk/models/enums/__init__.py @@ -9,13 +9,26 @@ from .regex_patterns import RegexPatterns +class Constants(Enum): + """Test API Key for use with Simengine.""" + + STUB_API_KEY = "TEST_vMiVbICUjBz4BZjq0TRBLC/9MrxY4+yjvb1G1RMxlJs" + CARRIER_ACCOUNT_ID_STUB = "car_41GrQHn5uouiPZc2TNE6PU29tZU9ud" + + class Endpoints(Enum): """API Endpoint URI's used throughout the ShipEngine SDK.""" - TEST_RPC_URL = "https://simengine.herokuapp.com/jsonrpc" SHIPENGINE_RPC_URL = "https://api.shipengine.com/jsonrpc" +class Events(Enum): + """ShipEngine Events emitted by the SDK when a request is sent or when a response is received.""" + + ON_REQUEST_SENT = "on_request_sent" + ON_RESPONSE_RECEIVED = "on_response_received" + + class RPCMethods(Enum): """A collection of RPC Methods used throughout the ShipEngine SDK.""" diff --git a/shipengine_sdk/models/package/__init__.py b/shipengine_sdk/models/package/__init__.py index 432526d..daeeff0 100644 --- a/shipengine_sdk/models/package/__init__.py +++ b/shipengine_sdk/models/package/__init__.py @@ -17,7 +17,7 @@ class Shipment: shipment_id: Optional[str] = None account_id: Optional[str] = None carrier_account: Optional[CarrierAccount] = None - carrier: Carrier + carrier: Optional[Carrier] = None estimated_delivery_date: Union[IsoString, str] actual_delivery_date: Union[IsoString, str] @@ -26,8 +26,8 @@ def __init__( ) -> 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 + 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( @@ -59,7 +59,7 @@ def _get_carrier_account(self, carrier: str, account_id: str) -> CarrierAccount: return target_carrier[0] raise ShipEngineError( - message=f"accountID [{account_id}] doesn't match any of the accounts connected to your ShipEngine Account." # noqa + 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]: @@ -77,7 +77,7 @@ def to_json(self) -> str: return json.dumps(self, default=lambda o: o.__dict__, indent=2) def __repr__(self): - return f"Shipment({self.shipment_id}, {self.account_id}, {self.carrier_account}, {self.carrier}, {self.estimated_delivery_date}, {self.actual_delivery_date})" # noqa + return f"Shipment({self.shipment_id}, {self.account_id})" class Package: @@ -90,11 +90,11 @@ class Package: tracking_url: Optional[str] def __init__(self, package: Dict[str, Any]) -> None: - self.package_id = package["packageID"] if "packageID" in package else 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 + self.tracking_url = package["trackingUrl"] if "trackingUrl" in package else None def to_dict(self) -> Dict[str, Any]: return (lambda o: o.__dict__)(self) @@ -125,15 +125,31 @@ class Location: def __init__(self, location_data: Dict[str, Any]) -> None: self.city_locality = ( - location_data["cityLocality"] if "cityLocality" in location_data else None + 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 else None + 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 ) - self.postal_code = location_data["postalCode"] if "postalCode" in location_data else None - self.country_code = location_data["countryCode"] if "countryCode" in location_data else None - if "coordinates" in location_data: + 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"] @@ -168,8 +184,15 @@ def __init__(self, event: Dict[str, Any]) -> None: self.carrier_status_code = ( event["carrierStatusCode"] if "carrierStatusCode" in event else None ) + self.carrier_detail_code = ( + event["carrierDetailCode"] if "carrierDetailCode" in event else None + ) self.signer = event["signer"] if "signer" in event else None - self.location = Location(event["location"]) if "location" 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) diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py index f38318d..31b4552 100644 --- a/shipengine_sdk/services/address_validation.py +++ b/shipengine_sdk/services/address_validation.py @@ -20,14 +20,14 @@ def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResul api_response: Dict[str, Any] = rpc_request( method=RPCMethods.ADDRESS_VALIDATE.value, config=config, - params={"address": address.to_dict()}, # type: ignore + 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 + if "normalizedAddress" in result and result["normalizedAddress"] is not None else None, messages=result["messages"], ) diff --git a/shipengine_sdk/services/track_package.py b/shipengine_sdk/services/track_package.py index c862efd..764d6c2 100644 --- a/shipengine_sdk/services/track_package.py +++ b/shipengine_sdk/services/track_package.py @@ -14,7 +14,7 @@ def track(tracking_data: Union[str, TrackingQuery], config: ShipEngineConfig) -> api_response = rpc_request( method=RPCMethods.TRACK_PACKAGE.value, config=config, - params={"packageID": tracking_data}, + params={"packageId": tracking_data}, ) return TrackPackageResult(api_response, config) diff --git a/shipengine_sdk/shipengine_config.py b/shipengine_sdk/shipengine_config.py index 5649b1b..596bbc1 100644 --- a/shipengine_sdk/shipengine_config.py +++ b/shipengine_sdk/shipengine_config.py @@ -2,6 +2,7 @@ import json from typing import Any, Dict, Optional +from .events import ShipEngineEventListener from .models import Endpoints from .util import is_api_key_valid, is_retries_valid, is_timeout_valid @@ -48,7 +49,11 @@ def __init__(self, config: Dict[str, Any]) -> None: self.retries: int = config["retries"] else: self.retries: int = self.DEFAULT_RETRIES - # TODO: add event listener to config object once it"s implemented. + + 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): """ @@ -79,7 +84,12 @@ def merge(self, new_config: Optional[Dict[str, Any]] = None): config.update( {"timeout": new_config["timeout"]} ) if "timeout" in new_config else config.update({"timeout": self.timeout}) - # TODO: added merge rule for event_listener once it is implemented. + + 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) diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py index 3d660a4..cdd02e3 100644 --- a/shipengine_sdk/util/iso_string.py +++ b/shipengine_sdk/util/iso_string.py @@ -11,9 +11,7 @@ def __init__(self, iso_string: str) -> None: A string representing a Date, DateTime, or DateTime with Timezone. The object also has a method to return a `datetime.datetime` object, which is the native datetime object in python as of 3.7. - This class object takes in an **ISO-8601** string. Learn more here: https://en.wikipedia.org/wiki/ISO_8601 - :param str iso_string: An `ISO-8601` string. Learn more here: https://en.wikipedia.org/wiki/ISO_8601 """ self.iso_string = iso_string @@ -25,27 +23,38 @@ 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(self.iso_string, "%Y-%m-%dT%H:%M:%S.%fZ") - elif self.is_valid_iso_string_no_tz(self.iso_string): - return datetime.fromisoformat(self.iso_string) + 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(self.iso_string): - return False if self.is_valid_iso_string_no_tz(self.iso_string) else True + if self.is_valid_iso_string_with_tz(self.iso_string): + return False if self.is_valid_iso_string_with_tz_no_tz(self.iso_string) else True @staticmethod - def is_valid_iso_string_no_tz(iso_str: str): - pattern = re.compile(RegexPatterns.VALID_ISO_STRING_NO_TZ.value) + 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(iso_str: str): - pattern = re.compile(RegexPatterns.VALID_ISO_STRING.value) + 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/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index ebe46c6..b45c2a0 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -174,8 +174,10 @@ def timeout_validation_error_assertions(error) -> None: assert error.source is ErrorSource.SHIPENGINE.value -def is_response_404(status_code: int, response_body: Dict[str, Any], config) -> None: - """Check if status_code is 404 and raises an error if so.""" +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.""" + + # 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"] @@ -194,12 +196,11 @@ def is_response_404(status_code: int, response_body: Dict[str, Any], config) -> error_code=ErrorCode.NOT_FOUND.value, ) - -def is_response_429(status_code: int, response_body: Dict[str, Any], config) -> None: - """Check if status_code is 429 and raises an error if so.""" + # Check if status_code is 429 and raises an error if so. if "error" in response_body and status_code == 429: error = response_body["error"] - retry_after = error["data"]["retryAfter"] + error_data = error["data"] + retry_after = error_data["details"]["retryAfter"] if retry_after > config.timeout: raise ClientTimeoutError( retry_after=config.timeout, @@ -213,9 +214,7 @@ def is_response_429(status_code: int, response_body: Dict[str, Any], config) -> request_id=response_body["id"], ) - -def is_response_500(status_code: int, response_body: Dict[str, Any]) -> None: - """Check if the status code is 500 and raises an error if so.""" + # 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"] diff --git a/tests/events/__init__.py b/tests/events/__init__.py new file mode 100644 index 0000000..39fe0c3 --- /dev/null +++ b/tests/events/__init__.py @@ -0,0 +1 @@ +"""Tests around events emitted from ShipEngine SDK.""" diff --git a/tests/events/test_emitted_events.py b/tests/events/test_emitted_events.py new file mode 100644 index 0000000..790ea0c --- /dev/null +++ b/tests/events/test_emitted_events.py @@ -0,0 +1,238 @@ +"""Test that `RequestSentEvents` are emitted from the SDK properly.""" +from datetime import datetime + +from pytest_mock import MockerFixture + +from shipengine_sdk import __version__ +from shipengine_sdk.errors import ( + ClientTimeoutError, + RateLimitExceededError, + ShipEngineError, +) +from shipengine_sdk.events import ( + RequestSentEvent, + ResponseReceivedEvent, + ShipEngineEventListener, +) +from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType +from shipengine_sdk.models.enums import Constants + +from ..util import ( + assert_on_429_exception, + configurable_stub_shipengine_instance, + valid_residential_address, +) + + +class TestEmittedEvents: + def test_user_agent_includes_correct_sdk_version(self, mocker: MockerFixture) -> None: + """DX-1517 - Test user agent includes correct SDK version.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 1, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + shipengine.validate_address(address=valid_residential_address()) + request_sent_return = request_sent_spy.spy_return + assert request_sent_return.headers["User-Agent"].split(" ")[0].split("/")[1] == __version__ + + def test_request_sent_event_on_retries(self, mocker: MockerFixture) -> None: + """DX-1521 - Test that a RequestSentEvent is emitted on retries.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 1, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 2 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 1 + assert type(request_sent_return.timestamp) == datetime + assert request_sent_return.timeout == config["timeout"] + assert request_sent_return.body["method"] == "carrier.listAccounts.v1" + assert request_sent_return.base_uri == "https://api.shipengine.com/jsonrpc" + assert request_sent_return.headers["Api-Key"] == Constants.STUB_API_KEY.value + assert request_sent_return.headers["Content-Type"] == "application/json" + assert ( + request_sent_return.message == "Retrying the ShipEngine carrier.listAccounts.v1 API" + " at https://api.shipengine.com/jsonrpc" + ) + + def test_response_received_event_success(self, mocker: MockerFixture) -> None: + """DX-1522 Test response received event success.""" + test_start_time = datetime.now() + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 2, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + shipengine.validate_address(address=valid_residential_address()) + + response_recd_return = response_received_spy.spy_return + assert response_received_spy.call_count == 1 + assert type(response_recd_return) == ResponseReceivedEvent + assert ( + response_recd_return.message + == "Received an HTTP 200 response from the ShipEngine address.validate.v1 API" + ) + assert response_recd_return.status_code == 200 + assert response_recd_return.base_uri == "https://api.shipengine.com/jsonrpc" + assert response_recd_return.body["method"] == "address.validate.v1" + assert response_recd_return.retry == 0 + assert response_recd_return.elapsed < test_start_time.second + assert response_recd_return.headers["Content-Type"].split(";")[0] == "application/json" + + def test_response_received_on_error(self, mocker: MockerFixture) -> None: + """DX-1523 - Test response received event on error.""" + test_start_time = datetime.now() + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 1, + "timeout": 10, + } + shipengine = configurable_stub_shipengine_instance(config=config) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + response_recd_return = response_received_spy.spy_return + assert type(response_recd_return) == ResponseReceivedEvent + assert ( + response_recd_return.message + == "Retrying the ShipEngine carrier.listAccounts.v1 API at https://api.shipengine.com/jsonrpc" + ) + assert response_recd_return.status_code == 429 + assert response_recd_return.base_uri == "https://api.shipengine.com/jsonrpc" + assert response_recd_return.body["method"] == "carrier.listAccounts.v1" + assert response_recd_return.retry == 1 + assert response_recd_return.elapsed < test_start_time.second + assert (response_recd_return.timestamp - test_start_time).total_seconds() > 1 + + def test_config_with_retries_disabled(self, mocker: MockerFixture) -> None: + """DX-1527 - Tests that the SDK does not automatically retry if retries in config is set to 0.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + shipengine = configurable_stub_shipengine_instance( + { + "api_key": Constants.STUB_API_KEY.value, + "retries": 0, + "timeout": 10, + } + ) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 0 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 0 + + def test_config_with_custom_retries(self, mocker: MockerFixture) -> None: + """DX-1528 - Test config with custom retries.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + shipengine = configurable_stub_shipengine_instance( + { + "api_key": Constants.STUB_API_KEY.value, + "retries": 3, + "timeout": 21, + } + ) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 4 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 3 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 4 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 3 + + def test_timeout_err_when_retry_greater_than_timeout(self, mocker: MockerFixture) -> None: + """DX-1529 - Test timeout error when retry_after is greater than timeout.""" + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + config = { + "api_key": Constants.STUB_API_KEY.value, + "retries": 3, + "timeout": 1, + } + shipengine = configurable_stub_shipengine_instance(config=config) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert type(err) == ClientTimeoutError + assert err.request_id is not None + assert err.request_id.startswith("req_") + assert ( + err.message + == f"The request took longer than the {config['timeout']} seconds allowed." + ) + assert err.source is ErrorSource.SHIPENGINE.value + assert err.error_type is ErrorType.SYSTEM.value + assert err.error_code is ErrorCode.TIMEOUT.value + assert err.url == "https://www.shipengine.com/docs/rate-limits" + + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 0 + assert request_sent_return.timeout == 1 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 1 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 0 + + def test_retry_waits_correct_amount_of_time(self, mocker: MockerFixture) -> None: + """DX-1530 - retry waits the correct amount of time.""" + test_start_time = datetime.now() + request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") + response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") + shipengine = configurable_stub_shipengine_instance( + { + "api_key": Constants.STUB_API_KEY.value, + "retries": 2, + "timeout": 10, + } + ) + try: + shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") + except ShipEngineError as err: + assert_on_429_exception(err=err, error_class=RateLimitExceededError) + + request_sent_return = request_sent_spy.spy_return + assert request_sent_spy.call_count == 3 + assert type(request_sent_return) == RequestSentEvent + assert request_sent_return.retry == 2 + assert request_sent_return.timeout == 10 + + response_recd_return = response_received_spy.spy_return + assert request_sent_spy.call_count == 3 + assert type(response_recd_return) == ResponseReceivedEvent + assert response_recd_return.retry == 2 + assert ( + int(str(round((test_start_time - datetime.now()).total_seconds())).strip("-")) <= 6 + ) diff --git a/tests/http_client/test_http_client.py b/tests/http_client/test_http_client.py index af6884a..b41776e 100644 --- a/tests/http_client/test_http_client.py +++ b/tests/http_client/test_http_client.py @@ -13,7 +13,6 @@ def validate_address(address): shipengine = ShipEngine( dict( api_key="baz", - base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=10, @@ -47,7 +46,7 @@ class TestShipEngineClient: def test_500_server_response(self): responses.add( responses.POST, - Endpoints.TEST_RPC_URL.value, + Endpoints.SHIPENGINE_RPC_URL.value, json={ "jsonrpc": "2.0", "id": "req_DezVNUvRkAP819f3JeqiuS", @@ -74,7 +73,7 @@ def test_500_server_response(self): def test_404_server_response(self): responses.add( responses.POST, - Endpoints.TEST_RPC_URL.value, + Endpoints.SHIPENGINE_RPC_URL.value, json={ "jsonrpc": "2.0", "id": "req_DezVNUvRkAP819f3JeqiuS", diff --git a/tests/models/address/test_address.py b/tests/models/address/test_address.py index 2c0856b..150c6f7 100644 --- a/tests/models/address/test_address.py +++ b/tests/models/address/test_address.py @@ -3,7 +3,7 @@ from shipengine_sdk.errors import ValidationError from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType -from tests.util.test_helpers import address_with_too_many_lines, empty_address_lines +from tests.util import address_with_too_many_lines, empty_address_lines def address_line_assertions(err: ValidationError, variant: str) -> None: diff --git a/tests/models/carriers/test_carrier_account.py b/tests/models/carriers/test_carrier_account.py index d796aff..4fd8ab0 100644 --- a/tests/models/carriers/test_carrier_account.py +++ b/tests/models/carriers/test_carrier_account.py @@ -11,7 +11,7 @@ def stub_carrier_account_object() -> Dict[str, Any]: from the returned ShipEngine API response. """ return { - "accountID": "car_1knseddGBrseWTiw", + "accountId": "car_1knseddGBrseWTiw", "accountNumber": "1169350", "carrierCode": "royal_mail", "name": "United Parcel Service", diff --git a/tests/models/track_package/test_package.py b/tests/models/track_package/test_package.py index abe4813..b21d5db 100644 --- a/tests/models/track_package/test_package.py +++ b/tests/models/track_package/test_package.py @@ -6,9 +6,9 @@ def stub_package_data() -> Dict[str, Any]: return { - "packageID": "pkg_1FedExAccepted", + "packageId": "pkg_1FedExAccepted", "trackingNumber": "5fSkgyuh3GkfUjTZSEAQ8gHeTU29tZ", - "trackingURL": "https://www.fedex.com/track/5fSkgyuh3GkfUjTZSEAQ8gHeTU29tZ", + "trackingUrl": "https://www.fedex.com/track/5fSkgyuh3GkfUjTZSEAQ8gHeTU29tZ", "weight": {"value": 76, "unit": "kilogram"}, "dimensions": {"length": 36, "width": 36, "height": 23, "unit": "inch"}, } diff --git a/tests/models/track_package/test_shipment.py b/tests/models/track_package/test_shipment.py index 1c692b7..ba30ac7 100644 --- a/tests/models/track_package/test_shipment.py +++ b/tests/models/track_package/test_shipment.py @@ -5,6 +5,7 @@ from shipengine_sdk.errors import ShipEngineError from shipengine_sdk.models import Shipment +from shipengine_sdk.models.enums import Constants from shipengine_sdk.util.iso_string import IsoString from ...util import stub_shipengine_config @@ -17,8 +18,8 @@ def stub_valid_shipment_data() -> Dict[str, Any]: """ return { "carrierCode": "fedex", - "carrierAccountID": "car_kfUjTZSEAQ8gHeT", - "shipmentID": "shp_yuh3GkfUjTZSEAQ", + "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value, + "shipmentId": "shp_yuh3GkfUjTZSEAQ", "estimatedDelivery": "2021-06-15T21:00:00.000Z", } @@ -26,18 +27,18 @@ def stub_valid_shipment_data() -> Dict[str, Any]: 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. + be returned by ShipEngine API, where the `carrierAccountId` is invalid. """ return { "carrierCode": "fedex", - "carrierAccountID": "car_kfUoSHIPENGINEQ8gHeT", - "shipmentID": "shp_yuh3GkfUjTZSEAQ", + "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 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"), diff --git a/tests/models/track_package/test_track_package_result.py b/tests/models/track_package/test_track_package_result.py index c0612e9..f7541a0 100644 --- a/tests/models/track_package/test_track_package_result.py +++ b/tests/models/track_package/test_track_package_result.py @@ -2,6 +2,7 @@ from typing import Any, Dict from shipengine_sdk.models import TrackingEvent, TrackPackageResult +from shipengine_sdk.models.enums import Constants from ...util import stub_shipengine_config @@ -17,14 +18,14 @@ def stub_track_package_data() -> Dict[str, Any]: "result": { "shipment": { "carrierCode": "fedex", - "carrierAccountID": "car_kfUjTZSEAQ8gHeT", - "shipmentID": "shp_tJUaQJz3Twz57iL", + "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value, + "shipmentId": "shp_tJUaQJz3Twz57iL", "estimatedDelivery": "2021-06-15T21:00:00.000Z", }, "package": { - "packageID": "pkg_1FedexDeLiveredException", + "packageId": "pkg_1FedexDeLiveredException", "trackingNumber": "2A4g3tJUaQJz3Twz57iLWBciD7wZWH", - "trackingURL": "https://www.fedex.com/track/2A4g3tJUaQJz3Twz57iLWBciD7wZWH", + "trackingUrl": "https://www.fedex.com/track/2A4g3tJUaQJz3Twz57iLWBciD7wZWH", "weight": {"value": 76, "unit": "kilogram"}, "dimensions": {"length": 36, "width": 36, "height": 23, "unit": "inch"}, }, diff --git a/tests/services/test_track_package.py b/tests/services/test_track_package.py index d4e3a59..49fd566 100644 --- a/tests/services/test_track_package.py +++ b/tests/services/test_track_package.py @@ -23,11 +23,11 @@ def assertions_on_delivered_after_exception_or_multiple_attempts( 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 == "unknown" - assert tracking_result.events[3].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 == "in_transit" + assert tracking_result.events[6].status == "attempted_delivery" assert tracking_result.events[7].status == "delivered" assert tracking_result.events[-1].status == "delivered" @@ -83,7 +83,7 @@ def test_track_by_tracking_number_and_carrier_code(self) -> None: 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.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 @@ -134,11 +134,7 @@ def test_multiple_delivery_attempts(self) -> None: 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[4].status == "attempted_delivery" - assert tracking_result.events[5].status == "in_transit" - assert tracking_result.events[6].status == "attempted_delivery" - assert tracking_result.events[7].status == "in_transit" - assert tracking_result.events[8].status == "delivered" + assert tracking_result.events[-1].status == "delivered" def test_delivered_on_first_try(self) -> None: """DX-1091 - Test delivered on first try tracking event.""" @@ -171,7 +167,6 @@ def test_delivered_with_signature(self) -> None: 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[2].status == "unknown" assert tracking_result.events[3].status == "in_transit" assert tracking_result.events[4].status == "delivered" assert tracking_result.events[-1].status == "delivered" @@ -229,11 +224,10 @@ def test_multiple_locations_in_tracking_event(self) -> None: 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 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 - assert tracking_result.events[4].location.latitude is None - assert tracking_result.events[4].location.longitude is None def test_carrier_date_time_without_timezone(self) -> None: """DX-1098 - Test track package where carrierDateTime has no timezone.""" @@ -321,4 +315,4 @@ def test_server_side_error(self) -> None: 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 connect to the database" + assert err.message == "Unable to process this request. A downstream API error occurred." diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index 5357b74..81b392f 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -14,9 +14,7 @@ def stub_config() -> dict: Return a test configuration dictionary to be used when instantiating the ShipEngine object. """ - return dict( - api_key="baz", base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=15 - ) + return dict(api_key="baz_sim", page_size=50, retries=2, timeout=15) def valid_residential_address() -> Address: @@ -59,7 +57,7 @@ def set_config_timeout(timeout: int) -> ShipEngineConfig: :raises: :class:`InvalidFieldValueError`: If invalid value is passed into `ShipEngineConfig` object at instantiation. """ - return ShipEngineConfig(dict(api_key="baz", timeout=timeout)) + return ShipEngineConfig(dict(api_key="baz_sim", timeout=timeout)) def set_config_retries(retries: int) -> ShipEngineConfig: @@ -73,7 +71,7 @@ def set_config_retries(retries: int) -> ShipEngineConfig: :raises: :class:`InvalidFieldValueError`: If invalid value is passed into `ShipEngineConfig` object at instantiation. """ - return ShipEngineConfig(dict(api_key="baz", retries=retries)) + return ShipEngineConfig(dict(api_key="baz_sim", retries=retries)) def complete_valid_config() -> ShipEngineConfig: @@ -83,8 +81,7 @@ def complete_valid_config() -> ShipEngineConfig: """ return ShipEngineConfig( dict( - api_key="baz", - base_uri=Endpoints.TEST_RPC_URL.value, + api_key="baz_sim", page_size=50, retries=2, timeout=10, @@ -99,8 +96,8 @@ def test_valid_custom_config(self): valid values for each attribute. """ valid_config: ShipEngineConfig = complete_valid_config() - assert valid_config.api_key == "baz" - assert valid_config.base_uri is Endpoints.TEST_RPC_URL.value + assert valid_config.api_key == "baz_sim" + assert valid_config.base_uri is Endpoints.SHIPENGINE_RPC_URL.value assert valid_config.page_size == 50 assert valid_config.retries == 2 assert valid_config.timeout == 10 @@ -127,7 +124,7 @@ def test_valid_retries(self): """Test case where a valid value is passed in for the retries.""" retries = 2 valid_retries = set_config_retries(retries) - assert valid_retries.api_key == "baz" + assert valid_retries.api_key == "baz_sim" assert valid_retries.retries == retries def test_invalid_retries_provided(self): @@ -195,7 +192,7 @@ def test_invalid_api_key_in_method_call(self): def test_config_defaults(self) -> None: """Test default retries.""" - config = ShipEngineConfig(dict(api_key="baz")) + config = ShipEngineConfig(dict(api_key="baz_sim")) assert config.retries == 1 assert config.page_size == 50 diff --git a/tests/util/test_helpers.py b/tests/util/test_helpers.py index 3eb7868..706155a 100644 --- a/tests/util/test_helpers.py +++ b/tests/util/test_helpers.py @@ -2,12 +2,16 @@ from typing import Dict, Optional, Union from shipengine_sdk import ShipEngine, ShipEngineConfig +from shipengine_sdk.errors import ShipEngineError from shipengine_sdk.models import ( Address, AddressValidateResult, - Endpoints, + ErrorCode, + ErrorSource, + ErrorType, TrackingQuery, ) +from shipengine_sdk.models.enums import Constants def stub_config( @@ -18,8 +22,7 @@ def stub_config( when instantiating the ShipEngine object. """ return dict( - api_key="baz", - base_uri=Endpoints.TEST_RPC_URL.value, + api_key=Constants.STUB_API_KEY.value, page_size=50, retries=retries, timeout=15, @@ -88,7 +91,7 @@ def address_with_warnings() -> Address: return Address( street=["170 Warning Blvd", "Apartment 32-B"], city_locality="Toronto", - state_province="ON", + state_province="On", postal_code="M6K 3C3", country_code="CA", ) @@ -121,7 +124,7 @@ def valid_canadian_address() -> Address: return Address( street=["170 Princes Blvd", "Ste 200"], city_locality="Toronto", - state_province="ON", + state_province="On", postal_code="M6K 3C3", country_code="CA", ) @@ -179,7 +182,7 @@ def unknown_address() -> Address: return Address( street=["4 Unknown St"], city_locality="Toronto", - state_province="ON", + state_province="On", postal_code="M6K 3C3", country_code="CA", ) @@ -240,6 +243,17 @@ def address_with_invalid_postal_code() -> Address: ) +def get_429_address() -> Address: + """Return an address that fetches a 429 fixture from the server.""" + return Address( + street=["429 Rate Limit Error"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + def get_server_side_error() -> Address: """Return an address that will cause the server to return a 500 server error.""" return Address( @@ -355,7 +369,7 @@ def canada_valid_avs_assertions( 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 == "M6 K 3 C3" + assert address.postal_code == "M6K 3C3" assert address.country_code == original_address.country_code.upper() assert address.is_residential is expected_residential_indicator @@ -389,6 +403,19 @@ def canada_valid_normalize_assertions( 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 == "M6 K 3 C3" + assert normalized_address.postal_code == "M6K 3C3" assert normalized_address.country_code == original_address.country_code.upper() assert normalized_address.is_residential is expected_residential_indicator + + +def assert_on_429_exception(err: ShipEngineError, error_class: object) -> None: + error = err.to_dict() + assert type(err) == error_class + assert error["request_id"] is not None + assert error["request_id"].startswith("req_") + assert error["source"] is ErrorSource.SHIPENGINE.value + assert error["error_type"] is ErrorType.SYSTEM.value + assert error["error_code"] is ErrorCode.RATE_LIMIT_EXCEEDED.value + assert error["message"] == "You have exceeded the rate limit." + assert error["url"] is not None + assert error["url"] == "https://www.shipengine.com/docs/rate-limits" diff --git a/tests/util/test_iso_string.py b/tests/util/test_iso_string.py index 63a723f..b204828 100644 --- a/tests/util/test_iso_string.py +++ b/tests/util/test_iso_string.py @@ -9,19 +9,14 @@ class TestIsoString: def test_to_string(self) -> None: iso_str = IsoString(self._test_iso_string_no_tz).to_string() - assert type(iso_str) is str def test_to_datetime_object(self) -> None: iso_str = IsoString(self._test_iso_string_no_tz).to_datetime_object() - assert type(iso_str) is datetime.datetime - def test_static_no_tz_check(self) -> None: - assert IsoString.is_valid_iso_string_no_tz(self._test_iso_string_no_tz) is True - def test_static_valid_iso_check(self) -> None: - assert IsoString.is_valid_iso_string(self._test_iso_string_no_tz) is True + assert IsoString.is_valid_iso_string_with_tz(self._test_iso_string_no_tz) is True def test_static_valid_iso_check_failure(self) -> None: - assert IsoString.is_valid_iso_string("2021-06-10T21:00:00.000K") is False + assert IsoString.is_valid_iso_string_with_tz("2021-06-10T21:00:00.000K") is False diff --git a/tox.ini b/tox.ini index 114a92e..f69bfac 100644 --- a/tox.ini +++ b/tox.ini @@ -30,10 +30,12 @@ changedir = tests deps = pytest pytest-cov + pytest-mock flake8 coverage coveralls responses + fuuid commands = pytest {posargs:} ; coveralls --submit={toxworkdir}/.coverage.{envname}