[
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: build\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\njobs:\n  lint:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v3\n      - run: python -m pip install --upgrade pip wheel\n      - run: pip install tox tox-gh-actions\n      - run: tox -eflake8\n      - run: tox -edocs\n  tests:\n    name: tests\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11']\n      fail-fast: false\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v3\n        with:\n          python-version: ${{ matrix.python }}\n      - run: python -m pip install --upgrade pip wheel\n      - run: pip install tox tox-gh-actions\n      - run: tox\n  coverage:\n    name: coverage\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v3\n      - run: python -m pip install --upgrade pip wheel\n      - run: pip install tox tox-gh-actions\n      - run: tox\n      - uses: codecov/codecov-action@v3\n        with:\n          files: ./coverage.xml\n          fail_ci_if_error: true\n          token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\nmise.toml\nrequirements*.txt\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n\nsphinx:\n  configuration: docs/conf.py\n\npython:\n  install:\n    - method: pip\n      path: .\n      extra_requirements:\n        - docs\n"
  },
  {
    "path": "CHANGES.md",
    "content": "# APIFairy Change Log\n\n**Release 1.5.1** - 2025-11-14\n\n- Pin redoc CDN package to v2 [#94](https://github.com/miguelgrinberg/apifairy/issues/94) ([commit](https://github.com/miguelgrinberg/apifairy/commit/7acc2b4927ecf07bc270b7497eaa3334d2c92e3b)) (thanks **Trevor Mack**!)\n\n**Release 1.5.0** - 2025-10-28\n\n- Add `APIFAIRY_APISPEC_VERSION` option to the configuration [#92](https://github.com/miguelgrinberg/apifairy/issues/92) ([commit](https://github.com/miguelgrinberg/apifairy/commit/3fdd7632b24669901a0ffe67c5366a2992b6548e)) (thanks **Daniel Black**!)\n- Add support for Marshmallow 4, Python 3.13, 3.14 and pypy-3.11. Remove Python 3.8. [#93](https://github.com/miguelgrinberg/apifairy/issues/93) ([commit](https://github.com/miguelgrinberg/apifairy/commit/a2b791d59963c3cb61f1dcc1625bc5cdb4151bdd))\n\n**Release 1.4.0** - 2024-01-15\n\n- Remove use of deprecated `flask.__version__` ([commit](https://github.com/miguelgrinberg/apifairy/commit/a21ecba2fc6dbcdbb7e25c44933116bcaea8aaa4))\n- Handle breaking changes in `webargs.use_args` decorator ([commit](https://github.com/miguelgrinberg/apifairy/commit/943d30303bbdcaabda028ada8e1b2fee0132e7fa))\n- Option to set the media type for the body explicitly [#78](https://github.com/miguelgrinberg/apifairy/issues/78) ([commit](https://github.com/miguelgrinberg/apifairy/commit/b6886ebb4dd276d1d6c68de1122f362e0dec1f84))\n- Update to latest versions of JS and CSS 3rd-party resources [#73](https://github.com/miguelgrinberg/apifairy/issues/73) ([commit](https://github.com/miguelgrinberg/apifairy/commit/f91945a89dde4362be81b4ad9feec1486ac13170)) (thanks **Frank Yu**!)\n- Examples added to the repository ([commit #1](https://github.com/miguelgrinberg/apifairy/commit/b864bd2d4bbaf39f238dcddb691bca2a0cf4a34b) [commit #2](https://github.com/miguelgrinberg/apifairy/commit/ed2c9b99e8ed8b7cd61a1b95f7f295bd2a902590) [commit #3](https://github.com/miguelgrinberg/apifairy/commit/5612f2648c7d118013d0e77565f960e5a5eec07d))\n- Add Python 3.12 to builds ([commit](https://github.com/miguelgrinberg/apifairy/commit/2f3b99c19b1ddaf197b6eb7cf74d645375a42c0f))\n- Migrate Python package metadata to pyproject.toml ([commit](https://github.com/miguelgrinberg/apifairy/commit/38d765b6a492a3c40cbf4fdff6e235be84c67111))\n\n**Release 1.3.0** - 2022-11-13\n\n- Support for documenting webhooks, per OpenAPI 3.1.0 spec ([commit](https://github.com/miguelgrinberg/apifairy/commit/f5b3843a7097c0d2a297e6074c2c1837521a4077))\n- Add Python 3.11 to test builds ([commit](https://github.com/miguelgrinberg/apifairy/commit/0d11acb143a6661f0a0d0b1e857a7626ba066f1d))\n- Stop testing Python 3.6 ([commit](https://github.com/miguelgrinberg/apifairy/commit/e17f702566792bdb045faebb21f1f682bca79b28))\n\n**Release 1.2.0** - 2022-10-06\n\n- Documentation of request and response headers [#63](https://github.com/miguelgrinberg/apifairy/issues/63) ([commit](https://github.com/miguelgrinberg/apifairy/commit/c2a9ec2cc5608f5c26c30428d964b964d00c8b8f))\n\n**Release 1.1.0** - 2022-09-22\n\n- Optional schema for error responses listed in the `@other_responses` decorator [#60](https://github.com/miguelgrinberg/apifairy/issues/60) ([commit](https://github.com/miguelgrinberg/apifairy/commit/e7164b2fada8666e1748fbd06cd78fed7b8d8867))\n- Optional decorators to apply to the apispec and documentation endpoints [#58](https://github.com/miguelgrinberg/apifairy/issues/58) ([commit](https://github.com/miguelgrinberg/apifairy/commit/f9b037d7654691ac39850c311cf5759a0a42a1ab))\n- Fixing some typos in documentation [#53](https://github.com/miguelgrinberg/apifairy/issues/53) ([commit](https://github.com/miguelgrinberg/apifairy/commit/972eb76d9494aceb0ca9d159a3d2ebf59f7e0603)) (thanks **GustavMauler**!)\n- Add link to Microblog API example in readme ([commit](https://github.com/miguelgrinberg/apifairy/commit/6bcdf2ff74008b37aab0f723343469713a6998fb))\n- Updated readme with a screenshot ([commit](https://github.com/miguelgrinberg/apifairy/commit/71d9e96a3abd34b6e528ab43679ac2b781c66dbe))\n\n**Release 1.0.0** - 2022-08-02\n\n- Document path parameters with string annotations ([commit](https://github.com/miguelgrinberg/apifairy/commit/4cade08b60ba4336fcfaf01e63b3ad4b72a8fccc))\n- Support for `typing.Annotated` in path parameter documentation ([commit](https://github.com/miguelgrinberg/apifairy/commit/aa090a0a1d06c298f81efaa3d0b10a844097caae))\n- Correct handling of custom blueprint ordering ([commit](https://github.com/miguelgrinberg/apifairy/commit/1ac7938c5c1288da953231818e567fe740b65ba6))\n- Documentation on how to add manually written documentation ([commit](https://github.com/miguelgrinberg/apifairy/commit/5bfda7e62891b84dfbd63ecaef83bc4191c99272))\n\n**Release 0.9.2** - 2022-07-20\n\n- Form and file upload support [#35](https://github.com/miguelgrinberg/apifairy/issues/35) ([commit](https://github.com/miguelgrinberg/apifairy/commit/59dfb3c252119beb982adef2346c76592ef14528))\n- Additional unit testing coverage ([commit](https://github.com/miguelgrinberg/apifairy/commit/407cf6ba724b6f4c5b90bae8685fee0697f16146))\n- Add Python 3.10 and PyPy 3.8 to builds ([commit](https://github.com/miguelgrinberg/apifairy/commit/66ad682d602f2551d0f075678b63b3f338ec6a28))\n\n**Release 0.9.1** - 2022-01-11\n\n- Mark request body as required when `@body` decorator is used [#37](https://github.com/miguelgrinberg/apifairy/issues/37) ([commit](https://github.com/miguelgrinberg/apifairy/commit/5558b240cf0697fd6da875fdb7b98b76eb6d2d30))\n- Set page title in rapidoc and elements templates ([commit](https://github.com/miguelgrinberg/apifairy/commit/95352b1c430183166a77459983190894c6596122))\n\n**Release 0.9.0** - 2021-12-14\n\n- Better ordering for authentication schemes ([commit](https://github.com/miguelgrinberg/apifairy/commit/a6067f8eeb1fe429935e75c0ca71389caed4754f))\n- Added rapidoc template ([commit](https://github.com/miguelgrinberg/apifairy/commit/ff9a161bc9edfe7e88f1b6f658ea12f2ae91a0e2))\n- Added Elements template ([commit](https://github.com/miguelgrinberg/apifairy/commit/d2ff0543cbf4ed8f293c48b1839445b3deacbf3d))\n- Documented how to create a custom documentation endpoint ([commit](https://github.com/miguelgrinberg/apifairy/commit/47d13793fa06a9f23eca5435478f42b103c980b3))\n\n**Release 0.8.2** - 2021-08-30\n\n- One more change needed to include HTML files in package ([commit](https://github.com/miguelgrinberg/apifairy/commit/7ed49227de57afbd51dbea5bd2b1e24ff12f733f))\n\n**Release 0.8.1** - 2021-08-30\n\n- Add the documentation templates back into the package [#2](https://github.com/miguelgrinberg/apifairy/issues/2) ([commit](https://github.com/miguelgrinberg/apifairy/commit/7e0115cd5706652d7208bfafb8b47e8fe84b5de7))\n\n**Release 0.8.0** - 2021-08-07\n\n- Add `servers` section ([commit](https://github.com/miguelgrinberg/apifairy/commit/6d5d614ff0dc9ef7666191f4ca7c9e9139518d99))\n- Add `operationId` for each endpoint ([commit](https://github.com/miguelgrinberg/apifairy/commit/198855f810b4f97b7f3e61c0cf602e31ab2e0fa8))\n- Add default description for responses ([commit](https://github.com/miguelgrinberg/apifairy/commit/73ec17f13933c5d4a55a81d5131706a531f88dfb))\n- Remove indentation spaces from docstrings [#30](https://github.com/miguelgrinberg/apifairy/issues/30) ([commit](https://github.com/miguelgrinberg/apifairy/commit/30ef9983bf0c5bb31451cdcc2d5d91447d3cf80e))\n- Support Flask 2 async views ([commit](https://github.com/miguelgrinberg/apifairy/commit/bae399aa76d13ebf167a5933f50ddbb5f3923039))\n- Support nested blueprints ([commit](https://github.com/miguelgrinberg/apifairy/commit/c5883a626631744c8ec28782bf852c738169dd8f)) (thanks **Grey Li**!)\n- Improved project structure ([commit](https://github.com/miguelgrinberg/apifairy/commit/1fbd5a59d3c8aa4e2ea38331c750e41f3164bd3f))\n\n**Release 0.7.0** - 2021-05-24\n\n- Correctly handle routes with multiple path arguments [#11](https://github.com/miguelgrinberg/apifairy/issues/11) ([commit](https://github.com/miguelgrinberg/apifairy/commit/898b2f1f6bb7de5b5125162fe17879e4d1734dee)) (thanks **Grey Li**!)\n- Use default status code when route returns a one-element tutple ([commit](https://github.com/miguelgrinberg/apifairy/commit/c895739ce51ea8165de8cd20e322dea7fd2c4645))\n- Update schema name resolver to remove unnecessary List suffix [#21](https://github.com/miguelgrinberg/apifairy/issues/21) ([commit](https://github.com/miguelgrinberg/apifairy/commit/fee7425c32ce0629d65cf1729337d3fe940864a6)) (thanks **Grey Li**!)\n- Fix path arguments order ([commit](https://github.com/miguelgrinberg/apifairy/commit/6793feb36c893212966eeaf4c9bea2b753e3d142)) (thanks **Grey Li**!)\n- Fix path arguments regex [#16](https://github.com/miguelgrinberg/apifairy/issues/16) ([commit](https://github.com/miguelgrinberg/apifairy/commit/7c81c154698dfab0a3c49613ea9885c2ea81be51)) (thanks **Grey Li**!)\n- Fix detection of view docstring [#8](https://github.com/miguelgrinberg/apifairy/issues/8) ([commit](https://github.com/miguelgrinberg/apifairy/commit/4dd8568f037b27a54bb1b57a4ea27580f97cf786)) (thanks **Grey Li**!)\n- Add missing backtick for inline code [#17](https://github.com/miguelgrinberg/apifairy/issues/17) ([commit](https://github.com/miguelgrinberg/apifairy/commit/e25f5487d1be1b9fef828ce8376e35f51d2231dc)) (thanks **Grey Li**!)\n- Document the process_apispec decorator ([commit](https://github.com/miguelgrinberg/apifairy/commit/fd22e11302da82e4aed58e5793efa997d113dc74))\n- Fix typo in Getting Started section [#13](https://github.com/miguelgrinberg/apifairy/issues/13) ([commit](https://github.com/miguelgrinberg/apifairy/commit/11bab4baf9f609c174ff8c7810a2f83f697257e5)) (thanks **Grey Li**!)\n- Fix typo in exception message [#20](https://github.com/miguelgrinberg/apifairy/issues/20) ([commit](https://github.com/miguelgrinberg/apifairy/commit/217a7fc976b860daa07199c297c7086b63e341be)) (thanks **Grey Li**!)\n- Add openapi-spec-validator into tests_require [#9](https://github.com/miguelgrinberg/apifairy/issues/9) ([commit](https://github.com/miguelgrinberg/apifairy/commit/faf551cd2bb224c33f5f6cfc94b2cb34a5249bf6)) (thanks **Grey Li**!)\n- Added missing import statements in documentation examples [#7](https://github.com/miguelgrinberg/apifairy/issues/7) ([commit](https://github.com/miguelgrinberg/apifairy/commit/316e0a5af3689947aa7d080c3c3aad87454235bd)) (thanks **Grey Li**!)\n- Move builds to GitHub actions ([commit](https://github.com/miguelgrinberg/apifairy/commit/b8cec62a7d719b6dd51b69dbf8f983b61459be94))\n\n**Release 0.6.2** - 2020-10-10\n\n- Documentation updates ([commit](https://github.com/miguelgrinberg/apifairy/commit/ae72b2abc850ecf58c47603fac39fc92fd5c76ec))\n\n**Release 0.6.1** - 2020-10-05\n\n- Fixed release script to include HTML templates\n- Rename blueprint to `apifairy`\n\n**Release 0.6.0** - 2020-10-03\n\n- More unit test coverage\n- Configuration through Flask's `config` object\n- Error handling\n\n**Release 0.5.0** - 2020-09-28\n\n- First public release!\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Miguel Grinberg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md LICENSE tox.ini\nrecursive-include docs *\nrecursive-exclude docs/_build *\nrecursive-include tests *\nexclude **/*.pyc\n"
  },
  {
    "path": "README.md",
    "content": "# APIFairy\n\n[![Build status](https://github.com/miguelgrinberg/apifairy/workflows/build/badge.svg)](https://github.com/miguelgrinberg/apifairy/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/apifairy/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/APIFairy)\n\nAPIFairy is a minimalistic API framework built on top of Flask, and with the\nsupport of Marshmallow schemas. Using a familiar decorator syntax you can\ngenerate a live documentation site directly from your source code.\n\nCheck out [Microblog-API](https://github.com/miguelgrinberg/microblog-api) to\nsee APIFairy in action in a non-trivial project.\n\n![APIFairy example](docs/_static/apispec-example.png)\n\nResources\n---------\n\n- [Documentation](http://apifairy.readthedocs.io/en/latest/)\n- [PyPI](https://pypi.python.org/pypi/APIFairy)\n- [Change Log](https://github.com/miguelgrinberg/APIFairy/blob/main/CHANGES.md)\n"
  },
  {
    "path": "bin/mkchangelog.py",
    "content": "import datetime\nimport re\nimport sys\nimport git\n\nURL = 'https://github.com/miguelgrinberg/apifairy'\nmerges = {}\n\n\ndef format_message(commit):\n    if commit.message.startswith('Version '):\n        return ''\n    if '#nolog' in commit.message:\n        return ''\n    if commit.message.startswith('Merge pull request'):\n        pr = commit.message.split('#')[1].split(' ')[0]\n        message = ' '.join([line for line in [line.strip() for line in commit.message.split('\\n')[1:]] if line])\n        merges[message] = pr\n        return ''\n    if commit.message.startswith('Release '):\n        return '\\n**{message}** - {date}\\n'.format(\n            message=commit.message.strip(),\n            date=datetime.datetime.fromtimestamp(commit.committed_date).strftime('%Y-%m-%d'))\n    message = ' '.join([line for line in [line.strip() for line in commit.message.split('\\n')] if line])\n    if message in merges:\n        message += ' #' + merges[message]\n    message = re.sub('\\\\(.*(#[0-9]+)\\\\)', '\\\\1', message)\n    message = re.sub('Fixes (#[0-9]+)', '\\\\1', message)\n    message = re.sub('fixes (#[0-9]+)', '\\\\1', message)\n    message = re.sub('#([0-9]+)', '[#\\\\1]({url}/issues/\\\\1)'.format(url=URL), message)\n    message += ' ([commit]({url}/commit/{sha}))'.format(url=URL, sha=str(commit))\n    if commit.author.name != 'Miguel Grinberg':\n        message += ' (thanks **{name}**!)'.format(name=commit.author.name)\n    return '- ' + message\n\n\ndef main(all=False):\n    repo = git.Repo()\n\n    for commit in repo.iter_commits():\n        if not all and commit.message.startswith('Release '):\n            break\n        message = format_message(commit)\n        if message:\n            print(message)\n\n\nif __name__ == '__main__':\n    main(all=len(sys.argv) > 1 and sys.argv[1] == 'all')\n"
  },
  {
    "path": "bin/release",
    "content": "#!/bin/bash -ex\n\nVERSION=\"$1\"\nVERSION_FILE=apifairy/__init__.py\n\nif [[ \"$VERSION\" == \"\" ]]; then\n    echo \"Usage: $0 <version>\"\n    exit 1\nfi\n\n# update change log\nhead -n 2 CHANGES.md > _CHANGES.md\necho \"**Release $VERSION** - $(date +%F)\" >> _CHANGES.md\necho \"\" >> _CHANGES.md\npip install gitpython\npython bin/mkchangelog.py >> _CHANGES.md\necho \"\" >> _CHANGES.md\nlen=$(wc -l < CHANGES.md)\ntail -n $(expr $len - 2) CHANGES.md >> _CHANGES.md\nvim _CHANGES.md\nset +e\ngrep -q ABORT _CHANGES.md\nif [[ \"$?\" == \"0\" ]]; then\n    rm _CHANGES.md\n    echo \"Aborted.\"\n    exit 1\nfi\nset -e\nmv _CHANGES.md CHANGES.md\n\nsed -i \"\" \"s/^__version__ = '.*'$/__version__ = '$VERSION'/\" $VERSION_FILE\nrm -rf dist\npip install --upgrade pip wheel twine\npython setup.py sdist bdist_wheel --universal\n\ngit add $VERSION_FILE CHANGES.md\ngit commit -m \"Release $VERSION\"\ngit tag -f v$VERSION\ngit push --tags origin master\n\nread -p \"Press any key to submit to PyPI or Ctrl-C to abort...\" -n1 -s\ntwine upload dist/*\n\nNEW_VERSION=\"${VERSION%.*}.$((${VERSION##*.}+1))dev\"\nsed -i \"\" \"s/^__version__ = '.*'$/__version__ = '$NEW_VERSION'/\" $VERSION_FILE\ngit add $VERSION_FILE\ngit commit -m \"Version $NEW_VERSION\"\ngit push origin master\necho \"Development is now open on version $NEW_VERSION!\"\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/apifairy_class.rst",
    "content": ".. APIFairy documentation master file, created by\n   sphinx-quickstart on Sun Sep 27 17:34:58 2020.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nThe APIFairy Class\n==================\n\nThe main function of the ``APIFairy`` instance is to gather all the information\nregistered by the decorators and generate an `OpenAPI 3.x\n<https://swagger.io/specification/>`_ compliant schema with it. This schema is\nthen used to render the documentation site using one of the available\nopen-source documentation projects that are compatible with this specification.\n\nIn addition to ducmentation, ``APIFairy`` allows the application to\ninstall a custom error handler to be used when a schema validation error occurs\nin routes decorated with the ``@body`` or ``@arguments`` decorators. It also\nregisters routes to serve the OpenAPI definition in JSON format and a\ndocumentation site based on one of the supported third-party documentation\nprojects.\n\nAPIFairy.apispec\n----------------\n\nThe ``apispec`` property returns the complete OpenAPI definition for the\nproject as a Python dictionary. The information used to build this data is\nobtained from several places:\n\n- The project's name and version are obtained from the ``APIFAIRY_TITLE`` and\n  ``APIFAIRY_VERSION`` configuration items respectively.\n- The top-level documentation for the project, which appears above the API\n  definitions, is obtained from the main module's docstring. Markdown can be\n  used to organize this content in sections and use rich-text formatting.\n- The paths are obtained from all the Flask routes that have been decorated\n  with at least one of the five decorators from this project. Routes that have\n  not been decorated with these decorators are not included in the\n  documentation.\n- The schemas and security schemes are collected from decorator usages.\n- Each path is documented using the information provided in the decorators,\n  plus the route definition for Flask and the docstring of the view function.\n  The first line of the docstring is used as a summary and the remaining lines\n  as a description.\n- If a route belongs to a blueprint, the corresponding path is tagged with the\n  blueprint name. Paths are grouped by their tag, which ensures that routes\n  from each blueprint are rendered together in their own section. The\n  ``APIFAIRY_TAGS`` configuration item can be used to provide a custom ordering\n  for tags.\n- Each security scheme is documented by inspecting the Flask-HTTPAuth object,\n  plus the contents of the ``__doc__`` property if it exists.\n\nAPIFairy.process_apispec\n------------------------\n\nThe ``process_apispec`` decorator can be used to register a custom function\nthat receives the generated OpenAPI definition as its single argument. The\nfunction can make changes and adjustments to it and return the modified\ndefinition, which will then be rendered::\n\n    @apifairy.process_apispec\n    def my_apispec_processor(spec):\n        # modify spec as needed here\n        return spec\n\nAPIFairy.error_handler\n----------------------\n\nThe ``error_handler`` method can be used to register a custom error handler\nfunction that will be invoked whenever a validation error is raised by the\nwebargs project. This method can be used as a decorator as follows::\n\n    @apifairy.error_handler\n    def my_error_handler(status_code, messages):\n        return {'code': status_code, 'messages': messages}, status_code\n\nThe ``status_code`` argument is the suggested HTTP status code, which is\ntypically 400 for a \"bad request\" response. The ``messages`` argument is a\ndictionary with all the validation error messages that were found, organized as\na dictionary with the following structure::\n\n    \"location1\": {\n        \"field1\": [\"message1\", \"message2\", ...],\n        \"field2\": [ ... ],\n        ...\n    },\n    \"location2\": { ... },\n    ...\n\nThe location keys can be ``'json'`` for the request body or ``'query'`` for the\nquery string.\n\nThe return value of the error handling function is interpreted as a standard\nFlask response, and returned to the client as such.\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- Project information -----------------------------------------------------\n\nproject = 'APIFairy'\ncopyright = '2020, Miguel Grinberg'\nauthor = 'Miguel Grinberg'\n\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autosectionlabel',\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'alabaster'\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\nhtml_theme_options = {\n    'description': ('A minimalistic API framework built on top of Flask, '\n                    'Marshmallow and friends.'),\n    'fixed_sidebar': True,\n    'github_user': 'miguelgrinberg',\n    'github_repo': 'APIFairy',\n    'github_button': True,\n    'github_type': 'star',\n    'github_banner': True,\n}\n"
  },
  {
    "path": "docs/decorators.rst",
    "content": ".. APIFairy documentation master file, created by\n   sphinx-quickstart on Sun Sep 27 17:34:58 2020.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nDecorator Reference\n===================\n\nThe core functionality of APIFairy is accessed through its five decorators,\nwhich are used to define what the inputs and outputs of each endpoint are.\n\n@arguments\n----------\n\nThe ``arguments`` decorator specifies input arguments, given in the query\nstring of the request URL. The only argument this decorator requires is the\nschema definition for the input data, which can be given as a schema class or\ninstance::\n\n    from apifairy import arguments\n\n    class PaginationSchema(ma.Schema):\n       page = ma.Int(missing=1)\n       limit = ma.Int(missing=10)\n\n    @app.route('/api/user/<int:id>/followers')\n    @arguments(PaginationSchema)\n    def get_followers(pagination, id):\n        page = pagination['page']\n        limit = pagination['limit']\n        # ...\n\nThe decorator will deserialize and validate the input data and will only\ninvoke the view function when the arguments are valid. In the case of a\nvalidation error, the error handler is invoked to generate an error response\nto the client.\n\nThe deserialized input data is passed to the view function as a positional\nargument. Note that Flask passes path arguments as keyword arguments, so the\nargument from this decorator must be defined first, as seen in the example\nabove. When multiple input decorators are used, the positional arguments are\ngiven in the same order as the decorators.\n\nUsing multiple inputs\n~~~~~~~~~~~~~~~~~~~~~\n\nThe ``arguments`` decorator can be given multiple times, but in that case the\nschemas must have their ``unknown`` attribute set to ``EXCLUDE``, so that the\narguments from the different schemas are assigned properly::\n\n    class PaginationSchema(ma.Schema):\n        page = ma.Int(missing=1)\n        limit = ma.Int(missing=10)\n\n    class FilterSchema(ma.Schema):\n        f = ma.Str()\n\n    @app.route('/api/user/<int:id>/followers')\n    @arguments(PaginationSchema(unknown=ma.EXCLUDE))\n    @arguments(FilterSchema(unknown=ma.EXCLUDE))\n    def get_followers(pagination, filter, id):\n        page = pagination['page']\n        limit = pagination['limit']\n        f = filter.get('filter')\n        # ...\n\nNote that in this example the ``filter`` argument does not have ``missing`` or\n``required`` attributes, so it will be considered optional. If the query string\ndoes not include it, then ``filter`` will be empty.\n\nLists\n~~~~~\n\nA list can be defined in the usual way using Marshmallow's ``List`` field::\n\n    class Filter(ma.Schema):\n        f = ma.List(ma.Str())\n\n    @app.route('/test')\n    @arguments(Filter())\n    def test(filter):\n        f = filter.get('f', [])\n        # ...\n\nThe client then must repeat the argument as many times as needed in the query\nstring. For example, the URL *http://localhost:5000/test?f=foo&f=bar* would\nset the ``filter`` argument to ``{'f': ['foo', 'bar']}``.\n\nAdvanced Usage\n~~~~~~~~~~~~~~\n\nThe ``arguments`` decorator is a thin wrapper around the ``use_args``\ndecorator from the `webargs <https://webargs.readthedocs.io/>`_ project with\nthe ``location`` argument set to ``query``. Any additional options are passed\ndirectly into ``use_args``, which among other things allow the use of other\nlocations for input arguments besides the query string.\n\n@body\n-----\n\nThe ``body`` decorator defines the structure of the body of the request. The\nonly required argument to this decorator is the schema definition for the\nrequest body, which can be given as a schema class or instance::\n\n    from apifairy import body\n\n    class UserSchema(ma.Schema):\n        id = ma.Int()\n        username = ma.Str(required=True)\n        email = ma.Str(required=True)\n        about_me = ma.Str(missing='')\n\n    @app.route('/users', methods=['POST'])\n    @body(UserSchema)\n    def create_user(user):\n        # ...\n\nThe decorator will deserialize and validate the input data and will only\ninvoke the view function when the data passes validation. In the case of a\nvalidation error, the error handler is invoked to generate an error response\nto the client.\n\nThe deserialized input data is passed to the view function as a positional\nargument. Note that Flask passes path arguments as keyword arguments, so the\nargument from this decorator must be defined first. When multiple input\ndecorators are used, the positional arguments are given in the same order as\nthe decorators.\n\nForms\n~~~~~\n\nThis decorator can also be used to configure an endpoint to accept form data,\nby adding the optional ``location`` argument set to ``form``::\n\n    from apifairy import body\n\n    class UserSchema(ma.Schema):\n        id = ma.Int()\n        username = ma.Str(required=True)\n        email = ma.Str(required=True)\n        about_me = ma.Str(missing='')\n\n    @app.route('/users', methods=['POST'])\n    @body(UserSchema, location='form')\n    def create_user(user):\n        # ...\n\nFile uploads can be declared with the ``FileField`` field type, which returns\na standard ``FileStorage`` object from Flask::\n\n    from apifairy import body\n    from apifairy.fields import FileField\n\n    class UserSchema(ma.Schema):\n        id = ma.Int()\n        username = ma.Str(required=True)\n        avatar = FileField()\n\n    @app.route('/users', methods=['POST'])\n    @body(UserSchema, location='form')\n    def create_user(user):\n        # ...\n\nThe ``FileField`` field type can also be combined with Marshmallow's ``List``\nto accept a list of files. But for this to work, the ``media_type`` argument\nneeds to be added to the ``@body`` decorator to ensure that the request is\nparsed as a multipart form::\n\n    from apifairy import body\n    from apifairy.fields import FileField\n\n    class UserSchema(ma.Schema):\n        id = ma.Int()\n        username = ma.Str(required=True)\n        files = ma.List(FileField())\n\n    @app.route('/users', methods=['POST'])\n    @body(UserSchema, location='form', media_type='multipart/form-data')\n    def create_user(user):\n        # ...\n\nAdvanced Usage\n~~~~~~~~~~~~~~\n\nThe ``body`` decorator is a thin wrapper around the ``use_args`` decorator\nfrom the `webargs <https://webargs.readthedocs.io/>`_ project with\nthe ``location`` argument set to ``json`` or ``form``. Any additional options\nare passed directly into ``use_args``.\n\n@response\n---------\n\nThe ``response`` decorator specifies the structure of the endpoint response.\nThe only required argument to this decorator is the schema that defines the\nresponse, which can be given as a schema class or instance::\n\n    from apifairy import response\n\n    @app.route('/users/<int:id>')\n    @response(UserSchema)\n    def get_user(id):\n        return User.query.get_or_404(id)\n\nThe decorator performs the serialization of the returned object or dictionary\nto JSON through the schema's ``jsonify()`` method.\n\nThis decorator accepts two optional arguments. The ``status_code`` argument is\nused to specify the HTTP status code for the response, when it is not the\ndefault of 200. The ``description`` argument is used to provide a text\ndescription of this response to be added to the documentation::\n\n    @app.route('/users', methods=['POST'])\n    @body(UserSchema)\n    @response(UserSchema, status_code=201, description='A user was created.')\n    def create_user(user):\n        # ...\n        \n@other_responses\n----------------\n\nThe ``other_responses`` decorator is used to specify additional responses the\nendpoint can return, usually as a result of an error condition. The only\nargument to this decorator is a dictionary with the keys set to numeric HTTP\nstatus codes. In its simplest form, the values of the dictionary are strings\nthat describe each response::\n\n    from apifairy import response, other_responses\n\n    @app.route('/users/<int:id>')\n    @response(UserSchema)\n    @other_responses({400: 'Invalid request.', 404: 'User not found.'})\n    def get_user(id):\n        # ...\n\nIf desired a schema can be provided for each response instead::\n\n    from apifairy import response, other_responses\n\n    @app.route('/users/<int:id>')\n    @response(UserSchema)\n    @other_responses({400: BadRequestSchema, 404: UserNotFoundSchema})\n    def get_user(id):\n        # ...\n\nFinally, a schema and a description can both be given as a tuple::\n\n    from apifairy import response, other_responses\n\n    @app.route('/users/<int:id>')\n    @response(UserSchema)\n    @other_responses({400: (BadRequestSchema, 'Invalid request.'),\n                      404: (UserNotFoundSchema, 'User not found.')})\n    def get_user(id):\n        # ...\n\nThis decorator does not perform any validation or formatting of error\nresponses, it just adds the information provided to the documentation.\n\n@authenticate\n-------------\n\nThe ``authenticate`` decorator is used to specify the authentication and\nauthorization requirements of the endpoint. The only required argument for\nthis decorator is an authentication object from the `Flask-HTTPAuth\n<https://flask-httpauth.readthedocs.io/>`_ extension::\n\n    from flask_httpauth import HTTPBasicAuth\n    from apifairy import authenticate\n\n    auth = HTTPBasicAuth()\n\n    @app.route('/users/<int:id>')\n    @authenticate(auth)\n    @response(UserSchema)\n    def get_user(id):\n        return User.query.get_or_404(id)\n\nThe decorator invokes the ``login_required`` method of the authentication\nobject, and also adds an Authentication section to the documentation.\n\nIf the roles feature of Flask-HTTPAuth is used, the documentation will include\nthe required role(s) for each endpoint. Any keyword arguments given to the\n``authenticate`` decorator, including the ``role`` argument, are passed\nthrough to Flask-HTTPAuth.\n\n@webhook\n--------\n\nThe ``webhook`` decorator is used to document a webhook, which is an endpoint\nthat must be implemented by the API client for the server to invoke as a\ncallback or notification. OpenAPI added support for webhooks in its 3.1.0\nversion.\n\nWebhooks are defined with a dummy function that is never invoked. After the\n``webhook`` decorator is applied, the ``arguments``, ``body``, ``response``\nand ``other_responses`` decorators can be used to document the inputs and\noutputs.\n\nExample::\n\n    from apifairy import webhook, body\n\n    @webhook\n    @body(ResultsSchema)\n    def results():\n        pass\n\nThe ``webhook`` decorator accepts three optional arguments. The ``method``\nargument is used to specify the HTTP method that the server will use to invoke\nthe webhook. If this argument is not specified, ``GET`` is used.\n\nThe ``blueprint`` argument is used to optionally specify a blueprint with which\nthis webhook should be grouped. This adds the a tag with the blueprint's name,\nwhich will make most documentation renderers add the webhook definition in the\nsame section as the endpoints in the blueprint.\n\nThe ``endpoint`` argument can be used to explicitly provide the endpoint name\nunder which the webhook should be documented. If this argument is not given,\nthe endpoint name is the name of the webhook function.\n\nThe next example shows webhook definition using a ``POST`` HTTP method, added\nto a ``users`` blueprint::\n\n    from apifairy import webhook, body\n\n    @webhook(method='POST', blueprint=users)\n    @body(ResultsSchema)\n    def results():\n        pass\n"
  },
  {
    "path": "docs/guide.rst",
    "content": "Documenting your API with APIFairy\n==================================\n\nAPIFairy can discover and document your API through its\n:ref:`decorators <Decorator Reference>`, but in most cases you'll want to\ncomplement automatically generated documentation with manually written notes.\nThe following sections describe all the places where APIFairy looks for text to\nattach to your project's documentation.\n\nProject Title and Version\n-------------------------\n\nThe title and version of your project are defined in the Flask configuration\nobject::\n\n    app = Flask(__name__)\n    app.config['APIFAIRY_TITLE'] = 'My API Project'\n    app.config['APIFAIRY_VERSION'] = '1.0'\n\nProject Overview\n----------------\n\nMost API documentation sites include one or more sections that provide general\nproject information for developers, such as how to authenticate, how\npagination works, or what is the structure of error responses. APIFairy looks\nfor project description text to attach to the documentation in module-level\ndocstrings in all the packages and modules referenced in the Flask\napplication's import name, starting from the right side.\n\nWhile different OpenAPI documentation renderers may have different expectations\nfor the formatting of this text, it is fairly common for documentation to be\nwritten in Markdown format, with support for long, multi-line text.\n\nTo help clarify how this works, consider a project with the following\nstructure:\n\n- my_api_project/\n   - api/\n      - __init__.py\n      - app.py\n      - routes.py\n   - project.py\n\nThe contents of *project.py* are::\n\n    from api.app import create_app\n\n    app = create_app()\n\nThe contents of *api/app.py* are::\n\n    from flask import Flask\n    from apifairy import APIFairy\n\n    apifairy = APIFairy()\n\n    def create_app():\n        app = Flask(__name__)\n        app.config['APIFAIRY_TITLE'] = 'My API Project'\n        app.config['APIFAIRY_VERSION'] = '1.0'\n        apifairy.init_app(app)\n        return app\n\nWith this project structure, the import name of the Flask application is\n``api.app``. In general, the import name of the application is the value that\nis passed as first argument to the ``Flask`` class. In most cases this is the\n``__name__`` Python global variable, which represents the fully qualified\npackage name of the module in which the application is defined.\n\nFollowing this example, APIFairy will first look for project-level\ndocumentation in the ``api.app`` module, which maps to the *api/app.py* file.\nDocumentation can then be added at the top of this file, as follows::\n\n    \"\"\"Welcome to My API Project!\n\n    ## Project Overview\n\n    This is the project overview.\n\n    ## Authentication\n\n    This is how authentication works.\n    \"\"\"\n    from flask import Flask\n    from apifairy import APIFairy\n\n    apifairy = APIFairy()\n\n    def create_app():\n        app = Flask(__name__)\n        app.config['APIFAIRY_TITLE'] = 'My API Project'\n        app.config['APIFAIRY_VERSION'] = '1.0'\n        apifairy.init_app(app)\n        return app\n\nIf APIFairy does not find a module docstring in ``api.app``, it will remove the\nlast component of the import name and try again. Following this example, this\nwould be ``api``, which is a package, so its docstring can be found in\n*api/__init__.py*.\n\nSo the alternative to putting the documentation in *api/app.py* is to leave\nthis file without a docstring, and instead add the documentation in\n*api/__init__.py*.\n\nEndpoints\n---------\n\nTo document an endpoint, add a docstring to its view function. The first line\nof the docstring should be a short summary of the endpoint's purpose. A longer\ndescription can be included starting from the second line.\n\nExample with just a summary::\n\n    @users.route('/users', methods=['POST'])\n    @body(user_schema)\n    @response(user_schema, 201)\n    def new(args):\n        \"\"\"Register a new user\"\"\"\n        user = User(**args)\n        db.session.add(user)\n        db.session.commit()\n        return user\n\nExample with summary and longer description::\n\n    @users.route('/users', methods=['POST'])\n    @body(user_schema)\n    @response(user_schema, 201)\n    def new(args):\n        \"\"\"Register a new user\n        Clients can use this endpoint when they need to register a new user\n        in the system.\n        \"\"\"\n        user = User(**args)\n        db.session.add(user)\n        db.session.commit()\n        return user\n\nAs with the project overview, these docstrings can also be written in Markdown.\n\nPath parameters\n---------------\n\nFor endpoints that have dynamic components in their path, APIFairy will\nautomatically extract their type directly from the Flask route specification.\nA text description of a parameter can be included by adding a string as an\nannotation.\n\nAnnotations have been evolving in recent releasees of Python, so the best\nformat to provide documentation for endpoint parameters depends on which\nversion of Python you are using.\n\nThe basic method, which works with any recent version of Python, involves\nsimply adding the documentation as a string annotation to the parameter::\n\n    @users.route('/users/<int:id>', methods=['GET'])\n    @authenticate(token_auth)\n    @response(user_schema)\n    def get(id: 'The id of the user to retrieve.'):  # noqa: F722\n        \"\"\"Retrieve a user by id\"\"\"\n        return db.session.get(User, id) or abort(404)\n\nWhile this method works, Python code linters and type checkers will flag the\nannotation as invalid, because they expect annotations to be used for type\nhints and not for documentation, so it may be necessary to add a ``noqa`` or\nsimilar comment for these errors to be ignored.\n\nIf using Python 3.9 or newer, luckily there is a better option. The\n`typing.Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated>`_\ntype can be used to provide a type hint for the parameter along with additional\nmetadata such as a documentation string::\n\n    from typing import Annotated\n\n    @users.route('/users/<int:id>', methods=['GET'])\n    @authenticate(token_auth)\n    @response(user_schema)\n    def get(id: Annotated[int, 'The id of the user to retrieve.']):\n        \"\"\"Retrieve a user by id\"\"\"\n        return db.session.get(User, id) or abort(404)\n\nEven if the project does not use type hints, using this format will prevent\nlinting and typing errors, so it is the preferred way to document a parameter.\n\nDocumentation for parameters can include multiple lines and paragraphs, if\ndesired. Markdown formatting is also supported by most OpenAPI renderers.\n\nSchemas\n-------\n\nMany of the :ref:`APIFairy decorators <Decorator Reference>` accept Marshmallow\nschemas as arguments. These schemas are automatically documented, including\ntheir field types and validation requirements.\n\nIf the application wants to provide additional information, a schema\ndescription can be provided in the ``description`` field of the schema's\nmetaclass::\n\n    class UserSchema(ma.SQLAlchemySchema):\n        class Meta:\n            model = User\n            ordered = True\n            description = 'This schema represents a user.'\n\n        id = ma.auto_field(dump_only=True)\n        url = ma.String(dump_only=True)\n        username = ma.auto_field(required=True,\n                                 validate=validate.Length(min=3, max=64))\n\nDocumentation that is specific to a schema field can be added in a\n``description`` argument when the field is declared::\n\n    class UserSchema(ma.SQLAlchemySchema):\n        class Meta:\n            model = User\n            ordered = True\n\n        id = ma.auto_field(dump_only=True, description=\"The user's id.\")\n        url = ma.String(dump_only=True, description=\"The user's unique URL.\")\n        username = ma.auto_field(required=True,\n                                 validate=validate.Length(min=3, max=64),\n                                 description=\"The user's username.\")\n\nQuery String\n------------\n\nAPIFairy will automatically document query string parameters for endpoints that\nuse the :ref:`@arguments` decorator::\n\n    @users.route('/users', methods=['GET'])\n    @arguments(pagination_schema)\n    @response(users_schema)\n    def get_users(pagination):\n        \"\"\"Retrieve all users\"\"\"\n        # ...\n\nRequest Headers\n---------------\n\nAPIFairy also documents request headers that are declared with the\n:ref:`@arguments` decorator. Note that this decorator defaults to the query\nstring, but the `location` argument can be set to `headers` when needed.\n\nExample::\n\n    class HeadersSchema(ma.Schema):\n        x_token = ma.String(data_key='X-Token', required=True)\n\n    @users.route('/users', methods=['GET'])\n    @arguments(HeadersSchema, location='headers')\n    @response(users_schema)\n    def get_users(headers):\n        \"\"\"Retrieve all users\"\"\"\n        # ...\n\nThe ``@arguments`` decorator can be given twice when an endpoint needs query\nstring and header arguments both::\n\n    @users.route('/users', methods=['GET'])\n    @arguments(PaginationSchema)\n    @arguments(HeadersSchema, location='headers')\n    @response(users_schema)\n    def all(pagination, headers):\n        \"\"\"Retrieve all users\"\"\"\n        # ...\n\nResponses\n---------\n\nIn addition to the schema documentation, an endpoint response can be given a\ntext description in a ``description`` argument to the ``@response`` decorator.\n\nExample::\n\n    @tokens.route('/tokens', methods=['PUT'])\n    @body(token_schema)\n    @response(token_schema, description='Newly issued access and refresh tokens')\n    def refresh(args):\n        \"\"\"Refresh an access token\"\"\"\n        ...\n\nFor endpoints that return information in response headers, the ``headers``\nargument can be used to add these to the documentation::\n\n    class HeadersSchema(ma.Schema):\n        x_token = ma.String(data_key='X-Token')\n\n    @tokens.route('/tokens', methods=['PUT'])\n    @body(token_schema)\n    @response(token_schema, headers=HeadersSchema)\n    def refresh(args):\n        \"\"\"Refresh an access token\"\"\"\n        ...\n\nError Responses\n---------------\n\nThe ``@other_responses`` decorator takes a dictionary argument, where the keys\nare the response status codes and the values provide the documentation.\n\nTo add text descriptions to these responses, set the value for each status code\nto a descrition string.\n\nExample::\n\n    @tokens.route('/tokens', methods=['PUT'])\n    @body(token_schema)\n    @response(token_schema, description='Newly issued access and refresh tokens')\n    @other_responses({401: 'Invalid access or refresh token',\n                      403: 'Insufficient permissions'})\n    def refresh(args):\n        \"\"\"Refresh an access token\"\"\"\n        ...\n\n\nTo document the error response with a schema, set the value to the schema\ninstance.\n\nExample::\n\n    @tokens.route('/tokens', methods=['PUT'])\n    @body(token_schema)\n    @response(token_schema, description='Newly issued access and refresh tokens')\n    @other_responses({401: invalid_token_schema,\n                      403: insufficient_permissions_schema})\n    def refresh(args):\n        \"\"\"Refresh an access token\"\"\"\n        ...\n\nA schema and a description can both be given as a tuple::\n\n    @tokens.route('/tokens', methods=['PUT'])\n    @body(token_schema)\n    @response(token_schema, description='Newly issued access and refresh tokens')\n    @other_responses({401: (invalid_token_schema, 'Invalid access or refresh token'),\n                      403: (insufficient_permissions_schema, 'Insufficient permissions')})\n    def refresh(args):\n        \"\"\"Refresh an access token\"\"\"\n        ...\n\nAuthentication\n--------------\n\nAPIFairy recognizes the Flask-HTTPAuth authentication object passed to the\n``@authenticate`` decorator and creates the appropriate structure according to\nthe OpenAPI specification. To add textual documentation, define a subclass of\nthe Flask-HTTPAuth authentication object and add a docstring with the\ndocumentation to it.\n\nExample::\n\n    from flask_httpauth import HTTPBasicAuth\n\n    class DocumentedAuth(HTTPBasicAuth):\n        \"\"\"Basic authentication scheme.\"\"\"\n        pass\n\n    basic_auth = DocumentedAuth()\n\n    @tokens.route('/tokens', methods=['POST'])\n    @authenticate(basic_auth)\n    @response(token_schema)\n    @other_responses({401: 'Invalid username or password'})\n    def new():\n        \"\"\"Create new access and refresh tokens\"\"\"\n        ...\n\nTags and Blueprints\n-------------------\n\nAPIFairy automatically creates OpenAPI tags for all the blueprints defined in\nthe application, assigns each endpoint to the corresponding tag, and generates\nthe OpenAPI documentation with the endpoints grouped by their tag.\n\nThe order in which the groups appear can be controlled with the\n``APIFAIRY_TAGS`` configuration variable, which is a list of the blueprint\nnames in the desired order. Any names that are not included in this list will\nexclude the associated endpoints from the documentation.\n\nA textual description for each blueprint can be provided as a module-level\ndocstring in the module in which the blueprint is defined.\n\nAnything else\n-------------\n\nFor any other documentation needs that are not covered by the options listed\nabove, the application can manually modify the OpenAPI structure. This can be\nachieved in a function decorated with the ``@process_apispec`` decorator::\n\n    @apifairy.process_apispec\n    def my_apispec_processor(spec):\n        # modify spec as needed here\n        return spec\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. APIFairy documentation master file, created by\n   sphinx-quickstart on Sun Sep 27 17:34:58 2020.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nAPIFairy\n========\n\nWelcome to the documentation for APIFairy, the minimalistic API framework for\nFlask.\n\n.. toctree::\n   :maxdepth: 2\n\n   intro\n   guide\n   decorators\n   apifairy_class\n"
  },
  {
    "path": "docs/intro.rst",
    "content": ".. APIFairy documentation master file, created by\n   sphinx-quickstart on Sun Sep 27 17:34:58 2020.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nGetting Started\n===============\n\nAPIFairy is a minimalistic API framework for Flask with the following goals:\n\n- Give you a way to specify what the input arguments for each endpoint are,\n  and automatically validate them for you.\n- Give you a way to specify what the response format for each endpoint is, and\n  automatically serialize these responses for you.\n- Automatically generate API documentation for your project.\n- Introduce the least amount of rules. You should be able to code your\n  endpoints in the style that you like.\n\nBelow you can see an example API endpoint augmented with\nAPIFairy decorators::\n\n    from apifairy import authenticate, body, response, other_responses\n\n    # ...\n\n    @posts_blueprint.route('/posts/<int:id>', methods=['PUT'])\n    @authenticate(token_auth)\n    @body(update_post_schema)\n    @response(post_schema)\n    @other_responses({404: 'Post not found'})\n    def put(updated_post, id):\n        \"\"\"Edit a post.\"\"\"\n        post = Post.query.get_or_404(id)\n        for attr, value in updated_post.items():\n            setattr(post, attr, value)\n        db.session.commit()\n        return post\n\nAPIFairy's decorators are simple wrappers for existing solutions. In the\nexample above, ``token_auth`` is an intialized authentication object from the\nFlask-HTTPAuth extension, and ``post_schema`` and ``update_post_schema`` are\nFlask-Marshmallow schema objects. Using the decorator wrappers allow APIFairy\nto automatically generate documentation using the OpenAPI 3.x standard. Below\nis a screenshot of the documentation for the above endpoint:\n\n.. image:: _static/apispec-example.png\n  :width: 100%\n  :alt: Automatic documentation example\n\nInstallation\n------------\n\nAPIFairy is installed with ``pip``::\n\n    pip install apifairy\n\nOnce installed, this package is initialized as a standard Flask extension::\n\n    from flask import Flask\n    from apifairy import APIFairy\n\n    app = Flask(__name__)\n    apifairy = APIFairy(app)\n\nThe two-phase initialization style is also supported::\n\n    from flask import Flask\n    from apifairy import APIFairy\n\n    apifairy = APIFairy()\n\n    def create_app():\n        app = Flask(__name__)\n        apifairy.init_app(app)\n        return app\n\nOnce APIFairy is initialized, automatically generated documentation can be\naccessed at the */docs* URL. The raw OpenAPI documentation data in JSON format\ncan be accessed at the */apispec.json* URL. Both URLs can be changed in the\nconfiguration if desired.\n\nConfiguration\n-------------\n\nAPIFairy imports its configuration from the Flask configuration object.\nThe available options are shown in the table below.\n\n=============================== ====== =============== =======================================================================================================\nName                            Type   Default         Description\n=============================== ====== =============== =======================================================================================================\n``APIFAIRY_TITLE``              String No title        The API's title.\n``APIFAIRY_VERSION``            String No version      The API's version.\n``APIFAIRY_APISPEC_PATH``       String */apispec.json* The URL path where the JSON OpenAPI specification for this project is served.\n``APIFAIRY_APISPEC_VERSION``    String ``None``        The version of the OpenAPI specification to generate for this project.\n``APIFAIRY_APISPEC_DECORATORS`` List   []              A list of decorators to apply to the JSON OpenAPI endpoint.\n``APIFAIRY_UI``                 String redoc           The documentation format to use. Supported formats are \"redoc\", \"swagger_ui\", \"rapidoc\" and \"elements\".\n``APIFAIRY_UI_PATH``            String */docs*         The URL path where the documentation is served.\n``APIFAIRY_UI_DECORATORS``      List   []              A list of decorators to apply to the documentation endpoint.\n``APIFAIRY_TAGS``               List   ``None``        A list of tags to include in the documentation, in the desired order.\n=============================== ====== =============== =======================================================================================================\n\nUsing a Custom Documentation Endpoint\n-------------------------------------\n\nAPIFairy provides templates for a few popular open source OpenAPI documentation renderers:\n\n- ``swagger_ui``: `Swagger UI <https://github.com/swagger-api/swagger-ui>`_\n- ``redoc``: `ReDoc <https://github.com/Redocly/redoc>`_\n- ``rapidoc``: `RapiDoc <https://github.com/mrin9/RapiDoc>`_\n- ``elements``: `Elements <https://github.com/stoplightio/elements>`_\n\nIf neither of these work for your project, or if you would like to configure\nany of these differently, you can set the ``APIFAIRY_UI_PATH`` to ``None`` in\nthe configuration to disable the default documentation endpoint, and then\nimplement your own.\n\nThe stock documentation options offered by this package are implemented as\nJinja2 templates, which you can `view on GitHub <https://github.com/miguelgrinberg/APIFairy/tree/main/src/apifairy/templates/apifairy>`_.\nTo implement a custom documentation, just create an endpoint in your Flask\napplication and render your own template, using the\n``{{ url_for('apifairy.json') }}`` expression where your documentation\nrenderer needs the API specification URL.\n\n.. note::\n    When using a custom documentation endpoint, the ``APIFAIRY_UI_PATH`` and\n    ``APIFAIRY_UI_DECORATORS`` configuration options are ignored.\n\nWhile less useful, the JSON OpenAPI specification endpoint can also be\ncustomized by setting the ``APIFAIRY_APISPEC_PATH`` configuration option to\n``None``. If a custom version of this endpoint is used, then the documentation\nendpoint must also be provided by the application.\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.http://sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\nFor a non-trivial example that uses Flask, Marshmallow and APIFairy together,\nsee the [microblog-api](https://github.com/miguelgrinberg/microblog-api) project.\n\nThis directory contains simpler examples that can be used as a starting point\nwhen building your own project.\n"
  },
  {
    "path": "examples/app.py",
    "content": "\"\"\"Welcome to the APIFairy Simple Example project!\n\n## Overview\n\nThis is a short and simple example that demonstrates many of the features of\nAPIFairy.\n\"\"\"\nfrom typing import Annotated\nfrom uuid import uuid4\nfrom flask import Flask, abort\nfrom flask_marshmallow import Marshmallow\nfrom apifairy import APIFairy, body, response, other_responses\n\napp = Flask(__name__)\napp.config['APIFAIRY_TITLE'] = 'APIFairy Simple Example'\napp.config['APIFAIRY_VERSION'] = '1.0'\nma = Marshmallow(app)\napifairy = APIFairy(app)\nusers = []\n\n\nclass UserSchema(ma.Schema):\n    class Meta:\n        description = 'This schema represents a user'\n\n    id = ma.String(dump_only=True, metadata={\"description\": \"The user's id\"})\n    username = ma.String(required=True, metadata={\"description\": \"The user's username\"})\n    first_name = ma.String(metadata={\"description\": \"The user's first name\"})\n    last_name = ma.String(metadata={\"description\": \"The user's last name\"})\n    age = ma.Integer(metadata={\"description\": \"The user's age\"})\n    password = ma.String(load_only=True, metadata={\"description\": \"The user's password\"})\n\n\n@app.get('/users')\n@response(UserSchema(many=True), description=\"The users\")\ndef get_users():\n    \"\"\"Return all the users.\"\"\"\n    return users\n\n\n@app.post('/users')\n@body(UserSchema)\n@response(UserSchema, description=\"The new user\")\n@other_responses({400: 'Duplicate username or validation error'})\ndef new_user(user):\n    \"\"\"Create a new user.\"\"\"\n    if any([u['username'] == user['username'] for u in users]):\n        abort(400)\n    new_id = uuid4().hex\n    user['id'] = new_id\n    users.append(user)\n    return user\n\n\n@app.get('/users/<id>')\n@response(UserSchema, description=\"The requested user\")\n@other_responses({404: 'User not found'})\ndef get_user(id: Annotated[str, 'The id of the user']):\n    \"\"\"Return a user.\"\"\"\n    user = [u for u in users if u['id'] == id]\n    if not user:\n        abort(404)\n    return user[0]\n\n\n@app.errorhandler(400)\ndef bad_request(e):\n    return {'code': 400, 'error': 'bad request'}\n\n\n@app.errorhandler(404)\ndef not_found(e):\n    return {'code': 404, 'error': 'not found'}\n\n\n@apifairy.error_handler\ndef validation_error(status_code, messages):\n    return {'code': status_code, 'error': 'validation error',\n            'messages': messages['json']}\n"
  },
  {
    "path": "examples/app_with_class_views.py",
    "content": "\"\"\"Welcome to the APIFairy Simple Example project!\n\n## Overview\n\nThis is a short and simple example that demonstrates many of the features of\nAPIFairy. The difference between this version of the example and `app.py` is\nthat in this example class-based views are used.\n\"\"\"\nfrom typing import Annotated\nfrom uuid import uuid4\nfrom flask import Flask, abort\nfrom flask.views import MethodView\nfrom flask_marshmallow import Marshmallow\nfrom apifairy import APIFairy, body, response, other_responses\n\napp = Flask(__name__)\napp.config['APIFAIRY_TITLE'] = 'APIFairy Simple Example'\napp.config['APIFAIRY_VERSION'] = '1.0'\nma = Marshmallow(app)\napifairy = APIFairy(app)\nusers = []\n\n\nclass UserSchema(ma.Schema):\n    class Meta:\n        description = 'This schema represents a user'\n\n    id = ma.String(dump_only=True, metadata={\"description\": \"The user's id\"})\n    username = ma.String(required=True, metadata={\"description\": \"The user's username\"})\n    first_name = ma.String(metadata={\"description\": \"The user's first name\"})\n    last_name = ma.String(metadata={\"description\": \"The user's last name\"})\n    age = ma.Integer(metadata={\"description\": \"The user's age\"})\n    password = ma.String(load_only=True, metadata={\"description\": \"The user's password\"})\n\n\nclass GetUsersEndpoint(MethodView):\n    decorators= [\n        response(UserSchema(many=True), description=\"The users\"),\n    ]\n\n    def get(self):\n        \"\"\"Return all the users.\"\"\"\n        return users\n     \n\nclass NewUserEndpoint(MethodView):\n    decorators = [\n        other_responses({400: 'Duplicate username or validation error'}),\n        response(UserSchema, description=\"The new user\"),\n        body(UserSchema),\n    ]\n\n    # important note: endpoints like this one that take arguments from APIFairy\n    # are currently broken, due to a bug in Flask\n    # see https://github.com/pallets/flask/issues/5199 \n    def post(self, user):\n        \"\"\"Create a new user.\"\"\"\n        if any([u['username'] == user['username'] for u in users]):\n            abort(400)\n        new_id = uuid4().hex\n        user['id'] = new_id\n        users.append(user)\n        return user\n\n\nclass UserEndpoint(MethodView):\n    decorators = [\n        response(UserSchema, description=\"The requested user\"),\n        other_responses({404: 'User not found'}),\n    ]\n\n    def get(self, id: Annotated[str, 'The id of the user']):\n        \"\"\"Return a user.\"\"\"\n        user = [u for u in users if u['id'] == id]\n        if not user:\n            abort(404)\n        return user[0]\n\n\napp.add_url_rule(\"/users\", view_func=GetUsersEndpoint.as_view(\"get_users\"))\napp.add_url_rule(\"/users\", view_func=NewUserEndpoint.as_view(\"new_user\"))\napp.add_url_rule(\"/user/<id>\", view_func=UserEndpoint.as_view(\"get_user\"))\n\n\n@app.errorhandler(400)\ndef bad_request(e):\n    return {'code': 400, 'error': 'bad request'}\n\n\n@app.errorhandler(404)\ndef not_found(e):\n    return {'code': 404, 'error': 'not found'}\n\n\n@apifairy.error_handler\ndef validation_error(status_code, messages):\n    return {'code': status_code, 'error': 'validation error',\n            'messages': messages['json']}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"apifairy\"\nversion = \"1.5.2.dev0\"\nauthors = [\n    { name = \"Miguel Grinberg\", email = \"miguel.grinberg@gmail.com\" },\n]\ndescription = \"A minimalistic API framework built on top of Flask, Marshmallow and friends.\"\nclassifiers = [\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python :: 3\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Operating System :: OS Independent\",\n]\nrequires-python = \">=3.9\"\ndependencies = [\n    \"flask >= 1.1.0\",\n    \"flask-marshmallow\",\n    \"webargs >= 8.3.0\",\n    \"flask-httpauth >= 4\",\n    \"apispec >= 4\",\n]\n\n[project.readme]\nfile = \"README.md\"\ncontent-type = \"text/markdown\"\n\n[project.urls]\nHomepage = \"https://github.com/miguelgrinberg/apifairy\"\n\"Bug Tracker\" = \"https://github.com/miguelgrinberg/apifairy/issues\"\n\n[project.optional-dependencies]\ndocs = [\n    \"sphinx\",\n]\ndev = [\n    \"tox\",\n]\n\n[tool.setuptools]\nzip-safe = false\ninclude-package-data = false\n\n[tool.setuptools.package-dir]\n\"\" = \"src\"\n\n[tool.setuptools.packages.find]\nwhere = [\n    \"src\",\n]\nnamespaces = false\n\n[tool.setuptools.package-data]\napifairy = [\n    \"templates/apifairy/*.html\",\n]\n\n[build-system]\nrequires = [\n    \"setuptools>=61.2\",\n]\nbuild-backend = \"setuptools.build_meta\"\n"
  },
  {
    "path": "src/apifairy/__init__.py",
    "content": "from .core import APIFairy  # noqa: F401\nfrom .decorators import authenticate, arguments, body, response, \\\n    other_responses, webhook  # noqa: F401\nfrom .fields import FileField  # noqa: F401\n"
  },
  {
    "path": "src/apifairy/core.py",
    "content": "from json import dumps\nimport re\nimport sys\ntry:\n    from typing import _AnnotatedAlias\nexcept ImportError:  # pragma: no cover\n    _AnnotatedAlias = None\n\nfrom apispec import APISpec\nfrom apispec.ext.marshmallow import MarshmallowPlugin\nfrom flask import current_app, Blueprint, render_template, request\nfrom flask_marshmallow import fields\ntry:\n    from flask_marshmallow import sqla\nexcept ImportError:\n    sqla = None\ntry:\n    from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth\nexcept ImportError:  # pragma: no cover\n    HTTPBasicAuth = None\n    HTTPTokenAuth = None\nfrom packaging.version import Version\nfrom werkzeug.http import HTTP_STATUS_CODES\n\nfrom apifairy.decorators import _webhooks\nfrom apifairy.exceptions import ValidationError\nfrom apifairy import fields as apifairy_fields\n\n\nclass APIFairy:\n    def __init__(self, app=None):\n        self.title = None\n        self.version = None\n        self.apispec_path = None\n        self.ui = None\n        self.ui_path = None\n        self.tags = None\n\n        self.apispec_callback = None\n        self.error_handler_callback = self.default_error_handler\n        self._apispec = None\n        if app is not None:  # pragma: no cover\n            self.init_app(app)\n\n    def init_app(self, app):\n        self.title = app.config.get('APIFAIRY_TITLE', 'No title')\n        self.version = app.config.get('APIFAIRY_VERSION', 'No version')\n        self.apispec_path = app.config.get('APIFAIRY_APISPEC_PATH',\n                                           '/apispec.json')\n        self.apispec_version = app.config.get('APIFAIRY_APISPEC_VERSION', None)\n        self.apispec_decorators = app.config.get(\n            'APIFAIRY_APISPEC_DECORATORS', [])\n        self.ui = app.config.get('APIFAIRY_UI', 'redoc')\n        self.ui_path = app.config.get('APIFAIRY_UI_PATH', '/docs')\n        self.ui_decorators = app.config.get('APIFAIRY_UI_DECORATORS', [])\n        self.tags = app.config.get('APIFAIRY_TAGS')\n\n        bp = Blueprint('apifairy', __name__, template_folder='templates')\n\n        if self.apispec_path:\n            def json():\n                return dumps(self.apispec), 200, \\\n                    {'Content-Type': 'application/json'}\n\n            for decorator in self.apispec_decorators:\n                json = decorator(json)\n            bp.add_url_rule(self.apispec_path, 'json', json)\n\n        if self.ui_path:\n            def docs():\n                return render_template(f'apifairy/{self.ui}.html',\n                                       title=self.title, version=self.version)\n\n            for decorator in self.ui_decorators:\n                docs = decorator(docs)\n            bp.add_url_rule(self.ui_path, 'docs', docs)\n\n        if self.apispec_path or self.ui_path:  # pragma: no cover\n            app.register_blueprint(bp)\n\n        @app.errorhandler(ValidationError)\n        def http_error(error):\n            return self.error_handler_callback(error.status_code,\n                                               error.messages)\n\n    def process_apispec(self, f):\n        self.apispec_callback = f\n        return f\n\n    def error_handler(self, f):\n        self.error_handler_callback = f\n        return f\n\n    def default_error_handler(self, status_code, messages):\n        return {'messages': messages}, status_code\n\n    @property\n    def apispec(self):\n        if self._apispec is None:\n            self._apispec = self._generate_apispec()\n            if self.apispec_callback:\n                self._apispec = self.apispec_callback(self._apispec)\n        return self._apispec\n\n    def _generate_apispec(self):\n        def resolver(schema):\n            name = schema.__class__.__name__\n            if name.endswith(\"Schema\"):\n                name = name[:-6] or name\n            if schema.partial:\n                name += 'Update'\n            return name\n\n        # info object\n        info = {}\n        module_name = current_app.import_name\n        while module_name:\n            module = sys.modules[module_name]\n            if module.__doc__:  # pragma: no cover\n                info['description'] = module.__doc__.strip()\n                break\n            if '.' not in module_name:\n                module_name = '.' + module_name\n            module_name = module_name.rsplit('.', 1)[0]\n\n        # servers\n        servers = [{'url': request.url_root}]\n\n        # tags\n        tag_names = self.tags\n        if tag_names is None:  # pragma: no branch\n            # auto-generate tags from blueprints\n            tag_names = []\n            for rule in current_app.url_map.iter_rules():\n                view_func = current_app.view_functions[rule.endpoint]\n                if hasattr(view_func, '_spec'):\n                    if '.' in rule.endpoint:\n                        blueprint = rule.endpoint.rsplit('.', 1)[0]\n                        if blueprint not in tag_names:  # pragma: no branch\n                            tag_names.append(blueprint)\n        tags = {}\n        for name, blueprint in current_app.blueprints.items():\n            if name not in tag_names:\n                continue\n            module = sys.modules[blueprint.import_name]\n            tag = {'name': name.title()}\n            if module.__doc__:  # pragma: no cover\n                tag['description'] = module.__doc__.strip()\n            tags[name] = tag\n        tag_list = [tags[name] for name in tag_names]\n        ma_plugin = MarshmallowPlugin(schema_name_resolver=resolver)\n        apispec_version = self.apispec_version\n        if apispec_version is None:\n            apispec_version = '3.1.0' if _webhooks else '3.0.3'\n        version = Version(apispec_version)\n        if version < Version('3.0.3'):\n            raise RuntimeError(\"Must use at openapi version '3.0.3' or newer\")\n        elif version < Version('3.1.0') and _webhooks:\n            raise RuntimeError(\"Must use at least openapi version '3.1.0' \"\n                               'when using the @webhook decorator')\n        spec = APISpec(\n            title=self.title,\n            version=self.version,\n            openapi_version=apispec_version,\n            plugins=[ma_plugin],\n            info=info,\n            servers=servers,\n            tags=tag_list,\n        )\n\n        # configure flask-marshmallow URL types\n        ma_plugin.converter.field_mapping[fields.URLFor] = ('string', 'url')\n        ma_plugin.converter.field_mapping[fields.AbsoluteURLFor] = \\\n            ('string', 'url')\n        if sqla is not None:  # pragma: no cover\n            ma_plugin.converter.field_mapping[sqla.HyperlinkRelated] = \\\n                ('string', 'url')\n\n        # configure FileField\n        ma_plugin.converter.field_mapping[apifairy_fields.FileField] = \\\n            ('string', 'binary')\n\n        # security schemes\n        auth_schemes = []\n        auth_names = []\n        for rule in current_app.url_map.iter_rules():\n            view_func = current_app.view_functions[rule.endpoint]\n            if hasattr(view_func, '_spec'):\n                auth = view_func._spec.get('auth')\n                if auth is not None and auth not in auth_schemes:\n                    auth_schemes.append(auth)\n                    if isinstance(auth, HTTPBasicAuth):\n                        name = 'basic_auth'\n                    elif isinstance(auth, HTTPTokenAuth):\n                        if auth.scheme == 'Bearer' and auth.header is None:\n                            name = 'token_auth'\n                        else:\n                            name = 'api_key'\n                    else:  # pragma: no cover\n                        raise RuntimeError('Unknown authentication scheme')\n                    if name in auth_names:\n                        apispec_version = 2\n                        new_name = f'{name}_{apispec_version}'\n                        while new_name in auth_names:  # pragma: no cover\n                            apispec_version += 1\n                            new_name = f'{name}_{apispec_version}'\n                        name = new_name\n                    auth_names.append(name)\n        security = {}\n        security_schemes = {}\n        for name, auth in zip(auth_names, auth_schemes):\n            security[auth] = name\n            if isinstance(auth, HTTPTokenAuth):\n                if auth.scheme == 'Bearer' and auth.header is None:\n                    security_schemes[name] = {\n                        'type': 'http',\n                        'scheme': 'bearer',\n                    }\n                else:\n                    security_schemes[name] = {\n                        'type': 'apiKey',\n                        'name': auth.header,\n                        'in': 'header',\n                    }\n            else:\n                security_schemes[name] = {\n                    'type': 'http',\n                    'scheme': 'basic',\n                }\n            if auth.__doc__:\n                security_schemes[name]['description'] = auth.__doc__.strip()\n        for prefix in ['basic_auth', 'token_auth', 'api_key']:\n            for name, scheme in security_schemes.items():\n                if name.startswith(prefix):\n                    spec.components.security_scheme(name, scheme)\n\n        # paths\n        paths = {}\n        rules = list(current_app.url_map.iter_rules())\n        rules = sorted(rules, key=lambda rule: len(rule.rule))\n        rules += _webhooks.values()\n        for rule in rules:\n            operations = {}\n            is_endpoint = True  # False for webhooks\n            view_func = current_app.view_functions.get(rule.endpoint)\n            if view_func is None:\n                is_endpoint = False\n                view_func = rule.view_func\n            if not hasattr(view_func, '_spec'):\n                continue\n            if '.' in rule.endpoint:\n                tag, endpoint = rule.endpoint.rsplit('.', 1)\n                tag = tag.title()\n            else:\n                tag = None\n                endpoint = rule.endpoint\n            methods = [method for method in rule.methods\n                       if method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']]\n            for method in methods:\n                operation_id = rule.endpoint.replace('.', '_')\n                if len(methods) > 1:\n                    operation_id = method.lower() + '_' + operation_id\n                operation = {\n                    'operationId': operation_id,\n                    'parameters': [\n                        {'in': location, 'schema': schema}\n                        for schema, location in view_func._spec.get('args', [])\n                        if location != 'body'\n                    ],\n                }\n                if tag:\n                    operation['tags'] = [tag]\n                docs = [line.strip() for line in (\n                    view_func.__doc__ or '').strip().split('\\n')]\n                if docs[0]:\n                    operation['summary'] = docs[0]\n                if len(docs) > 1:\n                    operation['description'] = '\\n'.join(docs[1:]).strip()\n                if view_func._spec.get('response'):\n                    code = str(view_func._spec['status_code'])\n                    operation['responses'] = {\n                        code: {\n                            'content': {\n                                'application/json': {\n                                    'schema': view_func._spec.get('response')\n                                }\n                            }\n                        }\n                    }\n                    operation['responses'][code]['description'] = \\\n                        view_func._spec['description'] or HTTP_STATUS_CODES[\n                            int(code)]\n                    if view_func._spec.get('response_headers'):\n                        schema = view_func._spec.get('response_headers')\n                        if isinstance(schema, type):  # pragma: no branch\n                            schema = schema()\n                        headers = ma_plugin.converter.schema2parameters(\n                            schema, location='headers')\n                        operation['responses'][code]['headers'] = {\n                            header['name']: header for header in headers}\n                else:\n                    operation['responses'] = {\n                        '204': {'description': HTTP_STATUS_CODES[204]}}\n\n                if view_func._spec.get('other_responses'):\n                    for status_code, response in view_func._spec.get(\n                            'other_responses').items():\n                        if not isinstance(response, (tuple, list)):\n                            response = (response,)\n                        operation['responses'][status_code] = {}\n                        for r in response:\n                            if isinstance(r, str):\n                                operation['responses'][status_code][\n                                    'description'] = r\n                            else:\n                                if isinstance(r, type):\n                                    r = r()  # instantiate the schema\n                                operation['responses'][status_code][\n                                    'content'] = {\n                                        'application/json': {\n                                            'schema': r\n                                        }\n                                    }\n                        if 'description' not in operation['responses'][\n                                status_code]:\n                            operation['responses'][status_code][\n                                'description'] = HTTP_STATUS_CODES[\n                                    int(status_code)]\n\n                if view_func._spec.get('body'):\n                    schema = view_func._spec.get('body')[0]\n                    location = view_func._spec.get('body')[1]\n                    media_type = view_func._spec.get('body')[2]\n                    if media_type is None and location == 'form':\n                        has_file = False\n                        for field in schema.dump_fields.values():\n                            if isinstance(field, apifairy_fields.FileField):\n                                has_file = True\n                                break\n                        media_type = 'application/x-www-form-urlencoded' \\\n                            if not has_file else 'multipart/form-data'\n                    if media_type is None:\n                        media_type = 'application/json'\n                    operation['requestBody'] = {\n                        'content': {\n                            media_type: {\n                                'schema': schema,\n                            }\n                        },\n                        'required': True,\n                    }\n\n                if view_func._spec.get('auth'):\n                    operation['security'] = [{\n                        security[view_func._spec['auth']]: view_func._spec[\n                            'roles']\n                    }]\n                operations[method.lower()] = operation\n\n            if is_endpoint:\n                path_arguments = re.findall(r'<(([^<:]+:)?([^>]+))>',\n                                            rule.rule)\n                if path_arguments:\n                    annotations = view_func.__annotations__ or {}\n                    arguments = []\n                    for _, type_, name in path_arguments:\n                        argument = {\n                            'in': 'path',\n                            'name': name,\n                        }\n                        if type_ == 'int:':\n                            argument['schema'] = {'type': 'integer'}\n                        elif type_ == 'float:':\n                            argument['schema'] = {'type': 'number'}\n                        else:\n                            argument['schema'] = {'type': 'string'}\n                        if isinstance(annotations.get(name), str):\n                            argument['description'] = annotations[name]\n                        elif _AnnotatedAlias and isinstance(\n                                annotations.get(name), _AnnotatedAlias):\n                            for annotation in annotations[name].__metadata__:\n                                if isinstance(annotation, str):\n                                    argument['description'] = annotation\n                                    break\n                        arguments.append(argument)\n\n                    for method, operation in operations.items():\n                        operation['parameters'] = arguments + \\\n                            operation['parameters']\n\n                path = re.sub(r'<([^<:]+:)?', '{', rule.rule).replace('>', '}')\n                if path not in paths:\n                    paths[path] = operations\n                else:\n                    paths[path].update(operations)\n            else:\n                # apispec does not support webhooks, so here they are added as\n                # paths, and later they are moved to their own section after\n                # the spec is generated\n                paths['webhook:' + endpoint] = operations\n        for path, operations in paths.items():\n            # sort by method before adding them to the spec\n            sorted_operations = {}\n            for method in ['get', 'post', 'put', 'patch', 'delete']:\n                if method in operations:\n                    sorted_operations[method] = operations[method]\n            spec.path(path=path, operations=sorted_operations)\n\n        spec = spec.to_dict()\n\n        # extract webhooks from paths and add them to the webhooks section\n        webhooks = {\n            path[8:]: operations for path, operations in spec['paths'].items()\n            if path.startswith('webhook:')\n        }\n        if webhooks:\n            paths = {\n                path: operations for path, operations in spec['paths'].items()\n                if not path.startswith('webhook:')\n            }\n            spec['paths'] = paths\n            spec['webhooks'] = webhooks\n        return spec\n"
  },
  {
    "path": "src/apifairy/decorators.py",
    "content": "from functools import wraps\n\nfrom flask import current_app, Response\nfrom webargs.flaskparser import FlaskParser as BaseFlaskParser\n\nfrom apifairy.exceptions import ValidationError\n\n\nclass FlaskParser(BaseFlaskParser):\n    USE_ARGS_POSITIONAL = False\n    DEFAULT_VALIDATION_STATUS = 400\n\n    def load_form(self, req, schema):\n        return {**self.load_files(req, schema),\n                **super().load_form(req, schema)}\n\n    def handle_error(self, error, req, schema, *, error_status_code,\n                     error_headers):\n        raise ValidationError(\n            error_status_code or self.DEFAULT_VALIDATION_STATUS,\n            error.messages)\n\n\nparser = FlaskParser()\nuse_args = parser.use_args\n_webhooks = {}\n\n\ndef _ensure_sync(f):\n    if hasattr(f, '_sync_ensured'):\n        return f\n\n    @wraps(f)\n    def wrapper(*args, **kwargs):\n        if hasattr(current_app, 'ensure_sync'):\n            return current_app.ensure_sync(f)(*args, **kwargs)\n        else:  # pragma: no cover\n            return f(*args, **kwargs)\n\n    wrapper._sync_ensured = True\n    return wrapper\n\n\ndef _annotate(f, **kwargs):\n    if not hasattr(f, '_spec'):\n        f._spec = {}\n    for key, value in kwargs.items():\n        f._spec[key] = value\n\n\ndef authenticate(auth, **kwargs):\n    def decorator(f):\n        roles = kwargs.get('role')\n        if not isinstance(roles, list):  # pragma: no cover\n            roles = [roles] if roles is not None else []\n        f = _ensure_sync(f)\n        _annotate(f, auth=auth, roles=roles)\n        return auth.login_required(**kwargs)(f)\n    return decorator\n\n\ndef arguments(schema, location='query', **kwargs):\n    if isinstance(schema, type):  # pragma: no cover\n        schema = schema()\n\n    def decorator(f):\n        f = _ensure_sync(f)\n        if not hasattr(f, '_spec') or f._spec.get('args') is None:\n            _annotate(f, args=[])\n        f._spec['args'].append((schema, location))\n        arg_name = f'{location}_{schema.__class__.__name__}_args'\n\n        @wraps(f)\n        def _f(*args, **kwargs):\n            location_args = kwargs.pop(arg_name, {})\n            return f(*args, location_args, **kwargs)\n\n        return use_args(schema, location=location, arg_name=arg_name,\n                        **kwargs)(_f)\n    return decorator\n\n\ndef body(schema, location='json', media_type=None, **kwargs):\n    if isinstance(schema, type):  # pragma: no cover\n        schema = schema()\n\n    def decorator(f):\n        f = _ensure_sync(f)\n        _annotate(f, body=(schema, location, media_type))\n        arg_name = f'{location}_{schema.__class__.__name__}_args'\n\n        @wraps(f)\n        def _f(*args, **kwargs):\n            location_args = kwargs.pop(arg_name, {})\n            return f(*args, location_args, **kwargs)\n\n        return use_args(schema, location=location, arg_name=arg_name,\n                        **kwargs)(_f)\n    return decorator\n\n\ndef response(schema, status_code=200, description=None, headers=None):\n    if isinstance(schema, type):  # pragma: no cover\n        schema = schema()\n\n    def decorator(f):\n        f = _ensure_sync(f)\n        _annotate(f, response=schema, status_code=status_code,\n                  description=description, response_headers=headers)\n\n        @wraps(f)\n        def _response(*args, **kwargs):\n            rv = f(*args, **kwargs)\n            if isinstance(rv, Response):  # pragma: no cover\n                raise RuntimeError(\n                    'The @response decorator cannot handle Response objects.')\n            if isinstance(rv, tuple):\n                json = schema.jsonify(rv[0])\n                if len(rv) == 2:\n                    if not isinstance(rv[1], int):\n                        rv = (json, status_code, rv[1])\n                    else:\n                        rv = (json, rv[1])\n                elif len(rv) >= 3:\n                    rv = (json, rv[1], rv[2])\n                else:\n                    rv = (json, status_code)\n                return rv\n            else:\n                return schema.jsonify(rv), status_code\n        return _response\n    return decorator\n\n\ndef other_responses(responses):\n    def decorator(f):\n        f = _ensure_sync(f)\n        _annotate(f, other_responses=responses)\n        return f\n    return decorator\n\n\ndef webhook(method='GET', blueprint=None, endpoint=None):\n    def decorator(f):\n        class WebhookRule:\n            def __init__(self, view_func, endpoint, methods):\n                self.view_func = view_func\n                self.endpoint = endpoint\n                self.methods = methods\n\n        nonlocal endpoint\n        endpoint = endpoint or f.__name__\n        if blueprint is not None:\n            endpoint = blueprint.name + '.' + endpoint\n        if endpoint not in _webhooks:\n            _webhooks[endpoint] = WebhookRule(f, endpoint, methods=[method])\n        else:\n            raise ValueError(f'Webhook {endpoint} has been defined twice')\n        return f\n\n    if callable(method) and blueprint is None and endpoint is None:\n        # invoked as a decorator without arguments\n        f = method\n        method = 'GET'\n        return decorator(f)\n    else:\n        # invoked as a decorator with arguments\n        return decorator\n"
  },
  {
    "path": "src/apifairy/exceptions.py",
    "content": "class ValidationError(Exception):\n    def __init__(self, status_code, messages):\n        self.status_code = status_code\n        self.messages = messages\n"
  },
  {
    "path": "src/apifairy/fields.py",
    "content": "from marshmallow import ValidationError\nfrom marshmallow.fields import Field\nfrom werkzeug.datastructures import FileStorage\n\n\nclass FileField(Field):\n    def _deserialize(self, value, attr, data, **kwargs):\n        if not isinstance(value, FileStorage):\n            raise ValidationError('Not a file.')\n        return value\n"
  },
  {
    "path": "src/apifairy/templates/apifairy/elements.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <title>{{ title }} {{ version }}</title>\n    <!-- Embed elements Elements via Web Component -->\n    <script src=\"https://unpkg.com/@stoplight/elements/web-components.min.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://unpkg.com/@stoplight/elements/styles.min.css\">\n  </head>\n  <body>\n    <elements-api\n      apiDescriptionUrl=\"{{ url_for('apifairy.json') }}\"\n      router=\"hash\"\n      layout=\"sidebar\"\n    />\n  </body>\n</html>\n"
  },
  {
    "path": "src/apifairy/templates/apifairy/rapidoc.html",
    "content": "<!doctype html> <!-- Important: must specify -->\n<html>\n  <head>\n    <title>{{ title }} {{ version }}</title>\n    <meta charset=\"utf-8\"> <!-- Important: rapi-doc uses utf8 characters -->\n    <script type=\"module\" src=\"https://unpkg.com/rapidoc/dist/rapidoc-min.js\"></script>\n  </head>\n  <body>\n    <rapi-doc spec-url=\"{{ url_for('apifairy.json') }}\" show-header=\"false\">\n    </rapi-doc>\n  </body>\n</html>\n"
  },
  {
    "path": "src/apifairy/templates/apifairy/redoc.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>{{ title }} {{ version }}</title>\n    <meta charset=\"utf-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700\" rel=\"stylesheet\">\n\n    <style>\n      body {\n        margin: 0;\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <redoc spec-url=\"{{ url_for('apifairy.json') }}\"></redoc>\n    <script src=\"https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js\"> </script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/apifairy/templates/apifairy/swagger_ui.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>{{ title }} {{ version }}</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"https://unpkg.com/swagger-ui-dist/swagger-ui.css\" >\n    <style>\n      html\n      {\n        box-sizing: border-box;\n        overflow: -moz-scrollbars-vertical;\n        overflow-y: scroll;\n      }\n\n      *,\n      *:before,\n      *:after\n      {\n        box-sizing: inherit;\n      }\n\n      body\n      {\n        margin:0;\n        background: #fafafa;\n      }\n    </style>\n  </head>\n\n  <body>\n    <div id=\"swagger-ui\"></div>\n\n    <script src=\"https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js\"></script>\n    <script src=\"https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js\"></script>\n    <script>\n      window.onload = function() {\n      const ui = SwaggerUIBundle({\n        url: \"{{ url_for('apifairy.json') }}\",\n        dom_id: '#swagger-ui',\n        deepLinking: true,\n        presets: [\n          SwaggerUIBundle.presets.apis,\n          SwaggerUIStandalonePreset\n        ],\n        plugins: [\n          SwaggerUIBundle.plugins.DownloadUrl\n        ],\n        layout: \"BaseLayout\"\n      })\n    }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_apifairy.py",
    "content": "from io import BytesIO\nimport sys\ntry:\n    from typing import Annotated\nexcept ImportError:\n    Annotated = None\nimport unittest\nimport pytest\n\nfrom flask import Flask, Blueprint, request, session, abort\nfrom flask_httpauth import HTTPBasicAuth, HTTPTokenAuth\nfrom flask_marshmallow import Marshmallow\nfrom marshmallow import EXCLUDE\nfrom openapi_spec_validator import validate_spec\n\nfrom apifairy import APIFairy, body, arguments, response, authenticate, \\\n    other_responses, webhook, FileField\n\nma = Marshmallow()\n\n\nclass Schema(ma.Schema):\n    class Meta:\n        unknown = EXCLUDE\n\n    id = ma.Integer(dump_default=123)\n    name = ma.Str(required=True)\n\n\nclass Schema2(ma.Schema):\n    class Meta:\n        unknown = EXCLUDE\n\n    id2 = ma.Integer(dump_default=123)\n    name2 = ma.Str(required=True)\n\n\nclass FooSchema(ma.Schema):\n    id = ma.Integer(dump_default=123)\n    name = ma.Str()\n\n\nclass QuerySchema(ma.Schema):\n    id = ma.Integer(load_default=1)\n\n\nclass FormSchema(ma.Schema):\n    csrf = ma.Str(required=True)\n    name = ma.Str(required=True)\n    age = ma.Int()\n\n\nclass FormUploadSchema(ma.Schema):\n    name = ma.Str()\n    file = FileField(required=True)\n\n\nclass FormUploadSchema2(ma.Schema):\n    name = ma.Str()\n    files = ma.List(FileField(), required=True)\n\n\nclass HeaderSchema(ma.Schema):\n    x_token = ma.Str(data_key='X-Token', required=True)\n\n\nclass TestAPIFairy(unittest.TestCase):\n    def create_app(self, config=None):\n        app = Flask(__name__)\n        app.config['APIFAIRY_TITLE'] = 'Foo'\n        app.config['APIFAIRY_VERSION'] = '1.0'\n        if config:\n            app.config.update(config)\n        ma.init_app(app)\n        apifairy = APIFairy(app)\n        return app, apifairy\n\n    def test_apispec(self):\n        app, apifairy = self.create_app()\n        auth = HTTPBasicAuth()\n\n        @apifairy.process_apispec\n        def edit_apispec(apispec):\n            assert apispec['openapi'] == '3.0.3'\n            apispec['openapi'] = '3.0.2'\n            return apispec\n\n        @auth.verify_password\n        def verify_password(username, password):\n            if username == 'foo' and password == 'bar':\n                return {'user': 'foo'}\n            elif username == 'bar' and password == 'foo':\n                return {'user': 'bar'}\n\n        @auth.get_user_roles\n        def get_roles(user):\n            if user['user'] == 'bar':\n                return 'admin'\n            return 'normal'\n\n        @app.route('/foo')\n        @authenticate(auth)\n        @arguments(QuerySchema)\n        @body(Schema)\n        @response(Schema)\n        @other_responses({404: 'foo not found'})\n        def foo():\n            return {'id': 123, 'name': auth.current_user()['user']}\n\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        validate_spec(rv.json)\n        assert rv.json['openapi'] == '3.0.2'\n        assert rv.json['info']['title'] == 'Foo'\n        assert rv.json['info']['version'] == '1.0'\n\n        assert apifairy.apispec is apifairy.apispec\n\n        rv = client.get('/docs')\n        assert rv.status_code == 200\n        assert b'redoc.standalone.js' in rv.data\n\n    def test_custom_apispec_path(self):\n        app, _ = self.create_app(config={'APIFAIRY_APISPEC_PATH': '/foo'})\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 404\n        rv = client.get('/foo')\n        assert rv.status_code == 200\n        assert set(rv.json.keys()) == {\n            'openapi', 'info', 'servers', 'paths', 'tags'}\n\n    def test_no_apispec_path(self):\n        app, _ = self.create_app(config={'APIFAIRY_APISPEC_PATH': None})\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 404\n\n    def test_custom_apispec_version(self):\n        app, _ = self.create_app(config={'APIFAIRY_APISPEC_VERSION': '3.1.0'})\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        assert set(rv.json.keys()) == {\n            'openapi', 'info', 'servers', 'paths', 'tags'}\n        assert rv.json['openapi'] == '3.1.0'\n\n    def test_custom_apispec_default_version(self):\n        app, _ = self.create_app()\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        assert set(rv.json.keys()) == {\n            'openapi', 'info', 'servers', 'paths', 'tags'}\n        assert rv.json['openapi'] == '3.0.3'\n\n    def test_custom_apispec_invalid_version_old(self):\n        app, _ = self.create_app(\n                config={'APIFAIRY_APISPEC_VERSION': '3.0.2'})\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 500\n\n    def test_custom_apispec_invalid_version_new(self):\n        app, _ = self.create_app(\n                config={'APIFAIRY_APISPEC_VERSION': '6.1.0'})\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 500\n\n    def test_custom_apispec_non_semver_version(self):\n        app, _ = self.create_app(\n                config={'APIFAIRY_APISPEC_VERSION': 'invalid'})\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 500\n\n    def test_ui(self):\n        app, _ = self.create_app(config={'APIFAIRY_UI': 'swagger_ui'})\n\n        client = app.test_client()\n        rv = client.get('/docs')\n        assert rv.status_code == 200\n        assert b'redoc.standalone.js' not in rv.data\n        assert b'swagger-ui-bundle.js' in rv.data\n\n    def test_custom_ui_path(self):\n        app, _ = self.create_app(config={'APIFAIRY_UI_PATH': '/foo'})\n\n        client = app.test_client()\n        rv = client.get('/docs')\n        assert rv.status_code == 404\n        rv = client.get('/foo')\n        assert rv.status_code == 200\n        assert b'redoc.standalone.js' in rv.data\n\n    def test_no_ui_path(self):\n        app, _ = self.create_app(config={'APIFAIRY_UI_PATH': None})\n\n        client = app.test_client()\n        rv = client.get('/docs')\n        assert rv.status_code == 404\n\n    def test_apispec_ui_decorators(self):\n        def auth(f):\n            def wrapper(*args, **kwargs):\n                if request.headers.get('X-Token') != 'foo' and \\\n                        session.get('X-Token') != 'foo':\n                    abort(401)\n                return f(*args, **kwargs)\n            return wrapper\n\n        def more_auth(f):\n            def wrapper(*args, **kwargs):\n                if request.headers.get('X-Key') != 'bar':\n                    abort(401)\n                session['X-Token'] = 'foo'\n                return f(*args, **kwargs)\n            return wrapper\n\n        app, apifairy = self.create_app(config={\n            'APIFAIRY_APISPEC_DECORATORS': [auth],\n            'APIFAIRY_UI_DECORATORS': [auth, more_auth]})\n        app.secret_key = 'secret'\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 401\n        rv = client.get('/docs')\n        assert rv.status_code == 401\n        rv = client.get('/apispec.json', headers={'X-Token': 'foo'})\n        assert rv.status_code == 200\n        rv = client.get('/docs', headers={'X-Key': 'bar'})\n        assert rv.status_code == 200\n\n    def test_body(self):\n        app, _ = self.create_app()\n\n        @app.route('/foo', methods=['POST'])\n        @body(Schema())\n        def foo(schema):\n            return schema\n\n        client = app.test_client()\n\n        rv = client.post('/foo')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'json': {'name': ['Missing data for required field.']}\n            }\n        }\n\n        rv = client.post('/foo', json={'id': 1})\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'json': {'name': ['Missing data for required field.']}\n            }\n        }\n\n        rv = client.post('/foo', json={'id': 1, 'name': 'bar'})\n        assert rv.status_code == 200\n        assert rv.json == {'id': 1, 'name': 'bar'}\n\n        rv = client.post('/foo', json={'name': 'bar'})\n        assert rv.status_code == 200\n        assert rv.json == {'name': 'bar'}\n\n    def test_body_form(self):\n        app, _ = self.create_app()\n\n        @app.route('/form', methods=['POST'])\n        @body(FormSchema(), location='form')\n        def foo(schema):\n            return schema\n\n        client = app.test_client()\n\n        rv = client.post('/form')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'form': {\n                    'csrf': ['Missing data for required field.'],\n                    'name': ['Missing data for required field.'],\n                }\n            }\n        }\n\n        rv = client.post('/form', data={'csrf': 'foo', 'age': '12'})\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'form': {'name': ['Missing data for required field.']}\n            }\n        }\n\n        rv = client.post('/form', data={'csrf': 'foo', 'name': 'bar'})\n        assert rv.status_code == 200\n        assert rv.json == {'csrf': 'foo', 'name': 'bar'}\n\n        rv = client.post('/form', data={'csrf': 'foo', 'name': 'bar',\n                                        'age': '12'})\n        assert rv.status_code == 200\n        assert rv.json == {'csrf': 'foo', 'name': 'bar', 'age': 12}\n\n    def test_body_form_upload(self):\n        app, _ = self.create_app()\n\n        @app.route('/form', methods=['POST'])\n        @body(FormUploadSchema(), location='form')\n        def foo(schema):\n            return {'name': schema.get('name'),\n                    'len': len(schema['file'].read())}\n\n        client = app.test_client()\n\n        rv = client.post('/form')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'form': {'file': ['Missing data for required field.']}\n            }\n        }\n\n        rv = client.post('/form', data={'name': 'foo'},\n                         content_type='multipart/form-data')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'form': {'file': ['Missing data for required field.']}\n            }\n        }\n\n        rv = client.post('/form', data={'file': 'foo'},\n                         content_type='multipart/form-data')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'form': {'file': ['Not a file.']}\n            }\n        }\n\n        rv = client.post('/form', data={'name': 'foo',\n                                        'file': (BytesIO(b'bar'), 'test.txt')})\n        assert rv.status_code == 200\n        assert rv.json == {'name': 'foo', 'len': 3}\n\n        rv = client.post('/form', data={'file': (BytesIO(b'bar'), 'test.txt')})\n        assert rv.status_code == 200\n        assert rv.json == {'name': None, 'len': 3}\n\n    def test_body_custom_error_handler(self):\n        app, apifairy = self.create_app()\n\n        @apifairy.error_handler\n        def error_handler(status_code, messages):\n            return {'errors': messages}, status_code\n\n        @app.route('/foo', methods=['POST'])\n        @body(Schema())\n        def foo(schema):\n            return schema\n\n        client = app.test_client()\n\n        rv = client.post('/foo')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'errors': {\n                'json': {'name': ['Missing data for required field.']}\n            }\n        }\n\n    def test_query(self):\n        app, _ = self.create_app()\n\n        @app.route('/foo', methods=['POST'])\n        @arguments(Schema())\n        @arguments(Schema2())\n        def foo(schema, schema2):\n            return {'name': schema['name'], 'name2': schema2['name2']}\n\n        client = app.test_client()\n\n        rv = client.post('/foo')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'query': {'name': ['Missing data for required field.']}\n            }\n        }\n\n        rv = client.post('/foo?id=1&name=bar')\n        assert rv.status_code == 400\n        assert rv.json == {\n            'messages': {\n                'query': {'name2': ['Missing data for required field.']}\n            }\n        }\n\n        rv = client.post('/foo?id=1&name=bar&id2=2&name2=baz')\n        assert rv.status_code == 200\n        assert rv.json == {'name': 'bar', 'name2': 'baz'}\n\n        rv = client.post('/foo?name=bar&name2=baz')\n        assert rv.status_code == 200\n        assert rv.json == {'name': 'bar', 'name2': 'baz'}\n\n    def test_response(self):\n        app, _ = self.create_app()\n\n        @app.route('/foo')\n        @response(Schema())\n        def foo():\n            return {'name': 'bar'}\n\n        @app.route('/bar')\n        @response(Schema(), status_code=201)\n        def bar():\n            return {'name': 'foo'}\n\n        @app.route('/baz')\n        @arguments(QuerySchema)\n        @response(Schema(), status_code=201)\n        def baz(query):\n            if query['id'] == 1:\n                return {'name': 'foo'}, 202\n            elif query['id'] == 2:\n                return {'name': 'foo'}, {'Location': '/baz'}\n            elif query['id'] == 3:\n                return {'name': 'foo'}, 202, {'Location': '/baz'}\n            return ({'name': 'foo'},)\n\n        client = app.test_client()\n\n        rv = client.get('/foo')\n        assert rv.status_code == 200\n        assert rv.json == {'id': 123, 'name': 'bar'}\n\n        rv = client.get('/bar')\n        assert rv.status_code == 201\n        assert rv.json == {'id': 123, 'name': 'foo'}\n\n        rv = client.get('/baz')\n        assert rv.status_code == 202\n        assert rv.json == {'id': 123, 'name': 'foo'}\n        assert 'Location' not in rv.headers\n\n        rv = client.get('/baz?id=2')\n        assert rv.status_code == 201\n        assert rv.json == {'id': 123, 'name': 'foo'}\n        assert rv.headers['Location'] in ['http://localhost/baz', '/baz']\n\n        rv = client.get('/baz?id=3')\n        assert rv.status_code == 202\n        assert rv.json == {'id': 123, 'name': 'foo'}\n        assert rv.headers['Location'] in ['http://localhost/baz', '/baz']\n\n        rv = client.get('/baz?id=4')\n        assert rv.status_code == 201\n        assert rv.json == {'id': 123, 'name': 'foo'}\n        assert 'Location' not in rv.headers\n\n    def test_basic_auth(self):\n        app, _ = self.create_app()\n        auth = HTTPBasicAuth()\n\n        @auth.verify_password\n        def verify_password(username, password):\n            if username == 'foo' and password == 'bar':\n                return {'user': 'foo'}\n            elif username == 'bar' and password == 'foo':\n                return {'user': 'bar'}\n\n        @auth.get_user_roles\n        def get_roles(user):\n            if user['user'] == 'bar':\n                return 'admin'\n            return 'normal'\n\n        @app.route('/foo')\n        @authenticate(auth)\n        def foo():\n            return auth.current_user()\n\n        @app.route('/bar')\n        @authenticate(auth, role='admin')\n        def bar():\n            return auth.current_user()\n\n        client = app.test_client()\n\n        rv = client.get('/foo')\n        assert rv.status_code == 401\n\n        rv = client.get('/foo',\n                        headers={'Authorization': 'Basic Zm9vOmJhcg=='})\n        assert rv.status_code == 200\n        assert rv.json == {'user': 'foo'}\n\n        rv = client.get('/bar',\n                        headers={'Authorization': 'Basic Zm9vOmJhcg=='})\n        assert rv.status_code == 403\n\n        rv = client.get('/foo',\n                        headers={'Authorization': 'Basic YmFyOmZvbw=='})\n        assert rv.status_code == 200\n        assert rv.json == {'user': 'bar'}\n\n        rv = client.get('/bar',\n                        headers={'Authorization': 'Basic YmFyOmZvbw=='})\n        assert rv.status_code == 200\n        assert rv.json == {'user': 'bar'}\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        assert rv.json['components']['securitySchemes'] == {\n            'basic_auth': {'scheme': 'basic', 'type': 'http'},\n        }\n        assert rv.json['paths']['/foo']['get']['security'] == [\n            {'basic_auth': []}]\n        assert rv.json['paths']['/bar']['get']['security'] == [\n            {'basic_auth': ['admin']}]\n\n    def test_token_auth(self):\n        app, _ = self.create_app()\n        auth = HTTPTokenAuth()\n\n        @auth.verify_token\n        def verify_token(token):\n            if token == 'foo':\n                return {'user': 'foo'}\n            elif token == 'bar':\n                return {'user': 'bar'}\n\n        @auth.get_user_roles\n        def get_roles(user):\n            if user['user'] == 'bar':\n                return 'admin'\n            return 'normal'\n\n        @app.route('/foo')\n        @authenticate(auth)\n        def foo():\n            return auth.current_user()\n\n        @app.route('/bar')\n        @authenticate(auth, role='admin')\n        def bar():\n            return auth.current_user()\n\n        client = app.test_client()\n\n        rv = client.get('/foo')\n        assert rv.status_code == 401\n\n        rv = client.get('/foo',\n                        headers={'Authorization': 'Bearer foo'})\n        assert rv.status_code == 200\n        assert rv.json == {'user': 'foo'}\n\n        rv = client.get('/bar',\n                        headers={'Authorization': 'Bearer foo'})\n        assert rv.status_code == 403\n\n        rv = client.get('/foo',\n                        headers={'Authorization': 'Bearer bar'})\n        assert rv.status_code == 200\n        assert rv.json == {'user': 'bar'}\n\n        rv = client.get('/bar',\n                        headers={'Authorization': 'Bearer bar'})\n        assert rv.status_code == 200\n        assert rv.json == {'user': 'bar'}\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        assert rv.json['components']['securitySchemes'] == {\n            'token_auth': {'scheme': 'bearer', 'type': 'http'},\n        }\n        assert rv.json['paths']['/foo']['get']['security'] == [\n            {'token_auth': []}]\n        assert rv.json['paths']['/bar']['get']['security'] == [\n            {'token_auth': ['admin']}]\n\n    def test_multiple_auth(self):\n        app, _ = self.create_app()\n        auth = HTTPTokenAuth()\n        auth.__doc__ = 'auth documentation'\n        auth2 = HTTPTokenAuth(header='X-Token')\n\n        class MyHTTPTokenAuth(HTTPTokenAuth):\n            \"\"\"custom auth documentation\"\"\"\n            pass\n\n        auth3 = MyHTTPTokenAuth()\n\n        @app.route('/foo')\n        @authenticate(auth)\n        def foo():\n            return auth.current_user()\n\n        @app.route('/bar')\n        @authenticate(auth2)\n        def bar():\n            return auth.current_user()\n\n        @app.route('/baz')\n        @authenticate(auth3)\n        def baz():\n            return auth.current_user()\n\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        assert rv.json['components']['securitySchemes'] == {\n            'token_auth': {'scheme': 'bearer', 'type': 'http',\n                           'description': 'auth documentation'},\n            'token_auth_2': {'scheme': 'bearer', 'type': 'http',\n                             'description': 'custom auth documentation'},\n            'api_key': {'type': 'apiKey', 'name': 'X-Token', 'in': 'header'},\n        }\n        assert rv.json['paths']['/foo']['get']['security'] == [\n            {'token_auth': []}]\n        assert rv.json['paths']['/bar']['get']['security'] == [\n            {'api_key': []}]\n        assert rv.json['paths']['/baz']['get']['security'] == [\n            {'token_auth_2': []}]\n\n    def test_apispec_schemas(self):\n        app, apifairy = self.create_app()\n\n        @app.route('/foo')\n        @response(Schema(partial=True))\n        def foo():\n            pass\n\n        @app.route('/bar')\n        @response(Schema2(many=True))\n        def bar():\n            pass\n\n        @app.route('/baz')\n        @response(FooSchema)\n        def baz():\n            pass\n\n        with app.test_request_context():\n            apispec = apifairy.apispec\n        assert len(apispec['components']['schemas']) == 3\n        assert 'SchemaUpdate' in apispec['components']['schemas']\n        assert 'Schema2' in apispec['components']['schemas']\n        assert 'Foo' in apispec['components']['schemas']\n\n    def test_endpoints(self):\n        app, apifairy = self.create_app()\n\n        @app.route('/users')\n        @response(Schema)\n        def get_users():\n            \"\"\"get users.\"\"\"\n            pass\n\n        @app.route('/users', methods=['POST', 'PUT'])\n        @body(FormSchema, location='form')\n        @response(Schema, status_code=201)\n        @other_responses({400: (Schema2, 'bad request'),\n                          401: ('unauthorized', FooSchema()),\n                          403: 'forbidden',\n                          404: Schema2(many=True)})\n        def new_user():\n            \"\"\"new user.\n            modify user.\n            \"\"\"\n            pass\n\n        @app.route('/upload', methods=['POST'])\n        @body(FormUploadSchema, location='form')\n        def upload():\n            \"\"\"upload file.\"\"\"\n            pass\n\n        @app.route('/uploads', methods=['POST'])\n        @body(FormUploadSchema2, location='form',\n              media_type='multipart/form-data')\n        def uploads():\n            \"\"\"upload files.\"\"\"\n            pass\n\n        @app.route('/tokens', methods=['POST'])\n        @response(Schema, headers=HeaderSchema)\n        def token():\n            \"\"\"get a token.\"\"\"\n            pass\n\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n\n        assert rv.json['paths']['/users']['get']['operationId'] == 'get_users'\n        assert list(rv.json['paths']['/users']['get']['responses']) == ['200']\n        assert rv.json['paths']['/users']['get']['summary'] == 'get users.'\n        assert 'description' not in rv.json['paths']['/users']['get']\n\n        assert rv.json['paths']['/users']['post']['operationId'] == \\\n            'post_new_user'\n        assert list(rv.json['paths']['/users']['post']['responses']) == \\\n            ['201', '400', '401', '403', '404']\n        assert rv.json['paths']['/users']['post']['summary'] == 'new user.'\n        assert rv.json['paths']['/users']['post']['description'] == \\\n            'modify user.'\n        assert 'application/x-www-form-urlencoded' in \\\n            rv.json['paths']['/users']['post']['requestBody']['content']\n\n        assert rv.json['paths']['/users']['put']['operationId'] == \\\n            'put_new_user'\n        assert list(rv.json['paths']['/users']['put']['responses']) == \\\n            ['201', '400', '401', '403', '404']\n        assert rv.json['paths']['/users']['put']['summary'] == 'new user.'\n        assert rv.json['paths']['/users']['put']['description'] == \\\n            'modify user.'\n\n        assert rv.json['paths']['/upload']['post']['operationId'] == 'upload'\n        assert list(rv.json['paths']['/upload']['post']['responses']) == \\\n            ['204']\n        assert rv.json['paths']['/upload']['post']['summary'] == 'upload file.'\n        assert 'description' not in rv.json['paths']['/upload']['post']\n        assert 'multipart/form-data' in \\\n            rv.json['paths']['/upload']['post']['requestBody']['content']\n\n        assert rv.json['paths']['/uploads']['post']['operationId'] == 'uploads'\n        assert list(rv.json['paths']['/uploads']['post']['responses']) == \\\n            ['204']\n        assert rv.json['paths']['/uploads']['post']['summary'] == \\\n            'upload files.'\n        assert 'description' not in rv.json['paths']['/uploads']['post']\n        assert 'multipart/form-data' in \\\n            rv.json['paths']['/uploads']['post']['requestBody']['content']\n\n        assert rv.json['paths']['/tokens']['post']['operationId'] == 'token'\n        assert list(rv.json['paths']['/tokens']['post']['responses']) == \\\n            ['200']\n        assert rv.json['paths']['/tokens']['post']['summary'] == 'get a token.'\n        assert 'description' not in rv.json['paths']['/tokens']['post']\n        assert 'headers' in \\\n            rv.json['paths']['/tokens']['post']['responses']['200']\n        assert 'X-Token' in \\\n            rv.json['paths']['/tokens']['post']['responses']['200']['headers']\n\n        r201 = {\n            'content': {\n                'application/json': {\n                    'schema': {'$ref': '#/components/schemas/Schema'}\n                }\n            },\n            'description': 'Created'\n        }\n        assert rv.json['paths']['/users']['post']['responses']['201'] == r201\n        assert rv.json['paths']['/users']['put']['responses']['201'] == r201\n\n        r400 = {\n            'content': {\n                'application/json': {\n                    'schema': {'$ref': '#/components/schemas/Schema2'}\n                }\n            },\n            'description': 'bad request'\n        }\n        assert rv.json['paths']['/users']['post']['responses']['400'] == r400\n        assert rv.json['paths']['/users']['put']['responses']['400'] == r400\n\n        r401 = {\n            'content': {\n                'application/json': {\n                    'schema': {'$ref': '#/components/schemas/Foo'}\n                }\n            },\n            'description': 'unauthorized'\n        }\n        assert rv.json['paths']['/users']['post']['responses']['401'] == r401\n        assert rv.json['paths']['/users']['put']['responses']['401'] == r401\n\n        r403 = {'description': 'forbidden'}\n        assert rv.json['paths']['/users']['post']['responses']['403'] == r403\n        assert rv.json['paths']['/users']['put']['responses']['403'] == r403\n\n        r404 = {\n            'content': {\n                'application/json': {\n                    'schema': {\n                        'items': {'$ref': '#/components/schemas/Schema2'},\n                        'type': 'array'\n                    }\n                }\n            },\n            'description': 'Not Found'\n        }\n        assert rv.json['paths']['/users']['post']['responses']['404'] == r404\n        assert rv.json['paths']['/users']['put']['responses']['404'] == r404\n\n    def test_apispec_path_parameters(self):\n        app, apifairy = self.create_app()\n\n        @app.route('/strings/<some_string>')\n        @response(Schema)\n        def get_string(some_string: 'some_string docs'):  # noqa: F722\n            pass\n\n        @app.route('/floats/<float:some_float>', methods=['POST'])\n        @response(Schema)\n        def get_float(some_float: float):\n            pass\n\n        if Annotated:\n            @app.route('/integers/<int:some_integer>', methods=['PUT'])\n            @response(Schema)\n            def get_integer(some_integer: Annotated[int, 1,\n                                                    'some_integer docs']):\n                pass\n\n            @app.route('/users/<int:user_id>/articles/<int:article_id>')\n            @response(Schema)\n            def get_article(user_id: Annotated[int, 1], article_id):\n                pass\n        else:\n            @app.route('/integers/<int:some_integer>', methods=['PUT'])\n            @response(Schema)\n            def get_integer(some_integer: 'some_integer docs'):  # noqa: F722\n                pass\n\n            @app.route('/users/<int:user_id>/articles/<int:article_id>')\n            @response(Schema)\n            def get_article(user_id, article_id):\n                pass\n\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        validate_spec(rv.json)\n\n        assert rv.json['paths']['/strings/{some_string}'][\n            'get']['parameters'][0]['in'] == 'path'\n        assert rv.json['paths']['/strings/{some_string}'][\n            'get']['parameters'][0]['description'] == 'some_string docs'\n        assert rv.json['paths']['/strings/{some_string}'][\n            'get']['parameters'][0]['name'] == 'some_string'\n        assert rv.json['paths']['/strings/{some_string}'][\n            'get']['parameters'][0]['schema']['type'] == 'string'\n\n        assert rv.json['paths']['/floats/{some_float}'][\n            'post']['parameters'][0]['in'] == 'path'\n        assert 'description' not in rv.json['paths']['/floats/{some_float}'][\n            'post']['parameters'][0]\n        assert rv.json['paths']['/floats/{some_float}'][\n            'post']['parameters'][0]['schema']['type'] == 'number'\n        assert rv.json['paths']['/floats/{some_float}'][\n            'post']['parameters'][0]['schema']['type'] == 'number'\n\n        assert rv.json['paths']['/integers/{some_integer}'][\n            'put']['parameters'][0]['in'] == 'path'\n        assert rv.json['paths']['/integers/{some_integer}'][\n            'put']['parameters'][0]['description'] == 'some_integer docs'\n        assert rv.json['paths']['/integers/{some_integer}'][\n            'put']['parameters'][0]['schema']['type'] == 'integer'\n\n        assert rv.json['paths']['/users/{user_id}/articles/{article_id}'][\n            'get']['parameters'][0]['in'] == 'path'\n        assert rv.json['paths']['/users/{user_id}/articles/{article_id}'][\n            'get']['parameters'][0]['name'] == 'user_id'\n        assert 'description' not in rv.json['paths'][\n            '/users/{user_id}/articles/{article_id}']['get']['parameters'][0]\n        assert rv.json['paths']['/users/{user_id}/articles/{article_id}'][\n            'get']['parameters'][1]['in'] == 'path'\n        assert rv.json['paths']['/users/{user_id}/articles/{article_id}'][\n            'get']['parameters'][1]['name'] == 'article_id'\n        assert 'description' not in rv.json['paths'][\n            '/users/{user_id}/articles/{article_id}']['get']['parameters'][1]\n\n    def test_path_arguments_detection(self):\n        app, apifairy = self.create_app()\n\n        @app.route('/<foo>')\n        @response(Schema)\n        def pattern1():\n            pass\n\n        @app.route('/foo/<bar>')\n        @response(Schema)\n        def pattern2():\n            pass\n\n        @app.route('/<foo>/bar')\n        @response(Schema)\n        def pattern3():\n            pass\n\n        @app.route('/<int:foo>/<bar>/baz')\n        @response(Schema)\n        def pattern4():\n            pass\n\n        @app.route('/foo/<int:bar>/<int:baz>')\n        @response(Schema)\n        def pattern5():\n            pass\n\n        @app.route('/<int:foo>/<bar>/<float:baz>')\n        @response(Schema)\n        def pattern6():\n            pass\n\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        validate_spec(rv.json)\n        assert '/{foo}' in rv.json['paths']\n        assert '/foo/{bar}' in rv.json['paths']\n        assert '/{foo}/bar' in rv.json['paths']\n        assert '/{foo}/{bar}/baz' in rv.json['paths']\n        assert '/foo/{bar}/{baz}' in rv.json['paths']\n        assert '/{foo}/{bar}/{baz}' in rv.json['paths']\n        assert rv.json['paths']['/{foo}/{bar}/{baz}']['get'][\n            'parameters'][0]['schema']['type'] == 'integer'\n        assert rv.json['paths']['/{foo}/{bar}/{baz}']['get'][\n            'parameters'][1]['schema']['type'] == 'string'\n        assert rv.json['paths']['/{foo}/{bar}/{baz}']['get'][\n            'parameters'][2]['schema']['type'] == 'number'\n\n    def test_path_tags_with_nesting_blueprints(self):\n        if not hasattr(Blueprint, 'register_blueprint'):\n            pytest.skip('This test requires Flask 2.0 or higher.')\n\n        app, apifairy = self.create_app()\n\n        parent_bp = Blueprint('parent', __name__, url_prefix='/parent')\n        child_bp = Blueprint('child', __name__, url_prefix='/child')\n\n        @parent_bp.route('/')\n        @response(Schema)\n        def foo():\n            pass\n\n        @child_bp.route('/')\n        @response(Schema)\n        def bar():\n            pass\n\n        parent_bp.register_blueprint(child_bp)\n        app.register_blueprint(parent_bp)\n\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        validate_spec(rv.json)\n        assert {'name': 'Parent'} in rv.json['tags']\n        assert {'name': 'Parent.Child'} in rv.json['tags']\n        assert rv.json['paths']['/parent/']['get']['tags'] == ['Parent']\n        assert rv.json['paths']['/parent/child/']['get'][\n            'tags'] == ['Parent.Child']\n\n    def test_async_views(self):\n        if not sys.version_info >= (3, 7):\n            pytest.skip('This test requires Python 3.7 or higher.')\n\n        app, apifairy = self.create_app()\n        auth = HTTPBasicAuth()\n\n        @auth.verify_password\n        def verify_password(username, password):\n            if username == 'foo' and password == 'bar':\n                return {'user': 'foo'}\n            elif username == 'bar' and password == 'foo':\n                return {'user': 'bar'}\n\n        @auth.get_user_roles\n        def get_roles(user):\n            if user['user'] == 'bar':\n                return 'admin'\n            return 'normal'\n\n        @app.route('/foo', methods=['POST'])\n        @authenticate(auth)\n        @arguments(QuerySchema)\n        @body(Schema)\n        @response(Schema)\n        @other_responses({404: 'foo not found'})\n        async def foo(query, body):\n            return {'id': query['id'], 'name': auth.current_user()['user']}\n\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        validate_spec(rv.json)\n        assert rv.json['openapi'] == '3.0.3'\n        assert rv.json['info']['title'] == 'Foo'\n        assert rv.json['info']['version'] == '1.0'\n\n        assert apifairy.apispec is apifairy.apispec\n\n        rv = client.get('/docs')\n        assert rv.status_code == 200\n        assert b'redoc.standalone.js' in rv.data\n\n        rv = client.post('/foo')\n        assert rv.status_code == 401\n\n        rv = client.post(\n            '/foo', headers={'Authorization': 'Basic Zm9vOmJhcg=='})\n        assert rv.json['messages']['json']['name'] == \\\n            ['Missing data for required field.']\n        assert rv.status_code == 400\n\n        rv = client.post('/foo', json={'name': 'john'},\n                         headers={'Authorization': 'Basic Zm9vOmJhcg=='})\n        assert rv.status_code == 200\n        assert rv.json == {'id': 1, 'name': 'foo'}\n\n        rv = client.post('/foo?id=2', json={'name': 'john'},\n                         headers={'Authorization': 'Basic Zm9vOmJhcg=='})\n        assert rv.status_code == 200\n        assert rv.json == {'id': 2, 'name': 'foo'}\n\n    def test_webhook(self):\n        app, apifairy = self.create_app()\n        bp = Blueprint('bp', __name__)\n\n        @webhook\n        @body(Schema)\n        def default_webhook():\n            pass\n\n        @webhook(endpoint='my-endpoint')\n        @body(Schema)\n        def custom_endpoint():\n            pass\n\n        @webhook(method='POST')\n        @body(Schema)\n        def post_webhook():\n            pass\n\n        @webhook(endpoint='tag.tagged-webhook')\n        @body(Schema)\n        def tagged_webhook():\n            pass\n\n        @webhook(blueprint=bp)\n        @body(Schema)\n        def blueprint_webhook():\n            pass\n\n        app.register_blueprint(bp)\n        client = app.test_client()\n\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 200\n        validate_spec(rv.json)\n        assert rv.json['openapi'] == '3.1.0'\n        assert 'default_webhook' in rv.json['webhooks']\n        assert 'get' in rv.json['webhooks']['default_webhook']\n        assert 'my-endpoint' in rv.json['webhooks']\n        assert 'get' in rv.json['webhooks']['my-endpoint']\n        assert 'post_webhook' in rv.json['webhooks']\n        assert 'post' in rv.json['webhooks']['post_webhook']\n        assert 'tagged-webhook' in rv.json['webhooks']\n        assert 'get' in rv.json['webhooks']['tagged-webhook']\n        assert 'blueprint_webhook' in rv.json['webhooks']\n        assert 'get' in rv.json['webhooks']['blueprint_webhook']\n\n    def test_webhook_invalid_apispec_version(self):\n        app, apifairy = self.create_app(\n                config={'APIFAIRY_APISPEC_VERSION': '3.0.3'})\n\n        @webhook\n        @body(Schema)\n        def unsupported_webhook():\n            pass\n\n        client = app.test_client()\n        rv = client.get('/apispec.json')\n        assert rv.status_code == 500\n\n    def test_webhook_duplicate(self):\n        app, apifairy = self.create_app()\n\n        def add_webhooks():\n            @webhook\n            @body(Schema)\n            def default_webhook():\n                pass\n\n            @webhook(endpoint='default_webhook')\n            @body(Schema)\n            def another_webhook():\n                pass\n\n        with pytest.raises(ValueError):\n            add_webhooks()\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist=flake8,py39,py310,py311,py312,py313,py314,pypy3,docs\nskip_missing_interpreters=True\n\n[gh-actions]\npython =\n    3.9: py39\n    3.10: py310\n    3.11: py311\n    3.12: py312\n    3.13: py313\n    3.14: py314\n    pypy-3: pypy3\n\n[testenv]\ncommands=\n    pip install -e .\n    pytest -p no:logging --cov=apifairy --cov-branch --cov-report=term-missing --cov-report=xml\ndeps=\n    asgiref\n    pytest\n    pytest-cov\n    openapi-spec-validator\n\n[testenv:flake8]\ndeps=\n    flake8\ncommands=\n    flake8 --exclude=\".*\" src/apifairy tests\n\n[testenv:docs]\nchangedir=docs\ndeps=\n    sphinx\nallowlist_externals=\n    make\ncommands=\n    make html\n"
  }
]