diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 040f20b3..7359b6c3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @scanapi/core-team @scanapi/maintaners +* @scanapi/core-team @scanapi/maintainers diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2a267d01..e967eea3 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ # These are supported funding model platforms -github: camilamaia +github: [scanapi, camilamaia] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d8b16c29..b0c12111 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: 🐛 Create a a bug report. -labels: bug, needs triage +labels: Bug, Needs Triage assignees: --- @@ -22,4 +22,4 @@ assignees: ### Anthing else we need to know? - \ No newline at end of file + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index c3b66a05..9d2c9ded 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: 🌟 Submit a feature request. -labels: enhancement, needs triage +labels: Enhancement, Needs Triage assignees: --- diff --git a/.github/ISSUE_TEMPLATE/how_to_question.md b/.github/ISSUE_TEMPLATE/how_to_question.md index d2bc3dd8..f6d04c9b 100644 --- a/.github/ISSUE_TEMPLATE/how_to_question.md +++ b/.github/ISSUE_TEMPLATE/how_to_question.md @@ -1,7 +1,7 @@ --- name: Question about: ❓ How to questions with scanapi -labels: question, support +labels: Question, Support assignees: --- @@ -19,4 +19,4 @@ assignees: --- -scanapi has a [discord server](https://discord.gg/f3cCXJ2ZPB) so feel free to join and say 👋 hello. Maybe share the issue link? \ No newline at end of file +scanapi has a [discord server](https://discord.gg/f3cCXJ2ZPB) so feel free to join and say 👋 hello. Maybe share the issue link? diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 85406411..c654d944 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -17,7 +17,8 @@ - [ ] Current PR does not significantly decrease the code coverage and docstring coverage. - [ ] My code follows the style guidelines of this project. - [ ] I have run ScanAPI locally and manually tested my changes. [Instructions](https://github.com/scanapi/scanapi/wiki/Run-ScanAPI-Locally). +- [ ] I have squashed my commits. [Instructions](https://github.com/scanapi/scanapi/wiki/Squashing-Commits). ## Issue -Closes +Closes # diff --git a/.github/workflows/first-interaction.yml b/.github/workflows/first-interaction.yml index 2e4f98a2..b42fb0c8 100644 --- a/.github/workflows/first-interaction.yml +++ b/.github/workflows/first-interaction.yml @@ -16,4 +16,4 @@ jobs: In the mean time, if you haven't had a chance please skim over the [First Pull Request Guide](https://github.com/scanapi/scanapi/wiki/First-Pull-Request) which all pull requests must adhere to. We hope to see you around! - pr-label: first contribution + pr-label: First Contribution diff --git a/.gitlint b/.gitlint new file mode 100644 index 00000000..03d26486 --- /dev/null +++ b/.gitlint @@ -0,0 +1,19 @@ +# Edit this file as you like. +# +# All these sections are optional. Each section with the exception of [general] represents +# one rule and each key in it is an option for that specific rule. +# +# Rules and sections can be referenced by their full name or by id. For example +# section "[body-max-line-length]" could also be written as "[B1]". Full section names are +# used in here for clarity. +# +[general] +ignore=body-is-missing +contrib=contrib-title-conventional-commits + +# This is a contrib rule - a community contributed rule. These are disabled by default. +# You need to explicitly enable them one-by-one by adding them to the "contrib" option +# under [general] section above. +# [contrib-title-conventional-commits] +# Specify allowed commit types. For details see: https://www.conventionalcommits.org/ +# types=bugfix,user-story,epic diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 00000000..1fcc6670 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,10 @@ +[mypy] +check_untyped_defs = true +disallow_incomplete_defs = true +ignore_missing_imports = true +show_error_codes = true +strict_equality = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f33a70e..a72bab81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,3 +16,8 @@ repos: hooks: - id: flake8 args: ["--ignore=E501,W501,E231,W503"] + - repo: https://github.com/jorisroovers/gitlint + # It must match the gitlint's version that is inside pyproject.toml + rev: v0.15.1 + hooks: + - id: gitlint diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea9de4b..01b26efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.6.0] - 2021-08-13 +### Changed +- Summary tests location to the top of the report [#479](https://github.com/scanapi/scanapi/pull/479) + +### Fixed +- Header table gets broken. [#432](https://github.com/scanapi/scanapi/pull/432) + ## [2.5.0] - 2021-07-23 ### Added - Enable 'vars' key at endpoint node. [#328](https://github.com/scanapi/scanapi/pull/328) @@ -219,7 +226,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix vars interpolation. -[Unreleased]: https://github.com/camilamaia/scanapi/compare/v2.5.0...HEAD +[Unreleased]: https://github.com/camilamaia/scanapi/compare/v2.6.0...HEAD +[2.6.0]: https://github.com/camilamaia/scanapi/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/camilamaia/scanapi/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/camilamaia/scanapi/compare/v2.3.0...v2.4.0 [2.3.0]: https://github.com/camilamaia/scanapi/compare/v2.2.0...v2.3.0 diff --git a/Dockerfile b/Dockerfile index 7ee89ff9..80b10624 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV PATH="~/.local/bin:${PATH}" RUN pip install pip setuptools --upgrade -RUN pip install scanapi==2.5.0 +RUN pip install scanapi==2.6.0 COPY . /app diff --git a/Makefile b/Makefile index 268515b2..3a5d0f35 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ -timestamp = `date +%s` - +timestamp = `date -u +'%Y%m%d%H%M%S'` test: @pytest --cov=./scanapi --cov-report=xml @@ -10,17 +9,20 @@ black: flake8: @poetry run flake8 --ignore=E501,W501,E231,W503 -check: black flake8 +mypy: + @poetry run mypy scanapi + +check: black flake8 mypy gitlint change-version: - @poetry version 2.1.0-$(timestamp) + @poetry version `poetry version -s | cut -f-3 -d.`.dev$(timestamp) format: @black -l 80 . --exclude=.venv install: @poetry install - @pre-commit install + @pre-commit install -f -t pre-commit --hook-type commit-msg sh: @poetry shell @@ -31,4 +33,7 @@ run: bandit: @bandit -r scanapi -.PHONY: test format check install sh run +gitlint: + @poetry run gitlint --ignore-stdin + +.PHONY: test black flake8 mypy check change-version format install sh run bandit gitlint diff --git a/README.md b/README.md index 87ce0406..db103ed9 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,10 @@ For example: ```yaml endpoints: - name: scanapi-demo # The API's name of your API - path: http://demo.scanapi.dev/api/ # The API's base url + path: http://demo.scanapi.dev/api/v1 # The API's base url requests: - - name: list_all_devs # The name of the first request - path: devs/ # The path of the first request + - name: list_all_users # The name of the first request + path: users/ # The path of the first request method: get # The HTTP method of the first request tests: - name: status_code_is_200 # The name of the first test for this request @@ -76,17 +76,17 @@ Then, the lib will hit the specified endpoints and generate a `scanapi-report.ht

An overview screenshot of the report. A screenshot of the report showing the request details. A screenshot of the report showing the response and test details @@ -102,13 +102,13 @@ You can find complete examples at [scanapi/examples][scanapi-examples]! This tutorial helps you to create integration tests for your REST API using ScanAPI -[![Watch the video](https://raw.githubusercontent.com/scanapi/scanapi/master/images/youtube-scanapi-tutorial.png)](https://www.youtube.com/watch?v=JIo4sA8LHco&t=2s) +[![Watch the video](https://raw.githubusercontent.com/scanapi/scanapi/main/images/youtube-scanapi-tutorial.png)](https://www.youtube.com/watch?v=JIo4sA8LHco&t=2s) ## Contributing Collaboration is super welcome! We prepared the [Newcomers Guide][newcomers-guide] to help you in the first steps. Every little bit of help counts! Feel free to create new [GitHub issues][github-issues] and interact here. -Let's build it together 🚀 +Let's build it together 🚀🚀 [github-issues]: https://github.com/scanapi/scanapi/issues [newcomers-guide]: https://github.com/scanapi/scanapi/wiki/Newcomers diff --git a/images/report-print-closed.png b/images/report-print-closed.png index 57f89ce9..f51ed604 100644 Binary files a/images/report-print-closed.png and b/images/report-print-closed.png differ diff --git a/images/report-print-request.png b/images/report-print-request.png index 65f66dbe..73ded489 100644 Binary files a/images/report-print-request.png and b/images/report-print-request.png differ diff --git a/images/report-print-response.png b/images/report-print-response.png index eb3d6b27..62a285fe 100644 Binary files a/images/report-print-response.png and b/images/report-print-response.png differ diff --git a/poetry.lock b/poetry.lock index 12feb22f..06abfd0b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,18 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "arrow" +version = "1.0.3" +description = "Better dates & times for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +python-dateutil = ">=2.7.0" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + [[package]] name = "atomicwrites" version = "1.4.0" @@ -115,7 +127,7 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.0.3" +version = "2.0.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -235,9 +247,22 @@ python-versions = ">=3.4" [package.dependencies] smmap = ">=3.0.1,<5" +[[package]] +name = "gitlint" +version = "0.15.1" +description = "Git commit message linter written in python, checks your commit messages for style." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +arrow = "1.0.3" +Click = "7.1.2" +sh = {version = "1.14.1", markers = "sys_platform != \"win32\""} + [[package]] name = "gitpython" -version = "3.1.18" +version = "3.1.20" description = "Python Git Library" category = "dev" optional = false @@ -245,11 +270,11 @@ python-versions = ">=3.6" [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} [[package]] name = "identify" -version = "2.2.11" +version = "2.2.13" description = "File identification library for Python" category = "dev" optional = false @@ -276,7 +301,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.1" +version = "4.6.3" description = "Read metadata from Python packages" category = "dev" optional = false @@ -293,7 +318,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [[package]] name = "importlib-resources" -version = "5.2.0" +version = "5.2.2" description = "Read resources from Python packages" category = "dev" optional = false @@ -316,7 +341,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.2" +version = "5.9.3" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -358,6 +383,32 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mypy" +version = "0.910" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +toml = "*" +typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "nodeenv" version = "1.6.0" @@ -395,11 +446,15 @@ python-versions = ">=2.6" [[package]] name = "platformdirs" -version = "2.0.2" +version = "2.2.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -574,7 +629,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" -version = "2021.7.6" +version = "2021.8.3" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -630,6 +685,14 @@ urllib3 = ">=1.25.10" [package.extras] tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)"] +[[package]] +name = "sh" +version = "1.14.1" +description = "Python subprocess replacement" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "six" version = "1.16.0" @@ -799,6 +862,30 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "types-pyyaml" +version = "5.4.6" +description = "Typing stubs for PyYAML" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-requests" +version = "2.25.6" +description = "Typing stubs for requests" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-setuptools" +version = "57.0.2" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "3.10.0.0" @@ -822,7 +909,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.6.0" +version = "20.7.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -839,7 +926,7 @@ six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] [[package]] name = "zipp" @@ -856,7 +943,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.6.2,<4.0.0" -content-hash = "636f913d2e4ecc1ce29a40d580074a13b2d50f9c6632c3685a44f955cb7625e2" +content-hash = "75b70e5d257e3e40fc434186daccaada6bf326858febb68aa65eead082aa950a" [metadata.files] alabaster = [ @@ -867,6 +954,10 @@ appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +arrow = [ + {file = "arrow-1.0.3-py3-none-any.whl", hash = "sha256:3515630f11a15c61dcb4cdd245883270dd334c83f3e639824e65a4b79cc48543"}, + {file = "arrow-1.0.3.tar.gz", hash = "sha256:399c9c8ae732270e1aa58ead835a79a40d7be8aa109c579898eb41029b5a231d"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -900,8 +991,8 @@ cfgv = [ {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"}, - {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"}, + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -998,13 +1089,17 @@ gitdb = [ {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] +gitlint = [ + {file = "gitlint-0.15.1-py2.py3-none-any.whl", hash = "sha256:7ebdb8e7d333e577e956225cbc3ad8e0e96d05e638e6d461c9b66b784f9d2ac4"}, + {file = "gitlint-0.15.1.tar.gz", hash = "sha256:4b22916dcbdca381244aee6cb8d8743756cfd98f27e7d1f02e78733f07c3c21c"}, +] gitpython = [ - {file = "GitPython-3.1.18-py3-none-any.whl", hash = "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"}, - {file = "GitPython-3.1.18.tar.gz", hash = "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b"}, + {file = "GitPython-3.1.20-py3-none-any.whl", hash = "sha256:b1e1c269deab1b08ce65403cf14e10d2ef1f6c89e33ea7c5e5bb0222ea593b8a"}, + {file = "GitPython-3.1.20.tar.gz", hash = "sha256:df0e072a200703a65387b0cfdf0466e3bab729c0458cf6b7349d0e9877636519"}, ] identify = [ - {file = "identify-2.2.11-py2.py3-none-any.whl", hash = "sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839"}, - {file = "identify-2.2.11.tar.gz", hash = "sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a"}, + {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, + {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -1015,20 +1110,20 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, - {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, + {file = "importlib_metadata-4.6.3-py3-none-any.whl", hash = "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b"}, + {file = "importlib_metadata-4.6.3.tar.gz", hash = "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9"}, ] importlib-resources = [ - {file = "importlib_resources-5.2.0-py3-none-any.whl", hash = "sha256:a0143290bef3cbc99de9e40176e4987780939a955b8632f02ce6c935f42e9bfc"}, - {file = "importlib_resources-5.2.0.tar.gz", hash = "sha256:22a2c42d8c6a1d30aa8a0e1f57293725bfd5c013d562585e46aff469e0ff78b3"}, + {file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"}, + {file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"}, ] 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.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"}, - {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"}, + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] jinja2 = [ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, @@ -1074,6 +1169,35 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +mypy = [ + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, @@ -1091,8 +1215,8 @@ pbr = [ {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, ] platformdirs = [ - {file = "platformdirs-2.0.2-py2.py3-none-any.whl", hash = "sha256:0b9547541f599d3d242078ae60b927b3e453f0ad52f58b4d4bc3be86aed3ec41"}, - {file = "platformdirs-2.0.2.tar.gz", hash = "sha256:3b00d081227d9037bbbca521a5787796b5ef5000faea1e43fd76f1d44b06fcfa"}, + {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, + {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -1173,47 +1297,39 @@ pyyaml = [ {file = "PyYAML-5.4.tar.gz", hash = "sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a"}, ] regex = [ - {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, - {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, - {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, - {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, - {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, - {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, - {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, - {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, - {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, - {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, - {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, - {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, - {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, + {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, + {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, + {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, + {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, + {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, + {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, + {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, + {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, + {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, + {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, + {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, + {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, + {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, @@ -1227,6 +1343,10 @@ responses = [ {file = "responses-0.12.1-py2.py3-none-any.whl", hash = "sha256:ef265bd3200bdef5ec17912fc64a23570ba23597fd54ca75c18650fa1699213d"}, {file = "responses-0.12.1.tar.gz", hash = "sha256:2e5764325c6b624e42b428688f2111fea166af46623cb0127c05f6afb14d3457"}, ] +sh = [ + {file = "sh-1.14.1-py2.py3-none-any.whl", hash = "sha256:75e86a836f47de095d4531718fe8489e6f7446c75ddfa5596f632727b919ffae"}, + {file = "sh-1.14.1.tar.gz", hash = "sha256:39aa9af22f6558a0c5d132881cf43e34828ca03e4ae11114852ca6a55c7c1d8e"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1311,6 +1431,18 @@ typed-ast = [ {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] +types-pyyaml = [ + {file = "types-PyYAML-5.4.6.tar.gz", hash = "sha256:745dcb4b1522423026bcc83abb9925fba747f1e8602d902f71a4058f9e7fb662"}, + {file = "types_PyYAML-5.4.6-py3-none-any.whl", hash = "sha256:96f8d3d96aa1a18a465e8f6a220e02cff2f52632314845a364ecbacb0aea6e30"}, +] +types-requests = [ + {file = "types-requests-2.25.6.tar.gz", hash = "sha256:e21541c0f55c066c491a639309159556dd8c5833e49fcde929c4c47bdb0002ee"}, + {file = "types_requests-2.25.6-py3-none-any.whl", hash = "sha256:a5a305b43ea57bf64d6731f89816946a405b591eff6de28d4c0fd58422cee779"}, +] +types-setuptools = [ + {file = "types-setuptools-57.0.2.tar.gz", hash = "sha256:4ead432cc394b8de5ee94a312494f3c730d21dbfef37f300a6ac5e742271d735"}, + {file = "types_setuptools-57.0.2-py3-none-any.whl", hash = "sha256:62c8dc465f29eeaad6b9025645138738d3df1cafc70cb0b14aea700946f2cbb7"}, +] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, @@ -1321,8 +1453,8 @@ urllib3 = [ {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] virtualenv = [ - {file = "virtualenv-20.6.0-py2.py3-none-any.whl", hash = "sha256:e4fc84337dce37ba34ef520bf2d4392b392999dbe47df992870dc23230f6b758"}, - {file = "virtualenv-20.6.0.tar.gz", hash = "sha256:51df5d8a2fad5d1b13e088ff38a433475768ff61f202356bb9812c454c20ae45"}, + {file = "virtualenv-20.7.1-py2.py3-none-any.whl", hash = "sha256:73863dc3be1efe6ee638e77495c0c195a6384ae7b15c561f3ceb2698ae7267c1"}, + {file = "virtualenv-20.7.1.tar.gz", hash = "sha256:57bcb59c5898818bd555b1e0cfcf668bd6204bc2b53ad0e70a52413bd790f9e4"}, ] zipp = [ {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, diff --git a/pyproject.toml b/pyproject.toml index 60b5ae9e..cd796c79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "scanapi" -version = "2.5.0" +version = "2.6.0" description = "Automated Testing and Documentation for your REST API" authors = ["Camila Maia "] license = "MIT" @@ -37,6 +37,11 @@ isort = "^5.5.3" bandit = "^1.6.2" pytest-it = "^0.1.4" flake8 = "3.8.4" +mypy = "^0.910" +types-requests = "^2.25.1" +types-PyYAML = "^5.4.3" +types-setuptools = "^57.0.0" +gitlint = "0.15.1" [tool.isort] multi_line_output = 3 diff --git a/scanapi/__main__.py b/scanapi/__main__.py index d1987970..5f48e48a 100644 --- a/scanapi/__main__.py +++ b/scanapi/__main__.py @@ -16,9 +16,7 @@ @click.group() @click.version_option(version=dist.version) def main(): - """ - Automated Testing and Documentation for your REST API. - """ + """Automated Testing and Documentation for your REST API.""" pass diff --git a/scanapi/config_loader.py b/scanapi/config_loader.py index 23c3b5b7..4c63a08a 100644 --- a/scanapi/config_loader.py +++ b/scanapi/config_loader.py @@ -1,6 +1,4 @@ -""" -Code based on solution https://gist.github.com/joshbode/569627ced3076931b02f -""" +"""Code Reference https://gist.github.com/joshbode/569627ced3076931b02f""" import logging import os @@ -8,7 +6,7 @@ import yaml -from scanapi.errors import EmptyConfigFileError +from scanapi.errors import BadConfigIncludeError, EmptyConfigFileError logger = logging.getLogger(__name__) @@ -28,10 +26,15 @@ def __init__(self, stream: IO) -> None: def construct_include(loader: Loader, node: yaml.Node) -> Any: """Include file referenced at node.""" - relative_path = os.path.join(loader.root, loader.construct_scalar(node)) + if not isinstance(node, yaml.ScalarNode): + include_node_str = yaml.serialize(node).strip() + message = f"Include tag value is not a scalar: {include_node_str}" + raise BadConfigIncludeError(message) + include_file_path = str(loader.construct_scalar(node)) + relative_path = os.path.join(loader.root, include_file_path) full_path = os.path.abspath(relative_path) - with open(full_path, "r") as f: + with open(full_path) as f: return yaml.load(f, Loader) @@ -50,4 +53,7 @@ def load_config_file(file_path): return data +# Ignore parameter types due to wrong annotations in `typeshed`. Fix submitted +# in https://github.com/python/typeshed/pull/5828 +# Can remove "type: ignore" once `types-PyYAML` has been updated. yaml.add_constructor("!include", construct_include, Loader) diff --git a/scanapi/errors.py b/scanapi/errors.py index cee7a7e4..b346ae83 100644 --- a/scanapi/errors.py +++ b/scanapi/errors.py @@ -1,4 +1,9 @@ class MalformedSpecError(Exception): + """Raised when API spec is invalid; + + base class for other exceptions + """ + pass @@ -7,7 +12,7 @@ class HTTPMethodNotAllowedError(MalformedSpecError): def __init__(self, method, allowed_methods, *args): message = f"HTTP method not supported: {method}. Supported methods: {allowed_methods}." - super(HTTPMethodNotAllowedError, self).__init__(message, *args) + super().__init__(message, *args) class InvalidKeyError(MalformedSpecError): @@ -15,7 +20,7 @@ class InvalidKeyError(MalformedSpecError): def __init__(self, key, scope, available_keys, *args): message = f"Invalid key '{key}' at '{scope}' scope. Available keys are: {available_keys}" - super(InvalidKeyError, self).__init__(message, *args) + super().__init__(message, *args) class MissingMandatoryKeyError(MalformedSpecError): @@ -24,7 +29,7 @@ class MissingMandatoryKeyError(MalformedSpecError): def __init__(self, missing_keys, scope, *args): missing_keys_str = ", ".join(f"'{k}'" for k in sorted(missing_keys)) message = f"Missing {missing_keys_str} key(s) at '{scope}' scope" - super(MissingMandatoryKeyError, self).__init__(message, *args) + super().__init__(message, *args) class InvalidPythonCodeError(MalformedSpecError): @@ -36,14 +41,14 @@ def __init__(self, error_message, code, *args): f"Exception: {error_message}. " f"Code: {code}." ) - super(InvalidPythonCodeError, self).__init__(error_message, *args) + super().__init__(error_message, *args) class BadConfigurationError(Exception): - """Raised when an environment variable was not set or was badly configured""" + """Raised when an environment variable was not set or badly configured.""" def __init__(self, env_var, *args): - super(BadConfigurationError, self).__init__( + super().__init__( f"{env_var} environment variable not set or badly configured", *args, ) @@ -54,4 +59,8 @@ class EmptyConfigFileError(Exception): def __init__(self, file_path, *args): message = f"File '{file_path}' is empty." - super(EmptyConfigFileError, self).__init__(message, *args) + super().__init__(message, *args) + + +class BadConfigIncludeError(Exception): + """Raised when the value of the !include yaml tag is not a scalar.""" diff --git a/scanapi/evaluators/code_evaluator.py b/scanapi/evaluators/code_evaluator.py index 03c4cad2..f90c6a6e 100644 --- a/scanapi/evaluators/code_evaluator.py +++ b/scanapi/evaluators/code_evaluator.py @@ -18,14 +18,33 @@ class CodeEvaluator: ) # ${{}} @classmethod - def evaluate(cls, sequence, vars, is_a_test_case=False): + def evaluate(cls, sequence, spec_vars, is_a_test_case=False): + """Receives a sequence of characters and evaluates any python code + present on it + + Args: + sequence[string]: sequence of characters to be evaluated + spec_vars[dict]: dictionary containing the SpecEvaluator variables + is_a_test_case[bool]: indicator for checking if the given evalution + is a test case + + Returns: + tuple: a tuple containing: + - [Boolean]: True if python statement is valid + - [string]: None if valid evalution, tested code otherwise + + Raises: + InvalidPythonCodeError: If receives invalid python statements + (eg. 1/0) + + """ match = cls.python_code_pattern.search(str(sequence)) if not match: return sequence code = match.group("python_code") - response = vars.get("response") + response = spec_vars.get("response") try: if is_a_test_case: @@ -37,23 +56,25 @@ def evaluate(cls, sequence, vars, is_a_test_case=False): @classmethod def _assert_code(cls, code, response): - """Assert a Python code statement + """Assert a Python code statement. Args: - code [string]: python code that ScanAPI needs to assert - response [requests.Response]: the response for the current request + code[string]: python code that ScanAPI needs to assert + response[requests.Response]: the response for the current request that is being tested Returns: - (boolean, string): a boolean that indicates if assert is True/False - and, if False, the code tested. + tuple: a tuple containing: + - [Boolean]: a boolean that indicates if assert + is True/False + - [string]: None if valid evalution, code tested otherwise + + Raises: + AssertionError: If python statement evaluates False """ - try: - assert eval(code) - return (True, None) - except AssertionError: - return (False, code.strip()) + ok = eval(code) # noqa + return ok, None if ok else code.strip() @classmethod def _evaluate_sequence(cls, sequence, match, code, response): diff --git a/scanapi/evaluators/spec_evaluator.py b/scanapi/evaluators/spec_evaluator.py index 22a0f9b6..c54e6392 100644 --- a/scanapi/evaluators/spec_evaluator.py +++ b/scanapi/evaluators/spec_evaluator.py @@ -8,10 +8,10 @@ class SpecEvaluator: - def __init__(self, endpoint, vars_, extras=None, filter_responses=True): + def __init__(self, endpoint, spec_vars, extras=None, filter_responses=True): self.endpoint = endpoint self.registry = {} - self.update(vars_, extras=extras, filter_responses=filter_responses) + self.update(spec_vars, extras=extras, filter_responses=filter_responses) def evaluate(self, element): return evaluate(element, self) @@ -19,14 +19,16 @@ def evaluate(self, element): def evaluate_assertion(self, element): return _evaluate_str(element, self, is_a_test_case=True) - def update(self, vars, extras=None, filter_responses=False): + def update(self, spec_vars, extras=None, filter_responses=False): if extras is None: extras = {} if filter_responses: - vars = self.filter_response_var(vars) + spec_vars = self.filter_response_var(spec_vars) - values = {key: evaluate(value, extras) for key, value in vars.items()} + values = { + key: evaluate(value, extras) for key, value in spec_vars.items() + } self.registry.update(extras) self.registry.update(values) @@ -43,8 +45,8 @@ def __getitem__(self, key): if key in self: return self.registry[key] - if key in self.endpoint.parent.vars: - return self.endpoint.parent.vars[key] + if key in self.endpoint.parent.spec_vars: + return self.endpoint.parent.spec_vars[key] raise KeyError(key) @@ -66,32 +68,35 @@ def keys(self): return self.registry.keys() @classmethod - def filter_response_var(cls, vars_): - """Returns a dictionary without vars that evaluate response. + def filter_response_var(cls, spec_vars): + """Returns a copy pf ``spec_vars`` without 'response' references. + + Any items with a ``response.*`` reference in their value are left out. + Returns: [dict]: filtered dictionary. """ pattern = re.compile(r"(?:(\s*response\.\w+))") - return {k: v for k, v in vars_.items() if not pattern.search(v)} + return {k: v for k, v in spec_vars.items() if not pattern.search(v)} @singledispatch -def evaluate(expression, vars): +def evaluate(expression, spec_vars): return expression @evaluate.register(str) -def _evaluate_str(element, vars, is_a_test_case=False): - return StringEvaluator.evaluate(element, vars, is_a_test_case) +def _evaluate_str(element, spec_vars, is_a_test_case=False): + return StringEvaluator.evaluate(element, spec_vars, is_a_test_case) @evaluate.register(dict) -def _evaluate_dict(element, vars): - return {key: evaluate(value, vars) for key, value in element.items()} +def _evaluate_dict(element, spec_vars): + return {key: evaluate(value, spec_vars) for key, value in element.items()} @evaluate.register(list) @evaluate.register(tuple) -def _evaluate_collection(elements, vars): - return [evaluate(item, vars) for item in elements] +def _evaluate_collection(elements, spec_vars): + return [evaluate(item, spec_vars) for item in elements] diff --git a/scanapi/evaluators/string_evaluator.py b/scanapi/evaluators/string_evaluator.py index fa971e71..b47649a1 100644 --- a/scanapi/evaluators/string_evaluator.py +++ b/scanapi/evaluators/string_evaluator.py @@ -14,19 +14,16 @@ class StringEvaluator: ) # ${} @classmethod - def evaluate(cls, sequence, vars, is_a_test_case=False): + def evaluate(cls, sequence, spec_vars, is_a_test_case=False): sequence = cls._evaluate_env_var(sequence) - sequence = cls._evaluate_custom_var(sequence, vars) + sequence = cls._evaluate_custom_var(sequence, spec_vars) - return CodeEvaluator.evaluate(sequence, vars, is_a_test_case) + return CodeEvaluator.evaluate(sequence, spec_vars, is_a_test_case) @classmethod def _evaluate_env_var(cls, sequence): matches = cls.variable_pattern.finditer(sequence) - if not matches: - return sequence - for match in matches: variable_name = match.group("variable") @@ -45,22 +42,19 @@ def _evaluate_env_var(cls, sequence): return sequence @classmethod - def _evaluate_custom_var(cls, sequence, vars): + def _evaluate_custom_var(cls, sequence, spec_vars): matches = cls.variable_pattern.finditer(sequence) - if not matches: - return sequence - for match in matches: variable_name = match.group("variable") if variable_name.isupper(): continue - if not vars.get(variable_name): + if not spec_vars.get(variable_name): continue - variable_value = vars.get(variable_name) + variable_value = spec_vars.get(variable_name) sequence = cls.replace_var_with_value( sequence, match.group(), variable_value diff --git a/scanapi/exit_code.py b/scanapi/exit_code.py index 8d681325..9d37bd99 100644 --- a/scanapi/exit_code.py +++ b/scanapi/exit_code.py @@ -1,15 +1,11 @@ -""" -Code based on solution https://github.com/pytest-dev/pytest/blob/\ -83891d9022076375cede03bfd8c932d450e6fcf8/src/_pytest/config/__init__.py#L67 -""" +"""Code based on solution: https://github.com/pytest-dev/pytest/blob/\ +83891d9022076375cede03bfd8c932d450e6fcf8/src/_pytest/config/__init__.py#L67""" import enum class ExitCode(enum.IntEnum): - """ - Encodes the valid exit codes by ScanAPI. - """ + """Encodes the valid exit codes by ScanAPI.""" #: tests passed OK = 0 diff --git a/scanapi/py.typed b/scanapi/py.typed new file mode 100644 index 00000000..1242d432 --- /dev/null +++ b/scanapi/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. diff --git a/scanapi/reporter.py b/scanapi/reporter.py index 8b843574..13f3f88c 100644 --- a/scanapi/reporter.py +++ b/scanapi/reporter.py @@ -14,6 +14,14 @@ class Reporter: + """Class that writes the scan report + + Attributes: + output_path[str, optional]: Report output path + template[str, optional]: Custom report template path + + """ + def __init__(self, output_path=None, template=None): """Creates a Reporter instance object.""" self.output_path = output_path or "scanapi-report.html" @@ -33,7 +41,7 @@ def write(self, results): logger.info("Writing documentation") template_path = self.template if self.template else "report.html" - has_external_template = True if self.template else False + has_external_template = bool(self.template) context = self._build_context(results) content = render(template_path, context, has_external_template) diff --git a/scanapi/settings.py b/scanapi/settings.py index 3f3ee969..0785bc4c 100644 --- a/scanapi/settings.py +++ b/scanapi/settings.py @@ -24,7 +24,7 @@ def __init__(self): self["template"] = None self["no_report"] = False - super(dict, self).__init__() + super().__init__() def save_config_file_preferences(self, config_path=None): """Saves the Settings object config file preferences.""" diff --git a/scanapi/template_render.py b/scanapi/template_render.py index c88b5f0b..ed0d9346 100644 --- a/scanapi/template_render.py +++ b/scanapi/template_render.py @@ -28,5 +28,5 @@ def render_body(request): content_type = request.headers.get("Content-Type") if content_type in ["application/json", "text/plain"]: return request.body.decode() - elif content_type.startswith("application"): + if content_type.startswith("application"): return "Binary content" diff --git a/scanapi/templates/report.html b/scanapi/templates/report.html index e19a8f39..7c79ccb0 100644 --- a/scanapi/templates/report.html +++ b/scanapi/templates/report.html @@ -91,25 +91,26 @@ /* END -- Wrapper */ /* Title */ - .title { + .summary__title { border-bottom: 8px solid #eeeeee; margin-bottom: 20px; padding: 20px; + order: 0; } - .title__name { + .summary__title__name { margin: 0 0 10px 0; padding: 0; font-size: 36px; font-weight: 300; } - .title__project { + .summary__title__project { font-weight: bold; color: #F67164; } - .title__overview { + .summary__title__overview { padding: 0; margin: 0; font-size: 16px; @@ -159,6 +160,10 @@ margin: auto; } + .wordbreak { + word-break: break-all; + } + @media only screen and (max-width: 767px) { .wrapper { width: auto; @@ -169,6 +174,10 @@ /* END -- HEADER */ /* Endpoint */ + + .summary__endpoint { + order: 2; + } .endpoint { overflow: hidden; margin-bottom: 20px; @@ -307,31 +316,32 @@ /* END -- ENDPOINT */ /* TESTS SUMMARY */ - .tests__summary { + .summary__tests { background: #f9f9f9; padding: 10px 12px; - margin: 0 20px; + margin: 20px; display: flex; flex-direction: column; border-radius: 10px; border: solid; border-width: 1px; + order: 1; } - .tests__summary--passed { + .summary__tests--passed { border-color: green; } - .tests__summary--failed { + .summary__tests--failed { border-color: red; } - .tests__summaryTitle { + .summary__tests-title { text-align: center; margin-bottom: 30px; } - .tests__summaryResults { + .summary__tests-results { margin-bottom: 21px; display: flex; justify-content: space-between; @@ -398,128 +408,57 @@ color: white; } /* END SNIPPET CONTENT */ + + .summary { + display: flex; + flex-direction: column; + }

-
-
+
+
{% if project_name %} -

Report generated for {{ project_name }}

+

Report generated for {{ project_name }}

{% else %} -

Report generated for your API

+

Report generated for your API

{% endif %} -

Generated at: {{now}}

+

Generated at: {{now}}

- - {% for result in results -%} - {% set response = result["response"] %} - {% set tests = result["tests_results"] %} - {% set endpoint_status_label = "P" if result["no_failure"] else "F" %} - {% set request = response.request %} - -
- -
- {% endfor %} {% set final_status = "passed" if session.succeed else "failed" %} - -
-

Tests Summary

-
+
+

Tests Summary

+
PASSED: {{session.successes}} FAILURES: {{session.failures}} ERRORS: {{session.errors}} Total Time: {{ session.elapsed_time() }}
+
-
- Generated by ScanAPI {{ scanapi_version }} -
- - - - + - + + } + openAnchoredEndpoint(); + diff --git a/scanapi/tree/endpoint_node.py b/scanapi/tree/endpoint_node.py index 0986b764..33af79f9 100644 --- a/scanapi/tree/endpoint_node.py +++ b/scanapi/tree/endpoint_node.py @@ -41,7 +41,7 @@ def __init__(self, spec, parent=None): self.parent = parent self.child_nodes = [] self.__build() - self.vars = SpecEvaluator(self, spec.get(VARS_KEY, {})) + self.spec_vars = SpecEvaluator(self, spec.get(VARS_KEY, {})) def __build(self): self._validate() @@ -68,7 +68,7 @@ def path(self): path = str(self.spec.get(PATH_KEY, "")).strip() url = join_urls(self.parent.path, path) if self.parent else path - return self.vars.evaluate(url) + return self.spec_vars.evaluate(url) @property def headers(self): diff --git a/scanapi/tree/request_node.py b/scanapi/tree/request_node.py index af4cf5af..64e586e7 100644 --- a/scanapi/tree/request_node.py +++ b/scanapi/tree/request_node.py @@ -77,21 +77,21 @@ def full_url_path(self): path = str(self.spec.get(PATH_KEY, "")) full_url = join_urls(base_path, path) - return self.endpoint.vars.evaluate(full_url) + return self.endpoint.spec_vars.evaluate(full_url) @property def headers(self): endpoint_headers = self.endpoint.headers headers = self.spec.get(HEADERS_KEY, {}) - return self.endpoint.vars.evaluate({**endpoint_headers, **headers}) + return self.endpoint.spec_vars.evaluate({**endpoint_headers, **headers}) @property def params(self): endpoint_params = self.endpoint.params params = self.spec.get(PARAMS_KEY, {}) - return self.endpoint.vars.evaluate({**endpoint_params, **params}) + return self.endpoint.spec_vars.evaluate({**endpoint_params, **params}) @property def delay(self): @@ -102,7 +102,7 @@ def delay(self): def body(self): body = self.spec.get(BODY_KEY) - return self.endpoint.vars.evaluate(body) + return self.endpoint.spec_vars.evaluate(body) @property def tests(self): @@ -128,9 +128,9 @@ def run(self): url = self.full_url_path logger.info("Making request %s %s", method, url) - self.endpoint.vars.update( + self.endpoint.spec_vars.update( self.spec.get(VARS_KEY, {}), - extras=dict(self.endpoint.vars), + extras=dict(self.endpoint.spec_vars), filter_responses=True, ) @@ -144,17 +144,17 @@ def run(self): allow_redirects=False, ) - extras = dict(self.endpoint.vars) + extras = dict(self.endpoint.spec_vars) extras["response"] = response - self.endpoint.vars.update( + self.endpoint.spec_vars.update( self.spec.get(VARS_KEY, {}), extras=extras, ) tests_results = self._run_tests() hide_sensitive_info(response) - del self.endpoint.vars["response"] + del self.endpoint.spec_vars["response"] return { "response": response, diff --git a/scanapi/tree/testing_node.py b/scanapi/tree/testing_node.py index 16bc80f9..9aebabaf 100644 --- a/scanapi/tree/testing_node.py +++ b/scanapi/tree/testing_node.py @@ -36,7 +36,10 @@ def full_name(self): def run(self): try: - passed, failure = self.request.endpoint.vars.evaluate_assertion( + ( + passed, + failure, + ) = self.request.endpoint.spec_vars.evaluate_assertion( self.assertion ) diff --git a/tests/data/api_non_scalar_include.yaml b/tests/data/api_non_scalar_include.yaml new file mode 100644 index 00000000..b3fe8281 --- /dev/null +++ b/tests/data/api_non_scalar_include.yaml @@ -0,0 +1,4 @@ +endpoints: + - name: scanapi-demo + path: ${BASE_URL} + requests: !include {a: "b"} diff --git a/tests/unit/evaluators/test_code_evaluator.py b/tests/unit/evaluators/test_code_evaluator.py index 8202cb11..b40498ee 100644 --- a/tests/unit/evaluators/test_code_evaluator.py +++ b/tests/unit/evaluators/test_code_evaluator.py @@ -70,12 +70,12 @@ def test_should_return_assert_results_2(self, sequence, expected, response): @mark.context("when it is a test case") @mark.context("when code breaks") @mark.it("should raises invalid python code error") - @mark.parametrize("sequence, vars_, is_a_test_case", test_data) + @mark.parametrize("sequence, spec_vars, is_a_test_case", test_data) def test_should_raises_invalid_python_code_error( - self, sequence, vars_, is_a_test_case + self, sequence, spec_vars, is_a_test_case ): with raises(InvalidPythonCodeError) as excinfo: - CodeEvaluator.evaluate(sequence, vars_, is_a_test_case) + CodeEvaluator.evaluate(sequence, spec_vars, is_a_test_case) assert isinstance(excinfo.value, InvalidPythonCodeError) diff --git a/tests/unit/evaluators/test_spec_evaluator.py b/tests/unit/evaluators/test_spec_evaluator.py index 88d73236..09a2d65a 100644 --- a/tests/unit/evaluators/test_spec_evaluator.py +++ b/tests/unit/evaluators/test_spec_evaluator.py @@ -77,8 +77,8 @@ class TestFilterResponseVar: def test_should_return_dictionary_without_response_var( self, spec_evaluator ): - vars_ = {"var_1": "${{response.json()['key']}}", "var_2": "foo"} - assert spec_evaluator.filter_response_var(vars_) == {"var_2": "foo"} + spec_vars = {"var_1": "${{response.json()['key']}}", "var_2": "foo"} + assert spec_evaluator.filter_response_var(spec_vars) == {"var_2": "foo"} @mark.describe("spec evaluator") diff --git a/tests/unit/evaluators/test_string_evaluator.py b/tests/unit/evaluators/test_string_evaluator.py index 79fc9bff..c6380c14 100644 --- a/tests/unit/evaluators/test_string_evaluator.py +++ b/tests/unit/evaluators/test_string_evaluator.py @@ -135,12 +135,15 @@ def test_should_return_sequence_2(self, sequence): @mark.it("should return sequence with evaluated custom var") @mark.parametrize("sequence, expected", test_data) def test_should_return_sequence_3(self, sequence, expected): - vars = { + spec_vars = { "user_id": "10", "apiKey": "abc123", "api-token": "xwo", } - assert StringEvaluator._evaluate_custom_var(sequence, vars) == expected + assert ( + StringEvaluator._evaluate_custom_var(sequence, spec_vars) + == expected + ) @mark.describe("string evaluator") diff --git a/tests/unit/test_config_loader.py b/tests/unit/test_config_loader.py index e82e6873..a8e8152f 100644 --- a/tests/unit/test_config_loader.py +++ b/tests/unit/test_config_loader.py @@ -1,7 +1,7 @@ from pytest import mark, raises from scanapi.config_loader import load_config_file -from scanapi.errors import EmptyConfigFileError +from scanapi.errors import BadConfigIncludeError, EmptyConfigFileError @mark.describe("config loader") @@ -64,3 +64,11 @@ def test_should_raise_exception_3(self): assert "[Errno 2] No such file or directory: " in str(excinfo.value) assert "tests/data/invalid_path/include.yaml'" in str(excinfo.value) + + @mark.context("include value is not a scalar") + @mark.it("should raise an exception") + def test_should_raise_exception_4(self): + with raises(BadConfigIncludeError) as excinfo: + load_config_file("tests/data/api_non_scalar_include.yaml") + + assert "Include tag value is not a scalar" in str(excinfo.value) diff --git a/tests/unit/tree/endpoint_node/test_get_specs.py b/tests/unit/tree/endpoint_node/test_get_specs.py index 726a49ee..5f6d6ea9 100644 --- a/tests/unit/tree/endpoint_node/test_get_specs.py +++ b/tests/unit/tree/endpoint_node/test_get_specs.py @@ -1,7 +1,79 @@ from pytest import mark +from scanapi.tree import EndpointNode + @mark.describe("endpoint node") @mark.describe("_get_specs") class TestGetSpecs: - pass # TODO + @mark.context("when both parent and child have the requested spec") + @mark.it("should return parent and child specs") + def test_when_parent_and_child_have_same_spec(self): + spec = { + "headers": {"child_foo": "child_bar"}, + "name": "node", + "requests": [], + } + + parent = EndpointNode( + { + "headers": {"parent_foo": "parent_bar"}, + "name": "node", + "requests": [], + } + ) + + node = EndpointNode(spec, parent,) + + specs = node._get_specs("headers") + + assert specs == {"parent_foo": "parent_bar", "child_foo": "child_bar"} + + @mark.context("when parent has the requested spec but child does not") + @mark.it("should return parent spec") + def test_when_parent_has_spec(self): + spec = {"name": "node", "requests": []} + + parent = EndpointNode( + { + "headers": {"parent_foo": "parent_bar"}, + "name": "node", + "requests": [], + } + ) + + node = EndpointNode(spec, parent,) + + specs = node._get_specs("headers") + + assert specs == {"parent_foo": "parent_bar"} + + @mark.context("when child has the requested spec but parent does not") + @mark.it("should return child spec") + def test_when_child_has_spec(self): + spec = { + "headers": {"child_foo": "child_bar"}, + "name": "node", + "requests": [], + } + + parent = EndpointNode({"name": "node", "requests": []}) + + node = EndpointNode(spec, parent,) + + specs = node._get_specs("headers") + + assert specs == {"child_foo": "child_bar"} + + @mark.context("when both parent and child do not have the requested spec") + @mark.it("should return an empty dictionary") + def test_when_there_is_no_spec(self): + spec = {"name": "node", "requests": []} + + parent = EndpointNode({"name": "node", "requests": []}) + + node = EndpointNode(spec, parent,) + + specs = node._get_specs("headers") + + assert specs == {} diff --git a/tests/unit/tree/endpoint_node/test_repr.py b/tests/unit/tree/endpoint_node/test_repr.py new file mode 100644 index 00000000..228591ed --- /dev/null +++ b/tests/unit/tree/endpoint_node/test_repr.py @@ -0,0 +1,20 @@ +from pytest import mark + +from scanapi.tree import EndpointNode + + +@mark.describe("endpoint node") +@mark.describe("__repr__") +class TestRepr: + @mark.context("when parent spec has no name defined") + @mark.it("should return ") + def test_when_parent_has_no_name(self): + node = EndpointNode({"name": "child-node"}, parent=EndpointNode({})) + assert repr(node) == "" + + @mark.context("when parent spec has a name defined") + @mark.it("should return ") + def test_when_parent_has_name(self): + parent = EndpointNode({"name": "root"}) + node = EndpointNode({"name": "child-node"}, parent=parent) + assert repr(node) == "" diff --git a/tests/unit/tree/endpoint_node/test_run.py b/tests/unit/tree/endpoint_node/test_run.py index a43602b1..535424ba 100644 --- a/tests/unit/tree/endpoint_node/test_run.py +++ b/tests/unit/tree/endpoint_node/test_run.py @@ -1,7 +1,99 @@ -from pytest import mark +import logging + +from pytest import fixture, mark + +from scanapi.exit_code import ExitCode +from scanapi.tree import EndpointNode + +log = logging.getLogger(__name__) @mark.describe("endpoint node") @mark.describe("run") class TestRun: - pass # TODO + @fixture + def mock_run_request(self, mocker): + return mocker.patch("scanapi.tree.request_node.RequestNode.run") + + @fixture + def mock_session(self, mocker): + return mocker.patch("scanapi.tree.endpoint_node.session") + + @mark.context("when requests are successful") + @mark.it("should return the responses of the requests") + def test_when_requests_are_successful(self, mock_run_request): + + mock_run_request.side_effect = ["foo", "bar"] + + node = EndpointNode( + { + "endpoints": [ + { + "name": "foo", + "requests": [ + {"name": "First", "path": "http://foo.com/first",}, + { + "name": "Second", + "path": "http://foo.com/second", + }, + ], + } + ], + "name": "node", + "requests": [], + } + ) + + requests_gen = node.run() + + requests = [] + + for request in requests_gen: + requests.append(request) + + assert len(requests) == 2 + + assert requests == ["foo", "bar"] + + @mark.context("when there is an error during a request") + @mark.it("should log the error and change session exit code") + def test_when_request_fails(self, mock_run_request, mock_session, caplog): + + mock_run_request.side_effect = ["foo", Exception("error: bar")] + + node = EndpointNode( + { + "endpoints": [ + { + "name": "foo", + "requests": [ + {"name": "First", "path": "http://foo.com/first",}, + { + "name": "Second", + "path": "http://foo.com/second", + }, + ], + } + ], + "name": "node", + "requests": [], + } + ) + + requests = [] + with caplog.at_level(logging.ERROR): + requests_gen = node.run() + + for request in requests_gen: + requests.append(request) + + assert len(requests) == 1 + + assert ( + "\nError to make request `http://foo.com/second`. \nerror: bar\n" + in caplog.text + ) + + assert mock_run_request.call_count == 2 + + assert mock_session.exit_code == ExitCode.REQUEST_ERROR diff --git a/tests/unit/tree/endpoint_node/test_validate.py b/tests/unit/tree/endpoint_node/test_validate.py index 67011629..fa377e15 100644 --- a/tests/unit/tree/endpoint_node/test_validate.py +++ b/tests/unit/tree/endpoint_node/test_validate.py @@ -1,6 +1,6 @@ from pytest import fixture, mark -from scanapi.tree import EndpointNode +from scanapi.tree import EndpointNode, tree_keys @mark.describe("endpoint node") @@ -32,7 +32,7 @@ def test_should_call_validate_keys(self, mock_validate_keys): "path", "requests", "delay", - "vars", + tree_keys.VARS_KEY, ), ("name",), "endpoint", diff --git a/tests/unit/tree/request_node/test_repr.py b/tests/unit/tree/request_node/test_repr.py new file mode 100644 index 00000000..cae3243a --- /dev/null +++ b/tests/unit/tree/request_node/test_repr.py @@ -0,0 +1,53 @@ +from pytest import mark + +from scanapi.tree import EndpointNode, RequestNode + + +@mark.describe("request node") +@mark.describe("__repr__") +class TestRepr: + @mark.context("when path is not defined") + @mark.it("should return ") + def test_when_path_is_not_defined(self): + endpoint = EndpointNode({"name": "foo", "requests": [{}]}) + request = RequestNode(spec={"name": "bar"}, endpoint=endpoint) + assert repr(request) == "" + + @mark.context("when endpoint node path is defined") + @mark.it("should return ") + def test_when_endpoint_path_is_defined(self): + base_path = "http://foo.com/api" + parent = EndpointNode( + {"path": base_path, "name": "parent-node", "requests": []} + ) + endpoint = EndpointNode( + {"path": "/foo", "name": "node", "requests": []}, parent=parent + ) + + request = RequestNode(spec={"name": "bar"}, endpoint=endpoint) + assert repr(request) == "" + + @mark.context("when request node path is defined") + @mark.it("should return ") + def test_when_request_node_path_is_defined(self): + endpoint = EndpointNode({"name": "foo", "requests": [{}]}) + request = RequestNode( + spec={"name": "bar", "path": "/bar/"}, endpoint=endpoint + ) + assert repr(request) == "" + + @mark.context("when both request and endpoint paths are defined") + @mark.it("should return ") + def test_when_request_and_endpoint_paths_are_defined(self): + base_path = "http://foo.com/api" + parent = EndpointNode( + {"path": base_path, "name": "parent-node", "requests": []} + ) + endpoint = EndpointNode( + {"path": "/foo", "name": "node", "requests": []}, parent=parent + ) + + request = RequestNode( + spec={"name": "bar", "path": "/bar/"}, endpoint=endpoint + ) + assert repr(request) == "" diff --git a/tests/unit/tree/request_node/test_validate.py b/tests/unit/tree/request_node/test_validate.py index 3f86bada..59b01137 100644 --- a/tests/unit/tree/request_node/test_validate.py +++ b/tests/unit/tree/request_node/test_validate.py @@ -1,6 +1,6 @@ from pytest import fixture, mark -from scanapi.tree import EndpointNode, RequestNode +from scanapi.tree import EndpointNode, RequestNode, tree_keys @mark.describe("request node") @@ -33,7 +33,7 @@ def test_should_call_validate_keys(self, mock_validate_keys): "params", "path", "tests", - "vars", + tree_keys.VARS_KEY, "delay", "retry", ),