├── .codecov.yml ├── .coveragerc ├── .flake8 ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── .mypy.ini ├── .pre-commit-config.yaml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── aiohttp_debugtoolbar ├── __init__.py ├── main.py ├── middlewares.py ├── panels │ ├── __init__.py │ ├── base.py │ ├── headers.py │ ├── logger.py │ ├── middlewares.py │ ├── performance.py │ ├── request_vars.py │ ├── routes.py │ ├── settings.py │ ├── templates │ │ ├── headers.jinja2 │ │ ├── logger.jinja2 │ │ ├── middlewares.jinja2 │ │ ├── performance.jinja2 │ │ ├── request_vars.jinja2 │ │ ├── routes.jinja2 │ │ ├── settings.jinja2 │ │ ├── sqlalchemy_explain.jinja2 │ │ ├── sqlalchemy_select.jinja2 │ │ ├── traceback.jinja2 │ │ └── versions.jinja2 │ ├── traceback.py │ └── versions.py ├── py.typed ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ ├── dashboard.css │ │ ├── debugger.css │ │ ├── highlightjs_default.min.css │ │ ├── prism.css │ │ ├── toolbar.css │ │ └── toolbar_button.css │ ├── font │ │ ├── FONT_LICENSE │ │ └── ubuntu.ttf │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── img │ │ ├── aiohttp.svg │ │ ├── asc.gif │ │ ├── back.png │ │ ├── back_hover.png │ │ ├── bg.gif │ │ ├── close.png │ │ ├── close_hover.png │ │ ├── console.png │ │ ├── desc.gif │ │ ├── headerbg.png │ │ ├── indicator.png │ │ ├── less.png │ │ ├── more.png │ │ ├── panel_bg.png │ │ ├── source.png │ │ ├── tick-red.png │ │ └── tick.png │ └── js │ │ ├── README.rst │ │ ├── bootstrap.min.js │ │ ├── debugger.js │ │ ├── highlight.min.js │ │ ├── jquery-1.10.2.min.js │ │ ├── jquery-1.10.2.min.map │ │ ├── jquery.cookie.js │ │ ├── jquery.tablesorter.min.js │ │ ├── prism.js │ │ ├── r.js │ │ ├── require.js │ │ ├── tests.html │ │ └── toolbar.js ├── tbtools │ ├── __init__.py │ ├── console.py │ ├── repr.py │ └── tbtools.py ├── templates │ ├── console.jinja2 │ ├── exception.jinja2 │ ├── exception_summary.jinja2 │ ├── global_tab.jinja2 │ ├── history_tab.jinja2 │ ├── redirect.jinja2 │ ├── settings_tab.jinja2 │ └── toolbar.jinja2 ├── toolbar.py ├── utils.py └── views.py ├── demo ├── README.rst ├── aiohttp_debugtoolba_sceenshot.png ├── demo.py ├── static │ ├── jquery-1.7.2.min.js │ ├── main.js │ └── require-1.0.6.js └── templates │ ├── ajax.jinja2 │ ├── error.jinja2 │ └── index.jinja2 ├── examples ├── extra_panels │ ├── extra_pgsql.py │ ├── extra_redis.py │ ├── extra_tpl │ │ ├── request_pgsql.jinja2 │ │ └── request_redis.jinja2 │ └── server.py └── simple.py ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── conftest.py ├── pep492 └── test_await.py ├── test_debug.py ├── test_exception_views.py ├── test_imports.py ├── test_middleware.py ├── test_panel.py ├── test_panels_versions.py ├── test_server_push.py ├── test_utils.py └── tpl └── test.jinja2 /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | notify: 4 | after_n_builds: 10 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = aiohttp_debugtoolbar, tests 4 | omit = site-packages, .tox 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | enable-extensions = G 3 | max-doc-length = 90 4 | max-line-length = 90 5 | select = A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,B901,B902,B903,B950 6 | # E226: Missing whitespace around arithmetic operators can help group things together. 7 | # E501: Superseeded by B950 (from Bugbear) 8 | # E722: Superseeded by B001 (from Bugbear) 9 | # W503: Mutually exclusive with W504. 10 | ignore = E226,E501,E722,W503 11 | per-file-ignores = 12 | # I900: Requirements not needed for examples 13 | examples/*:I900 14 | # S101: Pytest uses assert 15 | tests/*:S101 16 | 17 | # flake8-import-order 18 | application-import-names = aiohttp_debugtoolbar 19 | import-order-style = pycharm 20 | 21 | # flake8-requirements 22 | known-modules = :[aiohttp_debugtoolbar] 23 | requirements-file = requirements-dev.txt 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --squash "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6 8 | tags: [ 'v*' ] 9 | pull_request: 10 | branches: 11 | - master 12 | - '[0-9].[0-9]+' 13 | 14 | 15 | jobs: 16 | lint: 17 | name: Linter 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 5 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Setup Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: 3.9 27 | cache: 'pip' 28 | cache-dependency-path: '**/requirements*.txt' 29 | - name: Pre-Commit hooks 30 | uses: pre-commit/action@v3.0.1 31 | - name: Install dependencies 32 | uses: py-actions/py-dependency-install@v4 33 | with: 34 | path: requirements-dev.txt 35 | - name: Install itself 36 | run: | 37 | pip install . 38 | - name: Mypy 39 | run: mypy 40 | - name: Prepare twine checker 41 | run: | 42 | pip install -U build twine wheel 43 | python -m build 44 | - name: Run twine checker 45 | run: | 46 | twine check dist/* 47 | 48 | test: 49 | name: Test 50 | strategy: 51 | matrix: 52 | pyver: ['3.9', '3.10', '3.11', '3.12', '3.13'] 53 | os: [ubuntu, macos, windows] 54 | include: 55 | - pyver: pypy-3.9 56 | os: ubuntu 57 | runs-on: ${{ matrix.os }}-latest 58 | timeout-minutes: 15 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v4 62 | - name: Setup Python ${{ matrix.pyver }} 63 | uses: actions/setup-python@v5 64 | with: 65 | python-version: ${{ matrix.pyver }} 66 | cache: 'pip' 67 | cache-dependency-path: '**/requirements*.txt' 68 | - name: Install dependencies 69 | uses: py-actions/py-dependency-install@v4 70 | with: 71 | path: requirements.txt 72 | - name: Run unittests 73 | run: pytest --cov-report=xml tests 74 | env: 75 | COLOR: 'yes' 76 | - run: python -m coverage xml 77 | - name: Upload coverage 78 | uses: codecov/codecov-action@v5 79 | with: 80 | file: ./coverage.xml 81 | flags: unit 82 | 83 | check: # This job does nothing and is only used for the branch protection 84 | if: always() 85 | 86 | needs: [lint, test] 87 | 88 | runs-on: ubuntu-latest 89 | 90 | steps: 91 | - name: Decide whether the needed jobs succeeded or failed 92 | uses: re-actors/alls-green@release/v1 93 | with: 94 | jobs: ${{ toJSON(needs) }} 95 | 96 | deploy: 97 | name: Deploy 98 | environment: release 99 | runs-on: ubuntu-latest 100 | needs: check 101 | # Run only on pushing a tag 102 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 103 | steps: 104 | - name: Checkout 105 | uses: actions/checkout@v4 106 | - name: Setup Python 107 | uses: actions/setup-python@v5 108 | with: 109 | python-version: 3.13 110 | - name: Install dependencies 111 | run: 112 | python -m pip install -U build setuptools wheel twine 113 | - name: Make dists 114 | run: 115 | python -m build 116 | - name: Make Release 117 | uses: aio-libs/create-release@v1.6.6 118 | with: 119 | changes_file: CHANGES.rst 120 | name: aiohttp-debugtoolbar 121 | version_file: aiohttp_debugtoolbar/__init__.py 122 | github_token: ${{ secrets.GITHUB_TOKEN }} 123 | pypi_token: ${{ secrets.PYPI_API_TOKEN }} 124 | dist_dir: dist 125 | fix_issue_regex: "`#(\\d+) `" 126 | fix_issue_repl: "(#\\1)" 127 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "33 2 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ># Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | venv/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | cover 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | # PyCharm 59 | .idea 60 | 61 | # vim 62 | *.swp 63 | 64 | .pytest_cache 65 | <<<<<<< HEAD 66 | .python-version 67 | ======= 68 | >>>>>>> b32a82ee01dcc2d96a18cf00e5a6a614d880a724 69 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = aiohttp_debugtoolbar, demo, examples, tests 3 | #check_untyped_defs = True 4 | follow_imports_for_stubs = True 5 | #disallow_any_decorated = True 6 | #disallow_any_generics = True 7 | #disallow_any_unimported = True 8 | disallow_incomplete_defs = True 9 | disallow_subclassing_any = True 10 | #disallow_untyped_calls = True 11 | #disallow_untyped_decorators = True 12 | #disallow_untyped_defs = True 13 | implicit_reexport = False 14 | no_implicit_optional = True 15 | show_error_codes = True 16 | strict_equality = True 17 | warn_incomplete_stub = True 18 | warn_redundant_casts = True 19 | warn_unreachable = True 20 | warn_unused_ignores = True 21 | #warn_return_any = True 22 | 23 | 24 | [mypy-aiopg.*] 25 | ignore_missing_imports = True 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: 'v4.4.0' 4 | hooks: 5 | - id: check-merge-conflict 6 | - repo: https://github.com/asottile/yesqa 7 | rev: v1.5.0 8 | hooks: 9 | - id: yesqa 10 | additional_dependencies: 11 | - flake8-bandit==4.1.1 12 | - flake8-bugbear==23.7.10 13 | - flake8-import-order==0.18.2 14 | - flake8-requirements==1.7.8 15 | - repo: https://github.com/psf/black 16 | rev: '23.7.0' 17 | hooks: 18 | - id: black 19 | language_version: python3 # Should be a command that runs python 20 | - repo: https://github.com/pre-commit/pre-commit-hooks 21 | rev: 'v4.4.0' 22 | hooks: 23 | - id: end-of-file-fixer 24 | exclude: >- 25 | ^docs/[^/]*\.svg$ 26 | - id: requirements-txt-fixer 27 | - id: trailing-whitespace 28 | - id: file-contents-sorter 29 | files: | 30 | CONTRIBUTORS.txt| 31 | docs/spelling_wordlist.txt| 32 | .gitignore| 33 | .gitattributes 34 | - id: check-case-conflict 35 | - id: check-json 36 | - id: check-xml 37 | - id: check-executables-have-shebangs 38 | - id: check-toml 39 | - id: check-xml 40 | - id: check-yaml 41 | - id: debug-statements 42 | - id: check-added-large-files 43 | - id: check-symlinks 44 | - id: debug-statements 45 | - id: detect-aws-credentials 46 | args: ['--allow-missing-credentials'] 47 | - id: detect-private-key 48 | exclude: ^examples/ 49 | - repo: https://github.com/PyCQA/flake8 50 | rev: '6.1.0' 51 | hooks: 52 | - id: flake8 53 | exclude: "^docs/" 54 | additional_dependencies: 55 | - flake8-bandit==4.1.1 56 | - flake8-bugbear==23.7.10 57 | - flake8-import-order==0.18.2 58 | - flake8-requirements==1.7.8 59 | - repo: https://github.com/asottile/pyupgrade 60 | rev: 'v3.10.1' 61 | hooks: 62 | - id: pyupgrade 63 | args: ['--py36-plus'] 64 | - repo: https://github.com/Lucas-C/pre-commit-hooks-markup 65 | rev: v1.0.1 66 | hooks: 67 | - id: rst-linter 68 | files: >- 69 | ^[^/]+[.]rst$ 70 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | CHANGES 3 | ======= 4 | 5 | .. towncrier release notes start 6 | 7 | 0.6.1 (2023-11-19) 8 | ================== 9 | 10 | - Filtered out requests to debugtoolbar itself from the requests history. 11 | - Improved import time by delaying loading of package data. 12 | - Fixed static URLs when using yarl 1.9+. 13 | - Fixed a warning in the ``re`` module. 14 | - Switched to ``aiohttp.web.AppKey`` for aiohttp 3.9. 15 | - Dropped Python 3.7 and added Python 3.11. 16 | 17 | 0.6.0 (2020-01-25) 18 | ================== 19 | 20 | - Fixed ClassBasedView support. #207 21 | - Dropped aiohttp<3.3 support. 22 | - Dropped Python 3.4 support. 23 | - Dropped ``yield from`` and ``@asyncio.coroutine`` support. 24 | 25 | 0.5.0 (2018-02-14) 26 | ================== 27 | 28 | - Added safe filter to render_content. #195 29 | - Added support for aiohtp 3. 30 | 31 | 0.4.1 (2017-08-30) 32 | ================== 33 | 34 | - Fixed issue with redirects without location header. #174 35 | 36 | 0.4.0 (2017-05-04) 37 | ================== 38 | 39 | - Added asyncio trove classifier. 40 | - Addes support for aiohttp 2.0.7+. 41 | 42 | 0.3.0 (2016-11-18) 43 | ================== 44 | 45 | - Fixed middleware route finding when using sub-apps. #65 46 | - Added examples for extra panels: pgsql & redis monitor. #59 47 | 48 | 0.2.0 (2016-11-08) 49 | ================== 50 | 51 | - Refactored test suite. 52 | 53 | 0.1.4 (2016-11-07) 54 | ================== 55 | 56 | - Renamed to aiohttp-debugtoolbar. 57 | - Fixed imcompatibility with aiohttp 1.1. 58 | 59 | 0.1.3 (2016-10-27) 60 | ================== 61 | 62 | - Fixed a link to request info page, sort request information alphabetically. #52 63 | 64 | 0.1.2 (2016-09-27) 65 | ================== 66 | 67 | - Fixed empty functions names in performance panel. #43 (Thanks @kammala!) 68 | - Fixed flashing message during page rendering issue. #46 69 | 70 | 0.1.1 (2016-02-21) 71 | ================== 72 | 73 | - Fixed a demo. 74 | - Added syntax highlight in traceback view, switched highlighter from 75 | highlight.js to prism.js. #31 76 | 77 | 0.1.0 (2016-02-13) 78 | ================== 79 | 80 | - Added Python 3.5 support. (Thanks @stormandco!) 81 | - Added view source button in RoutesDebugPanel. (Thanks @stormandco!) 82 | - Dropped support for Python 3.3. (Thanks @sloria!) 83 | - Added middleware in setup method. (Thanks @sloria!) 84 | - Fixed bug with interactive console. 85 | - Fixed support for aiohttp>=0.21.1. 86 | 87 | 0.0.5 (2015-09-13) 88 | ================== 89 | 90 | - Fixed IPv6 socket family error. (Thanks @stormandco!) 91 | 92 | 0.0.4 (2015-09-05) 93 | ================== 94 | 95 | - Fixed support for aiohttp>=0.17. (Thanks @himikof!) 96 | 97 | 0.0.3 (2015-07-03) 98 | ================== 99 | 100 | - Switched template engine from mako to jinja2. (Thanks @iho!) 101 | - Added custom *yield from* to track context switches inside coroutine. 102 | - Implemented panel for collecting request log messages. 103 | - Disable toolbar code injecting for non web.Response answers 104 | (StreamResponse or WebSocketResponse for example). #12 105 | 106 | 0.0.2 (2015-05-26) 107 | ================== 108 | 109 | - Redesigned UI look-and-feel. 110 | - Renamed `toolbar_middleware_factory` to just `middleware`. 111 | 112 | 0.0.1 (2015-05-18) 113 | ================== 114 | 115 | - Initial release. 116 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGES.rst 3 | include README.rst 4 | graft aiohttp_debugtoolbar 5 | global-exclude *.pyc *.swp 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Some simple testing tasks (sorry, UNIX only). 2 | 3 | flake: 4 | flake8 --exclude=pep492 aiohttp_debugtoolbar tests 5 | 6 | test: flake 7 | pytest -s ./tests/ 8 | 9 | vtest: 10 | pytest -s -v ./tests/ 11 | 12 | cov cover coverage: flake 13 | pytest -s --cov-report term --cov-report html --cov aiohttp_debugtoolbar 14 | @echo "open file://`pwd`/htmlcov/index.html" 15 | 16 | clean: 17 | rm -rf `find . -name __pycache__` 18 | rm -f `find . -type f -name '*.py[co]' ` 19 | rm -f `find . -type f -name '*~' ` 20 | rm -f `find . -type f -name '.*~' ` 21 | rm -f `find . -type f -name '@*' ` 22 | rm -f `find . -type f -name '#*#' ` 23 | rm -f `find . -type f -name '*.orig' ` 24 | rm -f `find . -type f -name '*.rej' ` 25 | rm -f .coverage 26 | rm -rf coverage 27 | rm -rf build 28 | rm -rf cover 29 | rm -rf htmlcov 30 | 31 | doc: 32 | make -C docs html 33 | @echo "open file://`pwd`/docs/_build/html/index.html" 34 | 35 | .PHONY: all build venv flake test vtest testloop cov clean doc 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | aiohttp-debugtoolbar 2 | ==================== 3 | .. image:: https://travis-ci.org/aio-libs/aiohttp-debugtoolbar.svg?branch=master 4 | :target: https://travis-ci.org/aio-libs/aiohttp-debugtoolbar 5 | :alt: |Build status| 6 | .. image:: https://codecov.io/gh/aio-libs/aiohttp-debugtoolbar/branch/master/graph/badge.svg 7 | :target: https://codecov.io/gh/aio-libs/aiohttp-debugtoolbar 8 | :alt: |Coverage status| 9 | .. image:: https://img.shields.io/pypi/v/aiohttp-debugtoolbar.svg 10 | :target: https://pypi.python.org/pypi/aiohttp-debugtoolbar 11 | :alt: PyPI 12 | .. image:: https://badges.gitter.im/Join%20Chat.svg 13 | :target: https://gitter.im/aio-libs/Lobby 14 | :alt: Chat on Gitter 15 | 16 | **aiohttp_debugtoolbar** provides a debug toolbar for your aiohttp_ 17 | web application. Library is port of pyramid_debugtoolbar_ and 18 | still in early development stages. Basic functionality has been 19 | ported: 20 | 21 | * basic panels 22 | * intercept redirects 23 | * intercept and pretty print exception 24 | * interactive python console 25 | * show source code 26 | 27 | .. image:: https://raw.githubusercontent.com/aio-libs/aiohttp_debugtoolbar/master/demo/aiohttp_debugtoolba_sceenshot.png 28 | 29 | 30 | Ported Panels 31 | ------------- 32 | ``HeaderDebugPanel``, ``PerformanceDebugPanel``, ``TracebackPanel``, 33 | ``SettingsDebugPanel``, ``MiddlewaresDebugPanel``, ``VersionDebugPanel``, 34 | ``RoutesDebugPanel``, ``RequestVarsDebugPanel``, ``LoggingPanel`` 35 | 36 | 37 | Help Needed 38 | ----------- 39 | Are you coder looking for a project to contribute to 40 | python/asyncio libraries? This is the project for you! 41 | 42 | 43 | Install and Configuration 44 | ------------------------- 45 | :: 46 | 47 | $ pip install aiohttp_debugtoolbar 48 | 49 | 50 | In order to plug in ``aiohttp_debugtoolbar``, call 51 | ``aiohttp_debugtoolbar.setup`` on your app. 52 | 53 | .. code:: python 54 | 55 | import aiohttp_debugtoolbar 56 | app = web.Application(loop=loop) 57 | aiohttp_debugtoolbar.setup(app) 58 | 59 | 60 | Full Example 61 | ------------ 62 | 63 | .. code:: python 64 | 65 | import asyncio 66 | import jinja2 67 | import aiohttp_debugtoolbar 68 | import aiohttp_jinja2 69 | 70 | from aiohttp import web 71 | 72 | 73 | @aiohttp_jinja2.template('index.html') 74 | async def basic_handler(request): 75 | return {'title': 'example aiohttp_debugtoolbar!', 76 | 'text': 'Hello aiohttp_debugtoolbar!', 77 | 'app': request.app} 78 | 79 | 80 | async def exception_handler(request): 81 | raise NotImplementedError 82 | 83 | 84 | async def init(loop): 85 | # add aiohttp_debugtoolbar middleware to you application 86 | app = web.Application(loop=loop) 87 | # install aiohttp_debugtoolbar 88 | aiohttp_debugtoolbar.setup(app) 89 | 90 | template = """ 91 | 92 | 93 | {{ title }} 94 | 95 | 96 |

{{ text }}

97 |

98 | 99 | Exception example 100 |

101 | 102 | 103 | """ 104 | # install jinja2 templates 105 | loader = jinja2.DictLoader({'index.html': template}) 106 | aiohttp_jinja2.setup(app, loader=loader) 107 | 108 | # init routes for index page, and page with error 109 | app.router.add_route('GET', '/', basic_handler, name='index') 110 | app.router.add_route('GET', '/exc', exception_handler, 111 | name='exc_example') 112 | return app 113 | 114 | 115 | loop = asyncio.get_event_loop() 116 | app = loop.run_until_complete(init(loop)) 117 | web.run_app(app, host='127.0.0.1', port=9000) 118 | 119 | Settings 120 | -------- 121 | .. code:: python 122 | 123 | aiohttp_debugtoolbar.setup(app, hosts=['172.19.0.1', ]) 124 | 125 | Supported options 126 | 127 | 128 | - enabled: The debugtoolbar is disabled if False. By default is set to True. 129 | - intercept_redirects: If True, intercept redirect and display an intermediate page with a link to the redirect page. By default is set to True. 130 | - hosts: The list of allow hosts. By default is set to ['127.0.0.1', '::1']. 131 | - exclude_prefixes: The list of forbidden hosts. By default is set to []. 132 | - check_host: If False, disable the host check and display debugtoolbar for any host. By default is set to True. 133 | - max_request_history: The max value for storing requests. By default is set to 100. 134 | - max_visible_requests: The max value of display requests. By default is set to 10. 135 | - path_prefix: The prefix of path to debugtoolbar. By default is set to '/_debugtoolbar'. 136 | 137 | 138 | Thanks! 139 | ------- 140 | 141 | I've borrowed a lot of code from following projects. I highly 142 | recommend to check them out: 143 | 144 | * pyramid_debugtoolbar_ 145 | * django-debug-toolbar_ 146 | * flask-debugtoolbar_ 147 | 148 | Play With Demo 149 | -------------- 150 | 151 | https://github.com/aio-libs/aiohttp_debugtoolbar/tree/master/demo 152 | 153 | Requirements 154 | ------------ 155 | 156 | * aiohttp_ 157 | * aiohttp_jinja2_ 158 | 159 | 160 | .. _Python: https://www.python.org 161 | .. _asyncio: http://docs.python.org/3/library/asyncio.html 162 | .. _aiohttp: https://github.com/KeepSafe/aiohttp 163 | .. _aiopg: https://github.com/aio-libs/aiopg 164 | .. _aiomysql: https://github.com/aio-libs/aiomysql 165 | .. _aiohttp_jinja2: https://github.com/aio-libs/aiohttp_jinja2 166 | .. _pyramid_debugtoolbar: https://github.com/Pylons/pyramid_debugtoolbar 167 | .. _django-debug-toolbar: https://github.com/django-debug-toolbar/django-debug-toolbar 168 | .. _flask-debugtoolbar: https://github.com/mgood/flask-debugtoolbar 169 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import setup 2 | from .middlewares import middleware 3 | from .utils import APP_KEY 4 | 5 | __version__ = "0.6.1" 6 | 7 | __all__ = ("setup", "middleware", "APP_KEY") 8 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/main.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from pathlib import Path 3 | from typing import Iterable, Literal, Sequence, Type, TypedDict, Union 4 | 5 | import aiohttp_jinja2 6 | import jinja2 7 | from aiohttp import web 8 | 9 | from . import panels, views 10 | from .middlewares import middleware 11 | from .panels.base import DebugPanel 12 | from .utils import ( 13 | APP_KEY, 14 | AppState, 15 | ExceptionHistory, 16 | STATIC_ROUTE_NAME, 17 | TEMPLATE_KEY, 18 | ToolbarStorage, 19 | _Config, 20 | ) 21 | from .views import ExceptionDebugView 22 | 23 | default_panel_names = ( 24 | panels.HeaderDebugPanel, 25 | panels.PerformanceDebugPanel, 26 | panels.RequestVarsDebugPanel, 27 | panels.TracebackPanel, 28 | panels.LoggingPanel, 29 | ) 30 | 31 | 32 | default_global_panel_names = ( 33 | panels.RoutesDebugPanel, 34 | panels.SettingsDebugPanel, 35 | panels.MiddlewaresDebugPanel, 36 | panels.VersionDebugPanel, 37 | ) 38 | 39 | 40 | class _AppDetails(TypedDict): 41 | exc_history: ExceptionHistory 42 | pdtb_token: str 43 | request_history: ToolbarStorage 44 | settings: _Config 45 | 46 | 47 | def setup( 48 | app: web.Application, 49 | *, 50 | enabled: bool = True, 51 | intercept_exc: Literal["debug", "display", False] = "debug", 52 | intercept_redirects: bool = True, 53 | panels: Iterable[Type[DebugPanel]] = default_panel_names, 54 | extra_panels: Iterable[Type[DebugPanel]] = (), 55 | extra_templates: Union[str, Path, Iterable[Union[str, Path]]] = (), 56 | global_panels: Iterable[Type[DebugPanel]] = default_global_panel_names, 57 | hosts: Sequence[str] = ("127.0.0.1", "::1"), 58 | exclude_prefixes: Iterable[str] = (), 59 | check_host: bool = True, # disable host check 60 | button_style: str = "", 61 | max_request_history: int = 100, 62 | max_visible_requests: int = 10, 63 | path_prefix: str = "/_debugtoolbar", 64 | ) -> None: 65 | config = _Config( 66 | enabled=enabled, 67 | intercept_exc=intercept_exc, 68 | intercept_redirects=intercept_redirects, 69 | panels=tuple(panels), 70 | extra_panels=tuple(extra_panels), 71 | global_panels=tuple(global_panels), 72 | hosts=hosts, 73 | exclude_prefixes=tuple(exclude_prefixes), 74 | check_host=check_host, 75 | button_style=button_style, 76 | max_visible_requests=max_visible_requests, 77 | path_prefix=path_prefix, 78 | ) 79 | 80 | if middleware not in app.middlewares: 81 | app.middlewares.append(middleware) 82 | 83 | APP_ROOT = Path(__file__).parent 84 | templates_app = APP_ROOT / "templates" 85 | templates_panels = APP_ROOT / "panels/templates" 86 | 87 | if isinstance(extra_templates, (str, Path)): 88 | templ: Iterable[Union[str, Path]] = (extra_templates,) 89 | else: 90 | templ = extra_templates 91 | loader = jinja2.FileSystemLoader((templates_app, templates_panels, *templ)) 92 | aiohttp_jinja2.setup(app, loader=loader, app_key=TEMPLATE_KEY) 93 | 94 | static_location = APP_ROOT / "static" 95 | 96 | exc_handlers = ExceptionDebugView() 97 | 98 | app.router.add_static( 99 | path_prefix + "/static", static_location, name=STATIC_ROUTE_NAME 100 | ) 101 | 102 | app.router.add_route( 103 | "GET", path_prefix + "/source", exc_handlers.source, name="debugtoolbar.source" 104 | ) 105 | app.router.add_route( 106 | "GET", 107 | path_prefix + "/execute", 108 | exc_handlers.execute, 109 | name="debugtoolbar.execute", 110 | ) 111 | # app.router.add_route('GET', path_prefix + '/console', 112 | # exc_handlers.console, 113 | # name='debugtoolbar.console') 114 | app.router.add_route( 115 | "GET", 116 | path_prefix + "/exception", 117 | exc_handlers.exception, 118 | name="debugtoolbar.exception", 119 | ) 120 | # TODO: fix when sql will be ported 121 | # app.router.add_route('GET', path_prefix + '/sqlalchemy/sql_select', 122 | # name='debugtoolbar.sql_select') 123 | # app.router.add_route('GET', path_prefix + '/sqlalchemy/sql_explain', 124 | # name='debugtoolbar.sql_explain') 125 | 126 | app.router.add_route( 127 | "GET", path_prefix + "/sse", views.sse, name="debugtoolbar.sse" 128 | ) 129 | 130 | app.router.add_route( 131 | "GET", 132 | path_prefix + "/{request_id}", 133 | views.request_view, 134 | name="debugtoolbar.request", 135 | ) 136 | app.router.add_route( 137 | "GET", path_prefix, views.request_view, name="debugtoolbar.main" 138 | ) 139 | 140 | app[APP_KEY] = AppState( 141 | { 142 | "exc_history": ExceptionHistory(), 143 | "pdtb_token": secrets.token_hex(10), 144 | "request_history": ToolbarStorage(max_request_history), 145 | "settings": config, 146 | } 147 | ) 148 | if intercept_exc: 149 | app[APP_KEY]["exc_history"].eval_exc = intercept_exc == "debug" 150 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/middlewares.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import aiohttp_jinja2 4 | from aiohttp import web 5 | from aiohttp.typedefs import Handler 6 | from aiohttp.web_exceptions import HTTPMove 7 | 8 | from .tbtools.tbtools import get_traceback 9 | from .toolbar import DebugToolbar 10 | from .utils import ( 11 | APP_KEY, 12 | ContextSwitcher, 13 | REDIRECT_CODES, 14 | TEMPLATE_KEY, 15 | addr_in, 16 | hexlify, 17 | ) 18 | 19 | __all__ = ("middleware",) 20 | HTML_TYPES = ("text/html", "application/xhtml+xml") 21 | 22 | 23 | @web.middleware 24 | async def middleware(request: web.Request, handler: Handler) -> web.StreamResponse: 25 | app = request.app 26 | 27 | if APP_KEY not in app: 28 | raise RuntimeError( 29 | "Please setup debug toolbar with " "aiohttp_debugtoolbar.setup method" 30 | ) 31 | 32 | # just create namespace for handler 33 | settings = app[APP_KEY]["settings"] 34 | request_history = app[APP_KEY]["request_history"] 35 | exc_history = app[APP_KEY]["exc_history"] 36 | intercept_exc = app[APP_KEY]["settings"]["intercept_exc"] 37 | 38 | if not app[APP_KEY]["settings"]["enabled"]: 39 | return await handler(request) 40 | 41 | # request['exc_history'] = exc_history 42 | panel_classes = settings.get("panels", ()) + settings.get("extra_panels", ()) 43 | global_panel_classes = settings.get("global_panels", ()) 44 | hosts = settings.get("hosts", []) 45 | 46 | show_on_exc_only = settings.get("show_on_exc_only") 47 | intercept_redirects = settings["intercept_redirects"] 48 | 49 | root_url = app.router["debugtoolbar.main"].url_for().raw_path 50 | exclude_prefixes = settings.get("exclude_prefixes", ()) 51 | exclude = (root_url,) + exclude_prefixes 52 | 53 | p = request.raw_path 54 | starts_with_excluded = list(filter(None, map(p.startswith, exclude))) 55 | 56 | # FIXME: error when run trough unixsocket 57 | if request.transport: 58 | peername = request.transport.get_extra_info("peername") 59 | last_proxy_addr = peername[0] 60 | else: 61 | last_proxy_addr = "" 62 | 63 | # TODO: rethink access policy by host 64 | if settings.get("check_host"): 65 | if starts_with_excluded or not addr_in(last_proxy_addr, hosts): 66 | return await handler(request) 67 | 68 | toolbar = DebugToolbar(request, panel_classes, global_panel_classes) 69 | _handler = handler 70 | 71 | context_switcher = ContextSwitcher() 72 | for panel in toolbar.panels: 73 | _handler = panel.wrap_handler(_handler, context_switcher) 74 | 75 | try: 76 | response = await context_switcher(_handler(request)) 77 | except HTTPMove as exc: 78 | if not intercept_redirects: 79 | raise 80 | # Intercept http redirect codes and display an html page with a 81 | # link to the target. 82 | if not getattr(exc, "location", None): 83 | raise 84 | response = web.Response( 85 | status=exc.status, reason=exc.reason, text=exc.text, headers=exc.headers 86 | ) 87 | 88 | context = {"redirect_to": exc.location, "redirect_code": exc.status} 89 | 90 | _response = aiohttp_jinja2.render_template( 91 | "redirect.jinja2", request, context, app_key=TEMPLATE_KEY 92 | ) 93 | response = _response 94 | except web.HTTPException: 95 | raise 96 | except Exception as e: 97 | if intercept_exc: 98 | tb = get_traceback( 99 | info=sys.exc_info(), 100 | skip=1, 101 | show_hidden_frames=False, 102 | ignore_system_exceptions=True, 103 | exc=e, 104 | app=request.app, 105 | ) 106 | for frame in tb.frames: 107 | exc_history.frames[frame.id] = frame 108 | exc_history.tracebacks[tb.id] = tb 109 | request["pdbt_tb"] = tb 110 | 111 | # TODO: find out how to port following to aiohttp 112 | # or just remove it 113 | # token = request.app[APP_KEY]['pdtb_token'] 114 | # qs = {'token': token, 'tb': str(tb.id)} 115 | # msg = 'Exception at %s\ntraceback url: %s' 116 | # 117 | # exc_url = request.app.router['debugtoolbar.exception']\ 118 | # .url(query=qs) 119 | # assert exc_url, msg 120 | # exc_msg = msg % (request.path, exc_url) 121 | # logger.exception(exc_msg) 122 | 123 | # subenviron = request.environ.copy() 124 | # del subenviron['PATH_INFO'] 125 | # del subenviron['QUERY_STRING'] 126 | # subrequest = type(request).blank(exc_url, subenviron) 127 | # subrequest.script_name = request.script_name 128 | # subrequest.path_info = \ 129 | # subrequest.path_info[len(request.script_name):] 130 | # 131 | # response = request.invoke_subrequest(subrequest) 132 | body = tb.render_full(request).encode("utf-8", "replace") 133 | response = web.Response(body=body, status=500, content_type="text/html") 134 | 135 | await toolbar.process_response(request, response) 136 | 137 | request["id"] = str(id(request)) 138 | toolbar.status = response.status 139 | 140 | request_history.put(request["id"], toolbar) 141 | toolbar.inject(request, response) 142 | return response 143 | else: 144 | # logger.exception('Exception at %s' % request.path) 145 | raise e 146 | toolbar.status = response.status 147 | if intercept_redirects: 148 | # Intercept http redirect codes and display an html page with a 149 | # link to the target. 150 | if response.status in REDIRECT_CODES and getattr(response, "location", None): 151 | context = { 152 | "redirect_to": response.location, 153 | "redirect_code": response.status, 154 | } 155 | 156 | _response = aiohttp_jinja2.render_template( 157 | "redirect.jinja2", request, context, app_key=TEMPLATE_KEY 158 | ) 159 | response = _response 160 | 161 | await toolbar.process_response(request, response) 162 | request["id"] = hexlify(id(request)) 163 | 164 | # Don't store the favicon.ico request 165 | # it's requested by the browser automatically 166 | # Also ignore requests for debugtoolbar itself. 167 | tb_request = request.path.startswith(settings["path_prefix"]) 168 | if not tb_request and request.path != "/favicon.ico": 169 | request_history.put(request["id"], toolbar) 170 | 171 | if not show_on_exc_only and response.content_type in HTML_TYPES: 172 | toolbar.inject(request, response) 173 | 174 | return response 175 | 176 | 177 | toolbar_html_template = """\ 178 | 185 | 186 |
187 | 192 |
193 | """ 194 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/__init__.py: -------------------------------------------------------------------------------- 1 | from .headers import HeaderDebugPanel as HeaderDebugPanel 2 | from .logger import LoggingPanel as LoggingPanel 3 | from .middlewares import MiddlewaresDebugPanel as MiddlewaresDebugPanel 4 | from .performance import PerformanceDebugPanel as PerformanceDebugPanel 5 | from .request_vars import RequestVarsDebugPanel as RequestVarsDebugPanel 6 | from .routes import RoutesDebugPanel as RoutesDebugPanel 7 | from .settings import SettingsDebugPanel as SettingsDebugPanel 8 | from .traceback import TracebackPanel as TracebackPanel 9 | from .versions import VersionDebugPanel as VersionDebugPanel 10 | 11 | __all__ = ( 12 | "HeaderDebugPanel", 13 | "LoggingPanel", 14 | "MiddlewaresDebugPanel", 15 | "PerformanceDebugPanel", 16 | "RequestVarsDebugPanel", 17 | "RoutesDebugPanel", 18 | "SettingsDebugPanel", 19 | "TracebackPanel", 20 | "VersionDebugPanel", 21 | ) 22 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ..utils import render 4 | 5 | 6 | class DebugPanel(ABC): 7 | """Base class for debug panels. 8 | 9 | A new instance of this class is created for every request. 10 | 11 | A panel is notified of events throughout the request lifecycle. It 12 | is then persisted and used later by the toolbar to render its results 13 | as a tab on the interface. The lifecycle hooks are available in the 14 | following order: 15 | 16 | - :meth:`.__init__` 17 | - :meth:`.wrap_handler` 18 | - :meth:`.process_beforerender` 19 | - :meth:`.process_response` 20 | 21 | Each of these hooks is overridable by a subclass to gleen information 22 | from the request and other events for later display. 23 | 24 | The panel is later used to render its results. This is done on-demand 25 | and in the lifecycle of a request to the debug toolbar by invoking 26 | :meth:`.render_content`. Any data stored within :attr:`.data` is 27 | injected into the template prior to rendering and is thus a common 28 | location to store the contents of previous events. 29 | """ 30 | 31 | #: A unique identifier for the name of the panel. This **must** be 32 | #: defined by a subclass. 33 | name: str 34 | 35 | #: If ``False`` then the panel's tab will be disabled and 36 | #: :meth:`.render_content` will not be invoked. Most subclasses will 37 | #: want to set this to ``True``. 38 | has_content = False 39 | 40 | #: If the client is able to activate/de-activate the panel then this 41 | #: should be ``True``. 42 | user_activate = False 43 | 44 | #: This property will be set by the toolbar, indicating the user's 45 | #: decision to activate or deactivate the panel. If ``user_activate`` 46 | #: is ``False`` then ``is_active`` will always be set to ``True``. 47 | is_active = False 48 | 49 | #: Must be overridden by subclasses that are using the default 50 | #: implementation of ``render_content``. This is an 51 | #: :term:`asset specification` pointing at the template to be rendered 52 | #: for the panel. Usually of the format 53 | #: ``'mylib:templates/panel.jinja2'``. 54 | template: str 55 | 56 | #: Title showing in toolbar. Must be overridden. 57 | nav_title: str 58 | 59 | #: Subtitle showing until title in toolbar. 60 | nav_subtitle = "" 61 | 62 | #: Title showing in panel. Must be overridden. 63 | title: str 64 | 65 | #: The URL invoked when the panel's tab is cliked. May be overridden to 66 | #: define an arbitrary URL for the panel or do some other custom action 67 | #: when the user clicks on the panel's tab in the toolbar. 68 | url = "" 69 | 70 | @property 71 | def request(self): 72 | return self._request 73 | 74 | # Panel methods 75 | def __init__(self, request): 76 | """Configure the panel for a request. 77 | 78 | :param request: The instance of :class:`aiohttp.web.Request` that 79 | this object is wrapping. 80 | """ 81 | self._request = request 82 | self.data = {} 83 | 84 | def render_content(self, request): 85 | """Return a string containing the HTML to be rendered for the panel. 86 | 87 | By default this will render the template defined by the 88 | :attr:`.template` attribute with a rendering context defined by 89 | :attr:`.data` combined with the ``dict`` returned from 90 | :meth:`.render_vars`. 91 | 92 | The ``request`` here is the active request in the toolbar. Not the 93 | original request that this panel represents. 94 | """ 95 | context = self.data.copy() 96 | context.update(self.render_vars(request)) 97 | return render(self.template, request.app, context, request=request) 98 | 99 | @property 100 | def dom_id(self): 101 | """The ``id`` tag of the panel's tab. May be used by CSS and 102 | Javascript to implement custom styles and actions.""" 103 | return "pDebug%sPanel" % (self.name.replace(" ", "")) 104 | 105 | async def process_response(self, response): # noqa: B027 106 | """Receives the response generated by the request. 107 | 108 | Override this method to track properties of the response. 109 | """ 110 | 111 | def wrap_handler(self, handler, context_switcher): 112 | """May be overridden to monitor the entire lifecycle of the request. 113 | 114 | A handler receives a request and returns a response. An example 115 | implementation may be: 116 | 117 | .. code-block:: python 118 | 119 | def wrap_handler(self, handler, context_switcher): 120 | async def wrapper(request): 121 | start_time = time.time() 122 | response = await handler(request) 123 | end_time = time.time() 124 | self.data['duration'] = end_time - start_time 125 | return response 126 | return wrapper 127 | 128 | context_switcher can be used for context switch tracking, you can 129 | add your callback right before context switch in or out. 130 | """ 131 | return handler 132 | 133 | def render_vars(self, request): 134 | """Invoked by the default implementation of :meth:`.render_content` 135 | and should return a ``dict`` of values to use when rendering the 136 | panel's HTML content. This value is usually injected into templates 137 | as the rendering context. 138 | 139 | The ``request`` here is the active request in the toolbar. Not the 140 | original request that this panel represents. 141 | """ 142 | return {} 143 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/headers.py: -------------------------------------------------------------------------------- 1 | from .base import DebugPanel 2 | 3 | __all__ = ["HeaderDebugPanel"] 4 | 5 | 6 | class HeaderDebugPanel(DebugPanel): 7 | """ 8 | A panel to display HTTP request and response headers. 9 | """ 10 | 11 | name = "Header" 12 | has_content = True 13 | template = "headers.jinja2" 14 | title = "HTTP Headers" 15 | nav_title = title 16 | 17 | def __init__(self, request): 18 | super().__init__(request) 19 | self._request_headers = [(k, v) for k, v in sorted(request.headers.items())] 20 | 21 | async def process_response(self, response): 22 | response_headers = [(k, v) for k, v in sorted(response.headers.items())] 23 | self.data = { 24 | "request_headers": self._request_headers, 25 | "response_headers": response_headers, 26 | } 27 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/logger.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | from collections import deque 4 | 5 | from .base import DebugPanel 6 | from ..utils import format_fname 7 | 8 | 9 | class RequestTrackingHandler(logging.Handler): 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | self._records = deque(maxlen=1000) 13 | 14 | @property 15 | def records(self): 16 | return self._records 17 | 18 | def emit(self, record): 19 | self._records.append(record) 20 | 21 | 22 | class LoggingPanel(DebugPanel): 23 | name = "logging" 24 | template = "logger.jinja2" 25 | title = "Log Messages" 26 | nav_title = "Logging" 27 | 28 | def __init__(self, request): 29 | super().__init__(request) 30 | self._log_handler = RequestTrackingHandler() 31 | 32 | def _install_handler(self): 33 | logging.root.addHandler(self._log_handler) 34 | 35 | def _uninstall_handler(self): 36 | logging.root.removeHandler(self._log_handler) 37 | 38 | def wrap_handler(self, handler, context_switcher): 39 | context_switcher.add_context_in(self._install_handler) 40 | context_switcher.add_context_out(self._uninstall_handler) 41 | return handler 42 | 43 | async def process_response(self, response): 44 | records = [] 45 | for record in self._log_handler.records: 46 | records.append( 47 | { 48 | "message": record.getMessage(), 49 | "time": datetime.datetime.fromtimestamp(record.created), 50 | "level": record.levelname, 51 | "file": format_fname(record.pathname), 52 | "file_long": record.pathname, 53 | "line": record.lineno, 54 | } 55 | ) 56 | self.data = {"records": records} 57 | 58 | @property 59 | def has_content(self): 60 | if self.data.get("records"): 61 | return True 62 | return False 63 | 64 | @property 65 | def nav_subtitle(self): 66 | if self.data: 67 | return "%d" % len(self.data.get("records")) 68 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/middlewares.py: -------------------------------------------------------------------------------- 1 | from .base import DebugPanel 2 | from ..utils import STATIC_ROUTE_NAME 3 | 4 | __all__ = ["MiddlewaresDebugPanel"] 5 | 6 | 7 | class MiddlewaresDebugPanel(DebugPanel): 8 | """ 9 | A panel to display the middlewares used by your aiohttp application. 10 | """ 11 | 12 | name = "Middlewares" 13 | has_content = True 14 | template = "middlewares.jinja2" 15 | title = "Middlewares" 16 | nav_title = title 17 | 18 | def __init__(self, request): 19 | super().__init__(request) 20 | if not request.app.middlewares: 21 | self.has_content = False 22 | self.is_active = False 23 | else: 24 | self.populate(request) 25 | 26 | def populate(self, request): 27 | middleware_names = [] 28 | for m in request.app.middlewares: 29 | if hasattr(m, "__name__"): 30 | # name for regular functions 31 | middleware_names.append(m.__name__) 32 | else: 33 | middleware_names.append(m.__repr__()) 34 | self.data = {"middlewares": middleware_names} 35 | 36 | def render_vars(self, request): 37 | static_path = self._request.app.router[STATIC_ROUTE_NAME].canonical 38 | return {"static_path": static_path} 39 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/performance.py: -------------------------------------------------------------------------------- 1 | import cProfile as profile 2 | import pstats 3 | import time 4 | 5 | try: 6 | import resource 7 | except ImportError: # Fails on Windows 8 | resource = None # type: ignore[assignment] 9 | 10 | from .base import DebugPanel 11 | from ..utils import format_fname 12 | 13 | __all__ = ["PerformanceDebugPanel"] 14 | 15 | 16 | class PerformanceDebugPanel(DebugPanel): 17 | """ 18 | Panel that looks at the performance of a request. 19 | 20 | It will display the time a request took and, optionally, the 21 | cProfile output. 22 | """ 23 | 24 | name = "Performance" 25 | user_activate = True 26 | stats = None 27 | function_calls = None 28 | has_content = True 29 | has_resource = bool(resource) 30 | template = "performance.jinja2" 31 | title = "Performance" 32 | nav_title = title 33 | 34 | def __init__(self, request): 35 | super().__init__(request) 36 | self.profiler = profile.Profile() 37 | 38 | def _wrap_timer_handler(self, handler): 39 | if self.has_resource: 40 | 41 | async def resource_timer_handler(request): 42 | _start_time = time.time() 43 | self._start_rusage = resource.getrusage(resource.RUSAGE_SELF) 44 | try: 45 | result = await handler(request) 46 | except BaseException: 47 | raise 48 | finally: 49 | self._end_rusage = resource.getrusage(resource.RUSAGE_SELF) 50 | self.total_time = (time.time() - _start_time) * 1000 51 | 52 | return result 53 | 54 | return resource_timer_handler 55 | 56 | async def noresource_timer_handler(request): 57 | _start_time = time.time() 58 | try: 59 | result = await handler(request) 60 | except BaseException: 61 | raise 62 | finally: 63 | self.total_time = (time.time() - _start_time) * 1000 64 | return result 65 | 66 | return noresource_timer_handler 67 | 68 | def _wrap_profile_handler(self, handler): 69 | if not self.is_active: 70 | return handler 71 | 72 | async def profile_handler(request): 73 | try: 74 | self.profiler.enable() 75 | try: 76 | result = await handler(request) 77 | finally: 78 | self.profiler.disable() 79 | except BaseException: 80 | raise 81 | finally: 82 | stats = pstats.Stats(self.profiler) 83 | function_calls = [] 84 | flist = stats.sort_stats("cumulative").fcn_list 85 | for func in flist: 86 | current = {} 87 | info = stats.stats[func] 88 | 89 | # Number of calls 90 | if info[0] != info[1]: 91 | current["ncalls"] = "%d/%d" % (info[1], info[0]) 92 | else: 93 | current["ncalls"] = info[1] 94 | 95 | # Total time 96 | current["tottime"] = info[2] * 1000 97 | 98 | # Quotient of total time divided by number of calls 99 | if info[1]: 100 | current["percall"] = info[2] * 1000 / info[1] 101 | else: 102 | current["percall"] = 0 103 | 104 | # Cumulative time 105 | current["cumtime"] = info[3] * 1000 106 | 107 | # Quotient of the cumulative time divided by the number 108 | # of primitive calls. 109 | if info[0]: 110 | current["percall_cum"] = info[3] * 1000 / info[0] 111 | else: 112 | current["percall_cum"] = 0 113 | 114 | # Filename 115 | filename = pstats.func_std_string(func) 116 | current["filename_long"] = filename 117 | current["filename"] = format_fname(filename) 118 | function_calls.append(current) 119 | 120 | self.stats = stats 121 | self.function_calls = function_calls 122 | 123 | return result 124 | 125 | return profile_handler 126 | 127 | def wrap_handler(self, handler, context_switcher): 128 | handler = self._wrap_profile_handler(handler) 129 | handler = self._wrap_timer_handler(handler) 130 | return handler 131 | 132 | @property 133 | def nav_subtitle(self): 134 | return "%0.2fms" % (self.total_time) 135 | 136 | def _elapsed_ru(self, name): 137 | return getattr(self._end_rusage, name) - getattr(self._start_rusage, name) 138 | 139 | async def process_response(self, response): 140 | vars = {"timing_rows": None, "stats": None, "function_calls": []} 141 | if self.has_resource: 142 | utime = 1000 * self._elapsed_ru("ru_utime") 143 | stime = 1000 * self._elapsed_ru("ru_stime") 144 | vcsw = self._elapsed_ru("ru_nvcsw") 145 | ivcsw = self._elapsed_ru("ru_nivcsw") 146 | # minflt = self._elapsed_ru('ru_minflt') 147 | # majflt = self._elapsed_ru('ru_majflt') 148 | 149 | # these are documented as not meaningful under Linux. If you're 150 | # running BSD # feel free to enable them, and add any others that 151 | # I hadn't gotten to before I noticed that I was getting nothing 152 | # but zeroes and that the docs agreed. :-( 153 | # 154 | # blkin = self._elapsed_ru('ru_inblock') 155 | # blkout = self._elapsed_ru('ru_oublock') 156 | # swap = self._elapsed_ru('ru_nswap') 157 | # rss = self._end_rusage.ru_maxrss 158 | # srss = self._end_rusage.ru_ixrss 159 | # urss = self._end_rusage.ru_idrss 160 | # usrss = self._end_rusage.ru_isrss 161 | 162 | # TODO l10n on values 163 | rows = ( 164 | ("User CPU time", "%0.3f msec" % utime), 165 | ("System CPU time", "%0.3f msec" % stime), 166 | ("Total CPU time", "%0.3f msec" % (utime + stime)), 167 | ("Elapsed time", "%0.3f msec" % self.total_time), 168 | ("Context switches", "%d voluntary, %d involuntary" % (vcsw, ivcsw)), 169 | # (_('Memory use'), '%d max RSS, %d shared, %d unshared' % ( 170 | # rss, srss, urss + usrss)), 171 | # (_('Page faults'), '%d no i/o, %d requiring i/o' % ( 172 | # minflt, majflt)), 173 | # (_('Disk operations'), '%d in, %d out, %d swapout' % ( 174 | # blkin, blkout, swap)), 175 | ) 176 | vars["timing_rows"] = rows 177 | if self.is_active: 178 | vars["stats"] = self.stats 179 | vars["function_calls"] = self.function_calls 180 | self.data = vars 181 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/request_vars.py: -------------------------------------------------------------------------------- 1 | from pprint import saferepr 2 | 3 | from .base import DebugPanel 4 | 5 | __all__ = ["RequestVarsDebugPanel"] 6 | 7 | 8 | class RequestVarsDebugPanel(DebugPanel): 9 | """ 10 | A panel to display request variables (POST/GET, session, cookies, and 11 | ad-hoc request attributes). 12 | """ 13 | 14 | name = "RequestVars" 15 | has_content = True 16 | template = "request_vars.jinja2" 17 | title = "Request Vars" 18 | nav_title = title 19 | 20 | def __init__(self, request): 21 | super().__init__(request) 22 | 23 | async def process_response(self, response): 24 | self.data = data = {} 25 | request = self.request 26 | post_data = await request.post() 27 | data.update( 28 | { 29 | "get": [(k, request.query.getall(k)) for k in sorted(request.query)], 30 | "post": [(k, saferepr(post_data.getall(k))) for k in sorted(post_data)], 31 | "cookies": [(k, v) for k, v in sorted(request.cookies.items())], 32 | "attrs": [(k, v) for k, v in sorted(request.items())], 33 | } 34 | ) 35 | 36 | # TODO: think about aiohttp_security 37 | 38 | # session to separate table 39 | session = request.get("aiohttp_session") 40 | if session and not session.empty: 41 | data.update( 42 | { 43 | "session": [(k, v) for k, v in sorted(session.items())], 44 | } 45 | ) 46 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/routes.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from .base import DebugPanel 4 | 5 | __all__ = ["RoutesDebugPanel"] 6 | 7 | 8 | class RoutesDebugPanel(DebugPanel): 9 | """ 10 | A panel to display the routes used by your aiohttp application. 11 | """ 12 | 13 | name = "Routes" 14 | has_content = True 15 | template = "routes.jinja2" 16 | title = "Routes" 17 | nav_title = title 18 | 19 | def __init__(self, request): 20 | super().__init__(request) 21 | self.populate(request) 22 | 23 | def populate(self, request): 24 | info = [] 25 | router = request.app.router 26 | 27 | for route in router.routes(): 28 | info.append( 29 | { 30 | "name": route.name or "", 31 | "method": route.method, 32 | "info": sorted(route.get_info().items()), 33 | "handler": repr(route.handler), 34 | "source": inspect.getsource(route.handler), 35 | } 36 | ) 37 | 38 | self.data = {"routes": info} 39 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/settings.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | from .base import DebugPanel 4 | from ..utils import APP_KEY 5 | 6 | __all__ = ["SettingsDebugPanel"] 7 | 8 | 9 | class SettingsDebugPanel(DebugPanel): 10 | """ 11 | A panel to display debug toolbar setting for now. 12 | """ 13 | 14 | name = "Settings" 15 | has_content = True 16 | template = "settings.jinja2" 17 | title = "Settings" 18 | nav_title = title 19 | 20 | def __init__(self, request): 21 | super().__init__(request) 22 | # TODO: show application setting here 23 | # always repr this stuff before it's sent to the template to appease 24 | # dumbass stuff like MongoDB's __getattr__ that always returns a 25 | # Collection, which fails when Jinja tries to look up __html__ on it. 26 | settings = request.app[APP_KEY]["settings"] 27 | # filter out non-pyramid prefixed settings to avoid duplication 28 | reprs = [(k, repr(v)) for k, v in settings.items()] 29 | self.data = {"settings": sorted(reprs, key=itemgetter(0))} 30 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/headers.jinja2: -------------------------------------------------------------------------------- 1 |

Request Headers

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for key, value in request_headers %} 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 | 21 |
KeyValue
{{ key }}{{ value }}
22 | 23 |

Response Headers

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for key, value in response_headers %} 37 | 38 | 39 | 40 | 41 | {% endfor %} 42 | 43 |
KeyValue
{{ key }}{{ value }}
44 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/logger.jinja2: -------------------------------------------------------------------------------- 1 | {% if records %}: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for record in records %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 | 21 |
LevelTimeMessageLocation
{{ record['level'] }}{{ record['time'] }}{{ record['message'] }}{{record['file']}}:{{record['line']}}
22 | {% else %}: 23 |

No messages logged.

24 | {% endif %} 25 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/middlewares.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for middleware in middlewares %} 10 | 11 | 12 | 13 | 14 | {% endfor %} 15 | 16 |
Order (from server to application)Middleware
{{ i }}{{ middleware }}
17 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/performance.jinja2: -------------------------------------------------------------------------------- 1 | {% if timing_rows %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for key, value in timing_rows %} 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 | 21 |
ResourceValue
{{ key }}{{ value }}
22 | {% else %} 23 |

Resource statistics have been disabled. This is because the 'resource' 24 | module could not be found. This module is not supported under Windows.

25 | {% endif %} 26 | 27 |

Profile

28 | {% if stats %} 29 |

Times in milliseconds

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for row in function_calls %} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% endfor %} 52 | 53 |
CallsTotalPercallCumuCumuPerFunc
{{ row['ncalls'] }}{{ '%.4f' % row['tottime'] }}{{ '%.4f' % row['percall'] }}{{ '%.4f' % row['cumtime'] }}{{ '%.4f' % row['percall_cum'] }}{{ row['filename']|e }}
54 | {% else %} 55 |

The profiler is not activated. Activate the checkbox in the toolbar to use it.

56 | {% endif %} 57 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/request_vars.jinja2: -------------------------------------------------------------------------------- 1 |

Cookie Variables

2 | {% if cookies %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for key, value in cookies %} 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 | 22 |
VariableValue
{{ key }}{{ value }}
23 | {% else %} 24 |

No cookie data

25 | {% endif %} 26 | 27 |

Session Variables

28 | {% if session %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% for key, value in session %} 42 | 43 | 44 | 45 | 46 | {% endfor %} 47 | 48 |
VariableValue
{{ key }}{{ value }}
49 | {% else %} 50 |

No session data

51 | {% endif %} 52 | 53 |

GET Variables

54 | {% if get %} 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {% for key, value in get %} 68 | 69 | 70 | 71 | 72 | {% endfor %} 73 | 74 |
VariableValue
{{ key }}{{ ', '.join(value) }}
75 | {% else %} 76 |

No GET data

77 | {% endif %} 78 | 79 |

POST Variables

80 | {% if post %} 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {% for key, value in post %} 94 | 95 | 96 | 97 | 98 | {% endfor %} 99 | 100 |
VariableValue
{{ key }}{{ value }}
101 | {% else %} 102 |

No POST data

103 | {% endif %} 104 | 105 |

Request attributes

106 | {% if attrs %} 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | {% for key, value in attrs %} 120 | 121 | 122 | 123 | 124 | {% endfor %} 125 | 126 |
AttributeValue
{{ key }}{{ value }}
127 | {% else %} 128 |

No request attributes

129 | {% endif %} 130 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/routes.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for route_info in routes %} 12 | 13 | 14 | 15 | 22 | 39 | 40 | {% endfor %} 41 | 42 |
Route NameMethodRoute InfoView Callable
{{ route_info['name'] }}{{ route_info['method'] }} 16 |
    17 | {% for name, value in route_info['info'] %} 18 |
  • {{ name }}: {{ value }} 19 | {% endfor %} 20 |
21 |
23 |
24 |
{{ route_info['handler']|e }}
25 |
26 | 33 |
34 |
35 |
36 |
{{ route_info['source'] }}
37 |
38 |
43 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/settings.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for key, value in settings %} 10 | 11 | 12 | 13 | 14 | {% endfor %} 15 | 16 |
KeyValue
{{ key }}{{ value|e }}
17 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/sqlalchemy_explain.jinja2: -------------------------------------------------------------------------------- 1 | 5 | 41 | 44 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/sqlalchemy_select.jinja2: -------------------------------------------------------------------------------- 1 | 5 | 45 | 48 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/traceback.jinja2: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{ exception_type }}

4 |
5 |
{{ exception }}
6 |
7 |

Traceback (most recent call last)

8 | {{ summary }} 9 |
10 |

11 | 12 | This is the Copy/Paste friendly version of the traceback. 13 |

14 | 16 |
17 | 18 |
19 |

20 | Warning: this feature should not be enabled on production 21 | systems. 22 |

23 | 24 | {% if evalex %} 25 |

26 | 27 | Hover over any gray area in the traceback and click on the 28 | 31 | button on the right hand side of that gray area to show an interactive 32 | console for the associated frame. Type arbitrary Python into the 33 | console; it will be evaluated in the context of the associated frame. In 34 | the interactive console there are helpers available for introspection: 35 | 36 |

    37 |
  • dump() shows all variables in the frame 38 |
  • dump(obj) dumps all that's known about the object 39 |
40 |

41 | {% endif %} 42 | 43 |

44 | Hover over any gray area in the traceback and click on 45 | 48 | on the right hand side of that gray area to show the source of the file 49 | associated with the frame. 50 |

51 | 52 |

53 | Click on the traceback header to switch back and forth between the 54 | rendered version of the traceback and a plaintext copy-paste-friendly 55 | version of the traceback. 56 |

57 | 58 |

59 | URL to recover this traceback page: {{ url }} 60 |

61 |
62 | 63 | 67 |
68 | 73 | 74 | 82 | 84 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/templates/versions.jinja2: -------------------------------------------------------------------------------- 1 |

Platform

2 |
{{ platform }}
3 |

aiohttp

4 |
{{ aiohttp_version }}
5 | 6 |

Packages

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for package in packages %} 18 | 19 | 20 | 21 | 24 | 25 | {% endfor %} 26 | 27 |
Package NameVersionDependencies
{{ package['name'] }}{{ package['version'] }} 22 | {{ package['dependencies']|join(', ') }} 23 |
28 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/traceback.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .base import DebugPanel 4 | from ..tbtools.tbtools import Traceback 5 | from ..utils import APP_KEY, ROOT_ROUTE_NAME, STATIC_ROUTE_NAME, escape 6 | 7 | __all__ = ["TracebackPanel"] 8 | 9 | 10 | class TracebackPanel(DebugPanel): 11 | name = "Traceback" 12 | template = "traceback.jinja2" 13 | title = "Traceback" 14 | nav_title = title 15 | 16 | def __init__(self, request): 17 | super().__init__(request) 18 | self.exc_history = request.app[APP_KEY]["exc_history"] 19 | 20 | @property 21 | def has_content(self): 22 | if self._request.get("pdbt_tb"): 23 | return True 24 | return False 25 | 26 | async def process_response(self, response): 27 | if not self.has_content: 28 | return 29 | traceback = self._request["pdbt_tb"] 30 | 31 | exc = escape(traceback.exception) 32 | summary = Traceback.render_summary( 33 | traceback, self._request.app, include_title=False 34 | ) 35 | token = self.request.app[APP_KEY]["pdtb_token"] 36 | url = "" # self.request.route_url(EXC_ROUTE_NAME, _query=qs) 37 | evalex = self.exc_history.eval_exc 38 | 39 | self.data = { 40 | "evalex": evalex and "true" or "false", 41 | "console": "false", 42 | "lodgeit_url": None, 43 | "title": exc, 44 | "exception": exc, 45 | "exception_type": escape(traceback.exception_type), 46 | "summary": summary, 47 | "plaintext": traceback.plaintext, 48 | "plaintext_cs": re.sub("-{2,}", "-", traceback.plaintext), 49 | "traceback_id": traceback.id, 50 | "token": token, 51 | "url": url, 52 | } 53 | 54 | def render_content(self, request): 55 | return super().render_content(request) 56 | 57 | def render_vars(self, request): 58 | static_path = self._request.app.router[STATIC_ROUTE_NAME].canonical 59 | root_path = self.request.app.router[ROOT_ROUTE_NAME].url_for() 60 | return {"static_path": static_path, "root_path": root_path} 61 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/panels/versions.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | from importlib.metadata import Distribution, version 4 | from operator import itemgetter 5 | from typing import ClassVar, Dict, List, Optional, TypedDict 6 | 7 | from .base import DebugPanel 8 | 9 | 10 | __all__ = ("VersionDebugPanel",) 11 | aiohttp_version = version("aiohttp") 12 | 13 | 14 | class _Package(TypedDict): 15 | version: str 16 | lowername: str 17 | name: str 18 | dependencies: List[str] 19 | url: Optional[str] 20 | 21 | 22 | class VersionDebugPanel(DebugPanel): 23 | """ 24 | Panel that displays the Python version, the aiohttp version, and the 25 | versions of other software on your PYTHONPATH. 26 | """ 27 | 28 | name = "Version" 29 | has_content = True 30 | template = "versions.jinja2" 31 | title = "Versions" 32 | nav_title = title 33 | packages: ClassVar[Optional[List[Dict[str, str]]]] = None 34 | 35 | def __init__(self, request): 36 | super().__init__(request) 37 | self.data = { 38 | "platform": self.get_platform(), 39 | "packages": self.get_packages(), 40 | "aiohttp_version": aiohttp_version, 41 | } 42 | 43 | @classmethod 44 | def get_packages(cls) -> List[Dict[str, str]]: 45 | if VersionDebugPanel.packages: 46 | return VersionDebugPanel.packages 47 | 48 | packages: List[_Package] = [] 49 | for distribution in Distribution.discover(): 50 | name = distribution.metadata["Name"] 51 | dependencies = [d for d in distribution.requires or ()] 52 | url = distribution.metadata.get("Home-page") 53 | 54 | packages.append( 55 | { 56 | "version": distribution.version, 57 | "lowername": name.lower(), 58 | "name": name, 59 | "dependencies": dependencies, 60 | "url": url, 61 | } 62 | ) 63 | 64 | VersionDebugPanel.packages = sorted(packages, key=itemgetter("lowername")) # type: ignore[arg-type] 65 | return VersionDebugPanel.packages 66 | 67 | def _get_platform_name(self): 68 | return platform.platform() 69 | 70 | def get_platform(self): 71 | return f"Python {sys.version} on {self._get_platform_name()}" 72 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/py.typed -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 70px; 8 | } 9 | 10 | a { 11 | color: #e28d29; 12 | } 13 | 14 | a:hover { 15 | color: #c56b00; 16 | } 17 | 18 | .navbar-collapse { 19 | border-color: #e28d29; 20 | } 21 | 22 | /* 23 | .navbar-inner { 24 | background-color: #dd7c0d; 25 | background-image: linear-gradient(left, #dd7c0d, #dbc1a4; 26 | background-image: -moz-linear-gradient(left, #dd7c0d, #dbc1a4; 27 | background-image: -ms-linear-gradient(left, #dd7c0d, #dbc1a4; 28 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#dd7c0d), to(#dbc1a4); 29 | background-image: -webkit-linear-gradient(left, #dd7c0d, #dbc1a4; 30 | background-image: -o-linear-gradient(left, #dd7c0d, #dbc1a4; 31 | } 32 | */ 33 | 34 | .navbar-inverse { 35 | background: #dd7c0d; /* Old browsers */ 36 | background: -moz-linear-gradient(left, #dd7c0d 0%, #c99662 100%); /* FF3.6+ */ 37 | background: -webkit-gradient(linear, left top, right top, color-stop(0%,#dd7c0d), color-stop(100%,#c99662)); /* Chrome,Safari4+ */ 38 | background: -webkit-linear-gradient(left, #dd7c0d 0%,#c99662 100%); /* Chrome10+,Safari5.1+ */ 39 | background: -o-linear-gradient(left, #dd7c0d 0%,#c99662 100%); /* Opera 11.10+ */ 40 | background: -ms-linear-gradient(left, #dd7c0d 0%,#c99662 100%); /* IE10+ */ 41 | background: linear-gradient(to right, #dd7c0d 0%,#c99662 100%); /* W3C */ 42 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dd7c0d', endColorstr='#c99662',GradientType=1 ); /* IE6-9 */ 43 | } 44 | 45 | .navbar-inverse .navbar-brand, .navbar-inverse .navbar-nav > li > a { 46 | color: #fff; 47 | } 48 | 49 | .navbar-inverse .navbar-nav > .active > a, 50 | .navbar-inverse .navbar-nav > .active > a:hover, 51 | .navbar-inverse .navbar-nav > .active > a:focus, 52 | 53 | .navbar-inverse .navbar-nav > .open > a, 54 | .navbar-inverse .navbar-nav > .open > a:hover, 55 | .navbar-inverse .navbar-nav > .open > a:focus, 56 | 57 | .navbar-inverse .navbar-nav > li.active > a, 58 | .navbar-inverse .navbar-nav > li.active:hover > a { 59 | background-color: #c56b00; 60 | } 61 | 62 | .navbar-brand > img { 63 | position: absolute; 64 | top: 10px; 65 | left: 10px; 66 | width: 35px; 67 | height: 35px; 68 | } 69 | 70 | .navbar-brand { 71 | padding-left: 50px; 72 | } 73 | 74 | /* 75 | * Global add-ons 76 | */ 77 | 78 | .sub-header { 79 | padding-bottom: 10px; 80 | border-bottom: 1px solid #eee; 81 | } 82 | 83 | 84 | /* 85 | * Sidebar 86 | */ 87 | 88 | /* Hide for mobile, show later */ 89 | .sidebar { 90 | display: none; 91 | } 92 | @media (min-width: 768px) { 93 | .navbar-nav { 94 | float: right!important; 95 | } 96 | .sidebar { 97 | position: fixed; 98 | top: 0; 99 | left: 0; 100 | bottom: 0; 101 | z-index: 1000; 102 | display: block; 103 | padding: 70px 20px 20px; 104 | background-color: #eee; 105 | border-right: 1px solid #eee; 106 | overflow-y: scroll; 107 | } 108 | } 109 | 110 | /* Sidebar navigation */ 111 | .nav-sidebar { 112 | margin-left: -20px; 113 | margin-right: -21px; /* 20px padding + 1px border */ 114 | margin-bottom: 20px; 115 | } 116 | .nav-sidebar > li > h4, .nav-sidebar > li > a { 117 | padding-left: 20px; 118 | padding-right: 20px; 119 | width: 100%; 120 | } 121 | .nav-sidebar > li > a { 122 | color: #555; 123 | } 124 | .nav-sidebar > .active > a { 125 | color: #fff; 126 | background: #999; 127 | } 128 | .nav-sidebar > .active:hover > a { 129 | background: #999; 130 | } 131 | 132 | /* 133 | * Main content 134 | */ 135 | 136 | .main { 137 | padding: 0px 20px 20px 20px; 138 | } 139 | @media (min-width: 768px) { 140 | .main { 141 | padding-left: 40px; 142 | padding-right: 40px; 143 | } 144 | } 145 | .main .page-header { 146 | margin-top: 0; 147 | } 148 | 149 | 150 | /* 151 | * Placeholder dashboard ideas 152 | */ 153 | 154 | .placeholders { 155 | margin-bottom: 30px; 156 | text-align: center; 157 | } 158 | .placeholders h4 { 159 | margin-bottom: 0; 160 | } 161 | .placeholder { 162 | margin-bottom: 20px; 163 | } 164 | .placeholder img { 165 | border-radius: 50%; 166 | } 167 | ul#requests li a { 168 | overflow: hidden; 169 | white-space: nowrap; 170 | text-overflow: ellipsis; 171 | display: inline-block; 172 | max-width: 300px; 173 | } 174 | /* override twitter bootstrap's tooltip styles */ 175 | .tooltip, .tooltip-inner { 176 | max-width: 100%; 177 | } 178 | .tooltip { 179 | word-wrap: break-word; 180 | word-break: break-all; 181 | } 182 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/css/debugger.css: -------------------------------------------------------------------------------- 1 | /* TODO: cleanup unused styles */ 2 | input { background-color: #fff; margin: 0; text-align: left; 3 | outline: none !important; } 4 | pre, code, table.source, 5 | textarea { font-family: Consolas, "andale mono", "lucida console", monospace; font-size: 14px; } 6 | 7 | div.debugger { text-align: left; padding: 12px; margin: auto; 8 | background-color: white; } 9 | h1 { margin: 0 0 0.3em 0; } 10 | div.detail p { margin: 0 0 8px 13px; font-size: 14px; } 11 | div.detail pre.errormsg { word-wrap: break-word; } 12 | div.explanation { margin: 20px 13px; font-size: 15px; color: #555; } 13 | div.footer { font-size: 13px; text-align: right; margin: 30px 0; 14 | color: #999999; } 15 | 16 | h2 { margin: 1.3em 0 0.0 0; padding: 9px; 17 | background: #e28d29; color: #000000; } 18 | h2.traceback, h3.traceback { 19 | color: #ffffff; 20 | border-top-right-radius: 4px; 21 | border-top-left-radius: 4px; 22 | } 23 | h2 small, h3 small { font-style: normal; color: #ffffff; font-weight: normal; } 24 | 25 | div.traceback { 26 | border-bottom-left-radius: 4px; 27 | border-bottom-right-radius: 1px; 28 | } 29 | div.traceback, div.plain { border: 1px solid #ddd; margin: 0 0 1em 0; padding: 10px; } 30 | div.plain p { margin: 0; } 31 | div.plain textarea, 32 | div.plain pre { margin: 10px 0 0 0; padding: 4px; 33 | background-color: #eeeeee; border: 1px solid #dddddd; } 34 | div.plain textarea { width: 99%; height: 300px; } 35 | div.traceback h3 { margin: 0; } 36 | div.traceback ul { list-style: none; margin: 0; padding: 0; } 37 | div.traceback h4 { font-size: 13px; font-weight: normal; margin: 5px 0; } 38 | div.traceback pre { margin: 0; padding: 5px 0 3px 5px; 39 | background-color: #f5f5f5;} 40 | div.traceback pre, 41 | div.box table.source { white-space: pre-wrap; /* css-3 should we be so lucky... */ 42 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 43 | white-space: -pre-wrap; /* Opera 4-6 ?? */ 44 | white-space: -o-pre-wrap; /* Opera 7 ?? */ 45 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 46 | _white-space: pre; /* IE only hack to re-specify in 47 | addition to word-wrap */ } 48 | div.box h2 { color: #ffffff; } 49 | div.traceback pre:hover { background-color: #eeeeee; color: black; cursor: pointer; } 50 | div.traceback blockquote { margin: 1em 0 0 0; padding: 0; } 51 | div.traceback pre:hover img { display: block; } 52 | div.traceback cite.filename { font-style: normal; color: #666666; } 53 | 54 | pre.console { border: 1px solid #ccc; background: white!important; 55 | color: black; padding: 5px!important; 56 | margin: 3px 0 0 0!important; cursor: default!important; 57 | overflow: auto; } 58 | pre.console form { color: #555; } 59 | pre.console input { background-color: transparent; color: #555; 60 | width: 90%; font-family: Consolas, "andale mono", "lucida console", monospace; font-size: 14px; 61 | border: none!important; } 62 | 63 | span.string { color: #30799B; } 64 | span.number { color: #9C1A1C; } 65 | span.help { color: #3A7734; } 66 | span.object { color: #485F6E; } 67 | 68 | pre.console div.traceback, 69 | pre.console div.box { margin: 5px; white-space: normal; 70 | border: 1px solid #dddddd; padding: 5px; 71 | font-family: sans-serif; } 72 | pre.console div.box h3, 73 | pre.console div.traceback h3 { margin: -5px -5px 5px -5px; padding: 5px; 74 | background: #e28d29; color: #ffffff; } 75 | 76 | pre.console div.traceback pre:hover { cursor: default; background: #eeeeee; } 77 | pre.console div.traceback pre.syntaxerror { background: #eeeeee; border: none; 78 | margin: 0px 0px 0px 0px; 79 | padding: 10px; border-top: 1px solid #dddddd; } 80 | pre.console div.noframe-traceback pre.syntaxerror { margin-top: 0px; border: none; } 81 | 82 | pre.console div.box pre.repr { padding: 0; margin: 0; background-color: white; border: none; } 83 | pre.console div.box table { margin-top: 6px; } 84 | pre.console div.box pre { border: none; } 85 | pre.console div.box pre.help { background-color: white; } 86 | pre.console div.box pre.help:hover { cursor: default; } 87 | pre.console table tr { vertical-align: top; } 88 | pre.console table th { white-space: nowrap; } 89 | div.console { border: 1px solid #dddddd; padding: 4px; background-color: #f5f5f5; } 90 | 91 | div.box table.source { border-collapse: collapse; width: 100%; background: #f5f5f5; 92 | font-size: 12px; 93 | } 94 | div.box table.source td { border-top: 1px solid #dddddd; padding: 4px 0 4px 10px; } 95 | div.box table.source td.lineno { color: #999; padding-right: 10px; width: 1px; } 96 | div.box table.source tr.in-frame { background-color: white; } 97 | div.box table.source tr.current { background-color: #dddddd; color: #e5762b; } 98 | div.sourceview { overflow: auto; border: 1px solid #ccc; } 99 | 100 | div.traceback .btn { margin: 1px 4px 1px 0; } 101 | div.traceback span, div.explanation span { margin: 2px; } 102 | 103 | div.sourceview span.current { 104 | display: inline-block; 105 | width: 100%; 106 | background-color: #ddd; 107 | } 108 | 109 | td.value { 110 | word-break: break-all; 111 | word-wrap: break-word; 112 | } 113 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/css/highlightjs_default.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#f0f0f0;-webkit-text-size-adjust:none}.hljs,.hljs-subst,.hljs-tag .hljs-title,.nginx .hljs-title{color:black}.hljs-string,.hljs-title,.hljs-constant,.hljs-parent,.hljs-tag .hljs-value,.hljs-rule .hljs-value,.hljs-preprocessor,.hljs-pragma,.hljs-name,.haml .hljs-symbol,.ruby .hljs-symbol,.ruby .hljs-symbol .hljs-string,.hljs-template_tag,.django .hljs-variable,.smalltalk .hljs-class,.hljs-addition,.hljs-flow,.hljs-stream,.bash .hljs-variable,.pf .hljs-variable,.apache .hljs-tag,.apache .hljs-cbracket,.tex .hljs-command,.tex .hljs-special,.erlang_repl .hljs-function_or_atom,.asciidoc .hljs-header,.markdown .hljs-header,.coffeescript .hljs-attribute,.tp .hljs-variable{color:#800}.smartquote,.hljs-comment,.hljs-annotation,.diff .hljs-header,.hljs-chunk,.asciidoc .hljs-blockquote,.markdown .hljs-blockquote{color:#888}.hljs-number,.hljs-date,.hljs-regexp,.hljs-literal,.hljs-hexcolor,.smalltalk .hljs-symbol,.smalltalk .hljs-char,.go .hljs-constant,.hljs-change,.lasso .hljs-variable,.makefile .hljs-variable,.asciidoc .hljs-bullet,.markdown .hljs-bullet,.asciidoc .hljs-link_url,.markdown .hljs-link_url{color:#080}.hljs-label,.ruby .hljs-string,.hljs-decorator,.hljs-filter .hljs-argument,.hljs-localvars,.hljs-array,.hljs-attr_selector,.hljs-important,.hljs-pseudo,.hljs-pi,.haml .hljs-bullet,.hljs-doctype,.hljs-deletion,.hljs-envvar,.hljs-shebang,.apache .hljs-sqbracket,.nginx .hljs-built_in,.tex .hljs-formula,.erlang_repl .hljs-reserved,.hljs-prompt,.asciidoc .hljs-link_label,.markdown .hljs-link_label,.vhdl .hljs-attribute,.clojure .hljs-attribute,.asciidoc .hljs-attribute,.lasso .hljs-attribute,.coffeescript .hljs-property,.hljs-phony{color:#88f}.hljs-keyword,.hljs-id,.hljs-title,.hljs-built_in,.css .hljs-tag,.hljs-doctag,.smalltalk .hljs-class,.hljs-winutils,.bash .hljs-variable,.pf .hljs-variable,.apache .hljs-tag,.hljs-type,.hljs-typename,.tex .hljs-command,.asciidoc .hljs-strong,.markdown .hljs-strong,.hljs-request,.hljs-status,.tp .hljs-data,.tp .hljs-io{font-weight:bold}.asciidoc .hljs-emphasis,.markdown .hljs-emphasis,.tp .hljs-units{font-style:italic}.nginx .hljs-built_in{font-weight:normal}.coffeescript .javascript,.javascript .xml,.lasso .markup,.tex .hljs-formula,.xml .javascript,.xml .vbscript,.xml .css,.xml .hljs-cdata{opacity:0.5} 2 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/css/prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+git+python+sql&plugins=line-highlight+line-numbers */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | text-shadow: 0 1px white; 12 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 13 | direction: ltr; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 33 | text-shadow: none; 34 | background: #b3d4fc; 35 | } 36 | 37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 38 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 39 | text-shadow: none; 40 | background: #b3d4fc; 41 | } 42 | 43 | @media print { 44 | code[class*="language-"], 45 | pre[class*="language-"] { 46 | text-shadow: none; 47 | } 48 | } 49 | 50 | /* Code blocks */ 51 | pre[class*="language-"] { 52 | padding: 1em; 53 | margin: .5em 0; 54 | overflow: auto; 55 | } 56 | 57 | :not(pre) > code[class*="language-"], 58 | pre[class*="language-"] { 59 | background: #f5f2f0; 60 | } 61 | 62 | /* Inline code */ 63 | :not(pre) > code[class*="language-"] { 64 | padding: .1em; 65 | border-radius: .3em; 66 | white-space: normal; 67 | } 68 | 69 | .token.comment, 70 | .token.prolog, 71 | .token.doctype, 72 | .token.cdata { 73 | color: slategray; 74 | } 75 | 76 | .token.punctuation { 77 | color: #999; 78 | } 79 | 80 | .namespace { 81 | opacity: .7; 82 | } 83 | 84 | .token.property, 85 | .token.tag, 86 | .token.boolean, 87 | .token.number, 88 | .token.constant, 89 | .token.symbol, 90 | .token.deleted { 91 | color: #905; 92 | } 93 | 94 | .token.selector, 95 | .token.attr-name, 96 | .token.string, 97 | .token.char, 98 | .token.builtin, 99 | .token.inserted { 100 | color: #690; 101 | } 102 | 103 | .token.operator, 104 | .token.entity, 105 | .token.url, 106 | .language-css .token.string, 107 | .style .token.string { 108 | color: #a67f59; 109 | background: hsla(0, 0%, 100%, .5); 110 | } 111 | 112 | .token.atrule, 113 | .token.attr-value, 114 | .token.keyword { 115 | color: #07a; 116 | } 117 | 118 | .token.function { 119 | color: #DD4A68; 120 | } 121 | 122 | .token.regex, 123 | .token.important, 124 | .token.variable { 125 | color: #e90; 126 | } 127 | 128 | .token.important, 129 | .token.bold { 130 | font-weight: bold; 131 | } 132 | .token.italic { 133 | font-style: italic; 134 | } 135 | 136 | .token.entity { 137 | cursor: help; 138 | } 139 | 140 | pre[data-line] { 141 | position: relative; 142 | padding: 1em 0 1em 3em; 143 | } 144 | 145 | .line-highlight { 146 | position: absolute; 147 | left: 0; 148 | right: 0; 149 | padding: inherit 0; 150 | margin-top: 1em; /* Same as .prism’s padding-top */ 151 | 152 | background: hsla(24, 20%, 50%,.08); 153 | background: -moz-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); 154 | background: -webkit-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); 155 | background: -o-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); 156 | background: linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); 157 | 158 | pointer-events: none; 159 | 160 | line-height: inherit; 161 | white-space: pre; 162 | } 163 | 164 | .line-highlight:before, 165 | .line-highlight[data-end]:after { 166 | content: attr(data-start); 167 | position: absolute; 168 | top: .4em; 169 | left: .6em; 170 | min-width: 1em; 171 | padding: 0 .5em; 172 | background-color: hsla(24, 20%, 50%,.4); 173 | color: hsl(24, 20%, 95%); 174 | font: bold 65%/1.5 sans-serif; 175 | text-align: center; 176 | vertical-align: .3em; 177 | border-radius: 999px; 178 | text-shadow: none; 179 | box-shadow: 0 1px white; 180 | } 181 | 182 | .line-highlight[data-end]:after { 183 | content: attr(data-end); 184 | top: auto; 185 | bottom: .4em; 186 | } 187 | pre.line-numbers { 188 | position: relative; 189 | padding-left: 3.8em; 190 | counter-reset: linenumber; 191 | } 192 | 193 | pre.line-numbers > code { 194 | position: relative; 195 | } 196 | 197 | .line-numbers .line-numbers-rows { 198 | position: absolute; 199 | pointer-events: none; 200 | top: 0; 201 | font-size: 100%; 202 | left: -3.8em; 203 | width: 3em; /* works for line-numbers below 1000 lines */ 204 | letter-spacing: -1px; 205 | border-right: 1px solid #999; 206 | 207 | -webkit-user-select: none; 208 | -moz-user-select: none; 209 | -ms-user-select: none; 210 | user-select: none; 211 | 212 | } 213 | 214 | .line-numbers-rows > span { 215 | pointer-events: none; 216 | display: block; 217 | counter-increment: linenumber; 218 | } 219 | 220 | .line-numbers-rows > span:before { 221 | content: counter(linenumber); 222 | color: #999; 223 | display: block; 224 | padding-right: 0.8em; 225 | text-align: right; 226 | } 227 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/css/toolbar.css: -------------------------------------------------------------------------------- 1 | ._200 { 2 | background-color: #468847; 3 | } 4 | ._500 { 5 | background-color: #b94a48; 6 | } 7 | ._404{ 8 | background-color: #3a87ad; 9 | } 10 | 11 | .switch { 12 | font-size: 10px; 13 | display: block; 14 | color: white; 15 | height: 16px; 16 | width: 16px; 17 | cursor: pointer; 18 | } 19 | 20 | .switch.active { 21 | background-image: url(../img/tick.png); 22 | } 23 | 24 | .switch.inactive{ 25 | background-image: url(../img/tick-red.png); 26 | } 27 | 28 | li.nav-label { 29 | position: relative; 30 | display: block; 31 | padding: 10px 15px 10px 0px; 32 | } 33 | 34 | li.nav-label strong { 35 | font-size: 150%; 36 | } 37 | a.active { 38 | background: #fff; 39 | } 40 | 41 | #pDebug #pDebugToolbarHandle { 42 | position:fixed; 43 | border:1px solid #e28d29; 44 | top: 25%; 45 | right:0; 46 | z-index:100000000; 47 | } 48 | 49 | @media print { 50 | #pDebug { 51 | display: none; 52 | } 53 | } 54 | 55 | #pDebug a#pShowToolBarButton { 56 | display:block; 57 | height:40px; 58 | width:40px; 59 | border-right:none; 60 | border-bottom:1px solid #fff; 61 | border-top:1px solid #fff; 62 | border-left:1px solid #fff; 63 | color:#fff; 64 | font-size:10px; 65 | font-weight:bold; 66 | text-decoration:none; 67 | text-align:center; 68 | text-indent:-999999px; 69 | background-color:#e28d29; 70 | background-size:40px 40px; 71 | background-image:url(../img/aiohttp.svg); 72 | background-position: left center; 73 | background-repead: no-repeat; 74 | opacity:0.8; 75 | filter:alpha(opacity=80); 76 | } 77 | 78 | #pDebug a#pShowToolBarButton:hover { 79 | background-color:#e28d29; 80 | padding-right:2px; 81 | border-top-color:#fff; 82 | border-left-color:#fff; 83 | border-bottom-color:#fff; 84 | opacity:1.0; 85 | } 86 | 87 | /* tablesorter */ 88 | .header { cursor: pointer; position: relative; } 89 | .header:before { position: absolute; right: 0; bottom: 40%; content: '▲'; font-size: 10px; color: #bbb; } 90 | .header:after { position: absolute; right: 0; bottom: 20%; content: '▼'; font-size: 10px; color: #bbb; } 91 | .headerSortUp:before { content: ''; } 92 | .headerSortUp:after { color: inherit; } 93 | .headerSortDown:before { color: inherit; } 94 | .headerSortDown:after { content: ''; } 95 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/css/toolbar_button.css: -------------------------------------------------------------------------------- 1 | 2 | #pDebug #pDebugToolbarHandle { 3 | position:fixed; 4 | border:2px solid #e28d29; 5 | top: 25%; 6 | right:0; 7 | z-index:100000000; 8 | 9 | -webkit-border-top-left-radius: 20px; 10 | -webkit-border-bottom-left-radius: 20px; 11 | -moz-border-radius-topleft: 20px; 12 | -moz-border-radius-bottomleft: 20px; 13 | border-top-left-radius: 20px; 14 | border-bottom-left-radius: 20px; 15 | } 16 | 17 | 18 | #pDebug a#pShowToolBarButton { 19 | display:block; 20 | height:40px; 21 | width:50px; 22 | border-right:none; 23 | border-bottom:1px solid #fff; 24 | border-top:1px solid #fff; 25 | border-left:1px solid #fff; 26 | color:#fff; 27 | font-size:10px; 28 | font-weight:bold; 29 | text-decoration:none; 30 | text-align:center; 31 | text-indent:-999999px; 32 | background-color:#fff; 33 | background-size:40px 40px; 34 | background-image:url(../img/aiohttp.svg); 35 | background-position: left center; 36 | background-repeat: no-repeat; 37 | opacity:0.8; 38 | filter:alpha(opacity=80); 39 | 40 | -webkit-border-top-left-radius: 20px; 41 | -webkit-border-bottom-left-radius: 20px; 42 | -moz-border-radius-topleft: 20px; 43 | -moz-border-radius-bottomleft: 20px; 44 | border-top-left-radius: 20px; 45 | border-bottom-left-radius: 20px; 46 | } 47 | 48 | #pDebug a#pShowToolBarButton:hover { 49 | background-color:#fff; 50 | border-top-color:#fff; 51 | border-left-color:#fff; 52 | border-bottom-color:#fff; 53 | 54 | border-right:5px solid #ffc17a; 55 | opacity:1.0; 56 | 57 | -webkit-border-top-left-radius: 20px; 58 | -webkit-border-bottom-left-radius: 20px; 59 | -moz-border-radius-topleft: 20px; 60 | -moz-border-radius-bottomleft: 20px; 61 | border-top-left-radius: 20px; 62 | border-bottom-left-radius: 20px; 63 | } 64 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/font/FONT_LICENSE: -------------------------------------------------------------------------------- 1 | ------------------------------- 2 | UBUNTU FONT LICENCE Version 1.0 3 | ------------------------------- 4 | 5 | PREAMBLE 6 | This licence allows the licensed fonts to be used, studied, modified and 7 | redistributed freely. The fonts, including any derivative works, can be 8 | bundled, embedded, and redistributed provided the terms of this licence 9 | are met. The fonts and derivatives, however, cannot be released under 10 | any other licence. The requirement for fonts to remain under this 11 | licence does not require any document created using the fonts or their 12 | derivatives to be published under this licence, as long as the primary 13 | purpose of the document is not to be a vehicle for the distribution of 14 | the fonts. 15 | 16 | DEFINITIONS 17 | "Font Software" refers to the set of files released by the Copyright 18 | Holder(s) under this licence and clearly marked as such. This may 19 | include source files, build scripts and documentation. 20 | 21 | "Original Version" refers to the collection of Font Software components 22 | as received under this licence. 23 | 24 | "Modified Version" refers to any derivative made by adding to, deleting, 25 | or substituting -- in part or in whole -- any of the components of the 26 | Original Version, by changing formats or by porting the Font Software to 27 | a new environment. 28 | 29 | "Copyright Holder(s)" refers to all individuals and companies who have a 30 | copyright ownership of the Font Software. 31 | 32 | "Substantially Changed" refers to Modified Versions which can be easily 33 | identified as dissimilar to the Font Software by users of the Font 34 | Software comparing the Original Version with the Modified Version. 35 | 36 | To "Propagate" a work means to do anything with it that, without 37 | permission, would make you directly or secondarily liable for 38 | infringement under applicable copyright law, except executing it on a 39 | computer or modifying a private copy. Propagation includes copying, 40 | distribution (with or without modification and with or without charging 41 | a redistribution fee), making available to the public, and in some 42 | countries other activities as well. 43 | 44 | PERMISSION & CONDITIONS 45 | This licence does not grant any rights under trademark law and all such 46 | rights are reserved. 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a 49 | copy of the Font Software, to propagate the Font Software, subject to 50 | the below conditions: 51 | 52 | 1) Each copy of the Font Software must contain the above copyright 53 | notice and this licence. These can be included either as stand-alone 54 | text files, human-readable headers or in the appropriate machine- 55 | readable metadata fields within text or binary files as long as those 56 | fields can be easily viewed by the user. 57 | 58 | 2) The font name complies with the following: 59 | (a) The Original Version must retain its name, unmodified. 60 | (b) Modified Versions which are Substantially Changed must be renamed to 61 | avoid use of the name of the Original Version or similar names entirely. 62 | (c) Modified Versions which are not Substantially Changed must be 63 | renamed to both (i) retain the name of the Original Version and (ii) add 64 | additional naming elements to distinguish the Modified Version from the 65 | Original Version. The name of such Modified Versions must be the name of 66 | the Original Version, with "derivative X" where X represents the name of 67 | the new work, appended to that name. 68 | 69 | 3) The name(s) of the Copyright Holder(s) and any contributor to the 70 | Font Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except (i) as required by this licence, (ii) to 72 | acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with 73 | their explicit written permission. 74 | 75 | 4) The Font Software, modified or unmodified, in part or in whole, must 76 | be distributed entirely under this licence, and must not be distributed 77 | under any other licence. The requirement for fonts to remain under this 78 | licence does not affect any document created using the Font Software, 79 | except any version of the Font Software extracted from a document 80 | created using the Font Software may only be distributed under this 81 | licence. 82 | 83 | TERMINATION 84 | This licence becomes null and void if any of the above conditions are 85 | not met. 86 | 87 | DISCLAIMER 88 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 89 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 90 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF 91 | COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 92 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 93 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 94 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 95 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER 96 | DEALINGS IN THE FONT SOFTWARE. 97 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/font/ubuntu.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/font/ubuntu.ttf -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/asc.gif -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/back.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/back_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/back_hover.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/bg.gif -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/close.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/close_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/close_hover.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/console.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/desc.gif -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/headerbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/headerbg.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/indicator.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/less.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/less.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/more.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/panel_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/panel_bg.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/source.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/tick-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/tick-red.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/img/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/aiohttp_debugtoolbar/static/img/tick.png -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/js/README.rst: -------------------------------------------------------------------------------- 1 | A note on the included libraries 2 | ================================ 3 | 4 | The following libraries have been patched to prevent conflicts with the 5 | applications that include this toolbar. If these libraries are upgraded they 6 | must be re-patched. 7 | 8 | - require.js 9 | - jquery-1.7.2.min.js 10 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/js/debugger.js: -------------------------------------------------------------------------------- 1 | pyramid_debugtoolbar_require.config({ 2 | paths: { 3 | "jquery": "jquery-1.10.2.min", 4 | "toolbar": "toolbar" 5 | } 6 | }); 7 | 8 | pyramid_debugtoolbar_require([ 9 | "jquery", 10 | "toolbar"], function($, tablesorter, toolbar) { 11 | 12 | $(function() { 13 | var sourceView = null; 14 | 15 | /** 16 | * if we are in console mode, show the console. 17 | */ 18 | if (window.CONSOLE_MODE && window.EVALEX) { 19 | openShell(null, $('div.console div.inner').empty(), 0); 20 | } 21 | 22 | $('div.traceback div.frame').each(function() { 23 | var 24 | target = $('pre', this) 25 | .click(function() { 26 | sourceButton.click(); 27 | }), 28 | consoleNode = null, source = null, 29 | frameID = this.id.substring(6); 30 | 31 | /** 32 | * Add an interactive console to the frames 33 | */ 34 | if (EVALEX) 35 | $('') 38 | .attr('title', 'Open an interactive python shell in this frame') 39 | .click(function() { 40 | consoleNode = openShell(consoleNode, target, frameID); 41 | return false; 42 | }) 43 | .prependTo(target); 44 | 45 | /** 46 | * Show sourcecode 47 | */ 48 | var sourceButton = $('') 51 | .attr('title', 'Display the sourcecode for this frame') 52 | .click(function () { 53 | if (!sourceView) 54 | $('h2', sourceView = 55 | $('

View Source

' + 56 | '
') 57 | .insertBefore('div.explanation')) 58 | .css('cursor', 'pointer') 59 | .click(function () { 60 | sourceView.slideUp('fast'); 61 | }); 62 | $.get(window.DEBUG_TOOLBAR_ROOT_PATH + '/source', 63 | {frm: frameID, token: window.DEBUGGER_TOKEN}, function (data) { 64 | var dataLine; 65 | var inFrame = data.inFrame; 66 | inFrame[0]++; 67 | if (inFrame !== null && inFrame[0] !== inFrame[1]) { 68 | dataLine = '' + inFrame[0] + '-' + (inFrame[1]); 69 | //if (data.line !== inFrame[0] && data.line !== inFrame[1]) { 70 | dataLine += (',' + data.line); 71 | //} 72 | } else { 73 | dataLine = '' + data.line 74 | } 75 | var code = '
'
 77 |                 + data.source + '
'; 78 | 79 | $('pre', sourceView) 80 | .replaceWith(code); 81 | 82 | Prism.highlightElement(document.querySelector('#pDebugTracebackSrc')); 83 | 84 | if (!sourceView.is(':visible')) 85 | sourceView.slideDown('fast', function () { 86 | focusSourceBlock(); 87 | }); 88 | else 89 | focusSourceBlock(); 90 | }); 91 | return false; 92 | }) 93 | .prependTo(target); 94 | }); 95 | 96 | /** 97 | * toggle traceback types on click. 98 | */ 99 | $('h2.traceback').click(function() { 100 | $(this).next().slideToggle('fast'); 101 | $('div.plain').slideToggle('fast'); 102 | }).css('cursor', 'pointer'); 103 | $('div.plain').hide(); 104 | 105 | /** 106 | * Add extra info (this is here so that only users with JavaScript 107 | * enabled see it.) 108 | */ 109 | $('span.nojavascript') 110 | .removeClass('nojavascript') 111 | .html('

To switch between the interactive traceback and the plaintext ' + 112 | 'one, you can click on the "Traceback" headline. From the text ' + 113 | 'traceback you can also create a paste of it. ' + (!window.EVALEX ? '' : 114 | 'For code execution mouse-over the frame you want to debug and ' + 115 | 'click on the console icon on the right side.' + 116 | '

You can execute arbitrary Python code in the stack frames and ' + 117 | 'there are some extra helpers available for introspection:' + 118 | '

')); 120 | 121 | /** 122 | * Add the pastebin feature 123 | */ 124 | $('div.plain form') 125 | .submit(function() { 126 | var label = $('input[type="submit"]', this); 127 | var old_val = label.val(); 128 | label.val('submitting...'); 129 | $.ajax({ 130 | dataType: 'json', 131 | url: window.DEBUG_TOOLBAR_ROOT_PATH + '/paste', 132 | data: {tb: window.TRACEBACK, token: window.DEBUGGER_TOKEN}, 133 | success: function(data) { 134 | $('div.plain span.pastemessage') 135 | .removeClass('pastemessage') 136 | .text('Paste created: ') 137 | .append($('#' + data.id + '').attr('href', data.url)); 138 | }, 139 | error: function() { 140 | alert('Error: Could not submit paste. No network connection?'); 141 | label.val(old_val); 142 | } 143 | }); 144 | return false; 145 | }); 146 | 147 | // if we have javascript we submit by ajax anyways, so no need for the 148 | // not scaling textarea. 149 | var plainTraceback = $('div.plain textarea'); 150 | plainTraceback.replaceWith($('
').text(plainTraceback.text()));
151 | 
152 |     /**
153 |      * Helper function for shell initialization
154 |      */
155 |     function openShell(consoleNode, target, frameID) {
156 |       if (consoleNode)
157 |         return consoleNode.slideToggle('fast');
158 |       consoleNode = $('
')
159 |         .appendTo(target.parent())
160 |         .hide()
161 |       var historyPos = 0, history = [''];
162 |       var output = $('
[console ready]
') 163 | .appendTo(consoleNode); 164 | var form = $('
>>>
') 165 | .submit(function() { 166 | var cmd = command.val(); 167 | $.get(window.DEBUG_TOOLBAR_ROOT_PATH + '/execute', { 168 | cmd: cmd, frm: frameID, token:window.DEBUGGER_TOKEN}, function(data) { 169 | var tmp = $('
').html(data); 170 | output.append(tmp); 171 | command.focus(); 172 | consoleNode.scrollTop(command.position().top); 173 | var old = history.pop(); 174 | history.push(cmd); 175 | if (typeof old != 'undefined') 176 | history.push(old); 177 | historyPos = history.length - 1; 178 | }); 179 | command.val(''); 180 | return false; 181 | }). 182 | appendTo(consoleNode); 183 | 184 | var command = $('') 185 | .appendTo(form) 186 | .keydown(function(e) { 187 | if (e.charCode == 100 && e.ctrlKey) { 188 | output.text('--- screen cleared ---'); 189 | return false; 190 | } 191 | else if (e.charCode == 0 && (e.keyCode == 38 || e.keyCode == 40)) { 192 | if (e.keyCode == 38 && historyPos > 0) 193 | historyPos--; 194 | else if (e.keyCode == 40 && historyPos < history.length) 195 | historyPos++; 196 | command.val(history[historyPos]); 197 | return false; 198 | } 199 | }); 200 | 201 | return consoleNode.slideDown('fast', function() { 202 | command.focus(); 203 | }); 204 | } 205 | 206 | /** 207 | * Focus the current block in the source view. 208 | */ 209 | function focusSourceBlock() { 210 | var tmp, line = $('pre code .line-highlight'); 211 | for (var i = 0; i < 7; i++) { 212 | tmp = line.prev(); 213 | if (!(tmp && tmp.is('.in-frame'))) 214 | break; 215 | line = tmp; 216 | } 217 | var container = $('div.sourceview'); 218 | $('html, body').scrollTop(line.offset().top); 219 | } 220 | }); 221 | $.noConflict(true); 222 | }); 223 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.3.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as anonymous module. 11 | define(['jquery'], factory); 12 | } else { 13 | // Browser globals. 14 | factory(jQuery); 15 | } 16 | }(function ($) { 17 | 18 | var pluses = /\+/g; 19 | 20 | function raw(s) { 21 | return s; 22 | } 23 | 24 | function decoded(s) { 25 | return decodeURIComponent(s.replace(pluses, ' ')); 26 | } 27 | 28 | function converted(s) { 29 | if (s.indexOf('"') === 0) { 30 | // This is a quoted cookie as according to RFC2068, unescape 31 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 32 | } 33 | try { 34 | return config.json ? JSON.parse(s) : s; 35 | } catch(er) {} 36 | } 37 | 38 | var config = $.cookie = function (key, value, options) { 39 | 40 | // write 41 | if (value !== undefined) { 42 | options = $.extend({}, config.defaults, options); 43 | 44 | if (typeof options.expires === 'number') { 45 | var days = options.expires, t = options.expires = new Date(); 46 | t.setDate(t.getDate() + days); 47 | } 48 | 49 | value = config.json ? JSON.stringify(value) : String(value); 50 | 51 | return (document.cookie = [ 52 | config.raw ? key : encodeURIComponent(key), 53 | '=', 54 | config.raw ? value : encodeURIComponent(value), 55 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 56 | options.path ? '; path=' + options.path : '', 57 | options.domain ? '; domain=' + options.domain : '', 58 | options.secure ? '; secure' : '' 59 | ].join('')); 60 | } 61 | 62 | // read 63 | var decode = config.raw ? raw : decoded; 64 | var cookies = document.cookie.split('; '); 65 | var result = key ? undefined : {}; 66 | for (var i = 0, l = cookies.length; i < l; i++) { 67 | var parts = cookies[i].split('='); 68 | var name = decode(parts.shift()); 69 | var cookie = decode(parts.join('=')); 70 | 71 | if (key && key === name) { 72 | result = converted(cookie); 73 | break; 74 | } 75 | 76 | if (!key) { 77 | result[name] = converted(cookie); 78 | } 79 | } 80 | 81 | return result; 82 | }; 83 | 84 | config.defaults = {}; 85 | 86 | $.removeCookie = function (key, options) { 87 | if ($.cookie(key) !== undefined) { 88 | // Must not alter options, thus extending a fresh object... 89 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 90 | return true; 91 | } 92 | return false; 93 | }; 94 | 95 | })); 96 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/js/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Tests for the Pyramid Debug Toolbar 6 | 7 | 8 | 9 |
10 | 11 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/static/js/toolbar.js: -------------------------------------------------------------------------------- 1 | var COOKIE_NAME_ACTIVE = 'pdtb_active'; 2 | 3 | 4 | function toggle_content(elem) { 5 | if (elem.is(':visible')) { 6 | elem.hide(); 7 | } else { 8 | elem.show(); 9 | } 10 | } 11 | 12 | function toggle_active(elem) { 13 | elem.toggleClass('active'); 14 | } 15 | 16 | jQuery(document).ready(function($) { 17 | 18 | 19 | // When clicked on the panels menu 20 | $(".pDebugPanels li:not(.disabled) a").click( function(event_) { 21 | event_.stopPropagation(); 22 | $(".pDebugPanels li").removeClass("active"); 23 | parent_ = $(this).parent(); 24 | toggle_active(parent_); 25 | 26 | $(".panelContent").hide(); 27 | $(".pDebugWindow").show(); 28 | current = $('.pDebugWindow #' + parent_.attr('id') + '-content'); 29 | current.show(); 30 | }); 31 | 32 | 33 | $('#settings .switch').click(function() { 34 | var $panel = $(this).parent(); 35 | var $this = $(this); 36 | var dom_id = $this.attr('id').replace("-switch", ""); 37 | // Turn cookie content into an array of active panels 38 | var active_str = $.cookie(COOKIE_NAME_ACTIVE); 39 | var active = (active_str) ? active_str.split(';') : []; 40 | active = $.grep(active, function(n,i) { return n != dom_id; }); 41 | if ($this.hasClass('active')) { 42 | $this.removeClass('active'); 43 | $this.addClass('inactive'); 44 | } 45 | else { 46 | active.push(dom_id); 47 | $this.removeClass('inactive'); 48 | $this.addClass('active'); 49 | } 50 | if (active.length > 0) { 51 | $.cookie(COOKIE_NAME_ACTIVE, active.join(';'), { 52 | path: '/', expires: 10 53 | }); 54 | } 55 | else { 56 | $.cookie(COOKIE_NAME_ACTIVE, null, { 57 | path: '/', expires: -1 58 | }); 59 | } 60 | }); 61 | 62 | // $(".pDebugSortable").tablesorter(); 63 | 64 | bootstrap_panels = ['pDebugVersionPanel', 'pDebugHeaderPanel'] 65 | 66 | for (var i = 0; i < bootstrap_panels.length; i++) { 67 | $('.pDebugWindow #' + bootstrap_panels[i] + '-content').show(); 68 | $('li#' + bootstrap_panels[i]).addClass('active'); 69 | } 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/tbtools/__init__.py: -------------------------------------------------------------------------------- 1 | # TODO: remove somehow 2 | def text_(s, encoding="latin-1", errors="strict"): 3 | if isinstance(s, bytes): 4 | return s.decode(encoding, errors) 5 | return s # pragma: no cover 6 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/tbtools/console.py: -------------------------------------------------------------------------------- 1 | """werkzeug.debug.console 2 | 3 | Interactive console support. 4 | 5 | :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. 6 | :license: BSD. 7 | """ 8 | import code 9 | import sys 10 | import threading 11 | from types import CodeType 12 | 13 | from .repr import debug_repr, dump, helper 14 | from ..utils import escape 15 | 16 | _local = threading.local() 17 | 18 | 19 | class HTMLStringO: 20 | """A StringO version that HTML escapes on write.""" 21 | 22 | def __init__(self): 23 | self._buffer = [] 24 | 25 | def isatty(self): 26 | return False 27 | 28 | def close(self): 29 | pass 30 | 31 | def flush(self): 32 | pass 33 | 34 | def seek(self, n, mode=0): 35 | pass 36 | 37 | def readline(self): 38 | if len(self._buffer) == 0: 39 | return "" 40 | ret = self._buffer[0] 41 | del self._buffer[0] 42 | return ret 43 | 44 | def reset(self): 45 | val = "".join(self._buffer) 46 | del self._buffer[:] 47 | return val 48 | 49 | def _write(self, x): 50 | if isinstance(x, bytes): 51 | x = str(x, encoding="utf-8", errors="replace") 52 | self._buffer.append(x) 53 | 54 | def write(self, x): 55 | self._write(escape(x)) 56 | 57 | def writelines(self, x): 58 | self._write(escape("".join(x))) 59 | 60 | 61 | class ThreadedStream: 62 | """Thread-local wrapper for sys.stdout for the interactive console.""" 63 | 64 | @staticmethod 65 | def push(): 66 | if not isinstance(sys.stdout, ThreadedStream): 67 | sys.stdout = ThreadedStream() 68 | _local.stream = HTMLStringO() 69 | 70 | @staticmethod 71 | def fetch(): 72 | try: 73 | stream = _local.stream 74 | except AttributeError: 75 | return "" 76 | return stream.reset() 77 | 78 | @staticmethod 79 | def displayhook(obj): 80 | try: 81 | stream = _local.stream 82 | except AttributeError: 83 | return _displayhook(obj) 84 | # stream._write bypasses escaping as debug_repr is 85 | # already generating HTML for us. 86 | if obj is not None: 87 | _local._current_ipy.locals["_"] = obj 88 | stream._write(debug_repr(obj)) 89 | 90 | def __setattr__(self, name, value): 91 | raise AttributeError("read only attribute %s" % name) 92 | 93 | def __dir__(self): 94 | return dir(sys.__stdout__) 95 | 96 | def __getattribute__(self, name): 97 | if name == "__members__": 98 | return dir(sys.__stdout__) 99 | try: 100 | stream = _local.stream 101 | except AttributeError: 102 | stream = sys.__stdout__ 103 | return getattr(stream, name) 104 | 105 | def __repr__(self): 106 | return repr(sys.__stdout__) 107 | 108 | 109 | # add the threaded stream as display hook 110 | _displayhook = sys.displayhook 111 | sys.displayhook = ThreadedStream.displayhook 112 | 113 | 114 | class _ConsoleLoader: 115 | def __init__(self): 116 | self._storage = {} 117 | 118 | def register(self, code, source): 119 | self._storage[id(code)] = source 120 | # register code objects of wrapped functions too. 121 | for var in code.co_consts: 122 | if isinstance(var, CodeType): 123 | self._storage[id(var)] = source 124 | 125 | def get_source_by_code(self, code): 126 | try: 127 | return self._storage[id(code)] 128 | except KeyError: 129 | pass 130 | 131 | 132 | def _wrap_compiler(console): 133 | compile = console.compile 134 | 135 | def func(source, filename, symbol): 136 | code = compile(source, filename, symbol) 137 | console.loader.register(code, source) 138 | return code 139 | 140 | console.compile = func 141 | 142 | 143 | class _InteractiveConsole(code.InteractiveInterpreter): 144 | def __init__(self, app, globals, locals): 145 | self._app = app 146 | code.InteractiveInterpreter.__init__(self, locals) 147 | self.globals = dict(globals) 148 | self.globals["dump"] = dump 149 | self.globals["help"] = helper 150 | self.globals["__loader__"] = self.loader = _ConsoleLoader() 151 | self.more = False 152 | self.buffer = [] 153 | _wrap_compiler(self) 154 | 155 | def runsource(self, source): 156 | source = source.rstrip() + "\n" 157 | ThreadedStream.push() 158 | prompt = self.more and "... " or ">>> " 159 | try: 160 | source_to_eval = "".join(self.buffer + [source]) 161 | if code.InteractiveInterpreter.runsource( 162 | self, source_to_eval, "", "single" 163 | ): 164 | self.more = True 165 | self.buffer.append(source) 166 | else: 167 | self.more = False 168 | del self.buffer[:] 169 | finally: 170 | output = ThreadedStream.fetch() 171 | return prompt + source + output 172 | 173 | def runcode(self, code): 174 | try: 175 | exec(code, self.globals, self.locals) # noqa: S102 176 | except Exception as exc: 177 | self.showtraceback(exc) 178 | 179 | def showtraceback(self, exc): 180 | from .tbtools import get_current_traceback 181 | 182 | tb = get_current_traceback(skip=1, exc=exc, app=self._app) 183 | sys.stdout._write(tb.render_summary(self._app)) 184 | 185 | def showsyntaxerror(self, filename=None): 186 | from .tbtools import get_current_traceback 187 | 188 | exc = SyntaxError(filename) 189 | tb = get_current_traceback(skip=6, exc=exc, app=self._app) 190 | sys.stdout._write(tb.render_summary(self._app)) 191 | 192 | def write(self, data): 193 | sys.stdout.write(data) 194 | 195 | 196 | class Console: 197 | """An interactive console.""" 198 | 199 | def __init__(self, app, globals=None, locals=None): 200 | self._app = app 201 | if locals is None: 202 | locals = {} 203 | if globals is None: 204 | globals = {} 205 | self._ipy = _InteractiveConsole(app, globals, locals) 206 | 207 | def eval(self, code): 208 | _local._current_ipy = self._ipy 209 | old_sys_stdout = sys.stdout 210 | try: 211 | return self._ipy.runsource(code) 212 | finally: 213 | sys.stdout = old_sys_stdout 214 | 215 | 216 | class _ConsoleFrame: 217 | """Helper class so that we can reuse the frame console code for the 218 | standalone console. 219 | """ 220 | 221 | def __init__(self, namespace, app): 222 | self.console = Console(namespace, app) 223 | self.id = 0 224 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/console.jinja2: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | {{ title }} // Werkzeug Debugger 6 | 7 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |

Interactive Console

20 |
21 | 22 | In this console you can execute Python expressions in the context of 23 | the application. The initial namespace was created by the debugger 24 | automatically. 25 | 26 |
27 |
28 |
The Console requires JavaScript.
29 |
30 | 31 | 35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/exception.jinja2: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ title }} // Werkzeug Debugger 7 | 8 | 9 | 10 | 12 | {# #} 13 | 14 | 22 | 24 | 25 | 26 |
27 | 28 |

{{ exception_type }}

29 |
30 |
{{ exception }}
31 |
32 |

Traceback (most recent call last)

33 | {{ summary|safe }} 34 |
35 |

36 | 37 | This is the Copy/Paste friendly version of the traceback. 38 |

39 | 41 |
42 | 43 |
44 |
{{ exception }}
45 |
46 | 47 |
48 |

49 | Warning: this feature should not be enabled on production 50 | systems. 51 |

52 | 53 | {% if evalex %} 54 |

55 | 56 | Hover over any gray area in the traceback and click on the 57 | 60 | button on the right hand side of that gray area to show an interactive 61 | console for the associated frame. Type arbitrary Python into the 62 | console; it will be evaluated in the context of the associated frame. In 63 | the interactive console there are helpers available for introspection: 64 | 65 |

    66 |
  • dump() shows all variables in the frame 67 |
  • dump(obj) dumps all that's known about the object 68 |
69 |

70 | {% endif %} 71 | 72 |

73 | Hover over any gray area in the traceback and click on 74 | 77 | on the right hand side of that gray area to show the source of the file 78 | associated with the frame. 79 |

80 | 81 |

82 | Click on the traceback header to switch back and forth between the 83 | rendered version of the traceback and a plaintext copy-paste-friendly 84 | version of the traceback. 85 |

86 | 87 |

88 | URL to recover this traceback page: {{ url }} 89 |

90 |
91 | 92 | 96 |
97 | 102 | 103 | {# #} 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/exception_summary.jinja2: -------------------------------------------------------------------------------- 1 |
2 | {{ title }} 3 |
    {{ frames|safe }}
4 | {{ description|safe }} 5 |
6 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/global_tab.jinja2: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 37 |
38 |
39 |
40 |
41 | {% for panel in global_panels %} 42 | {% if panel.has_content %} 43 | 53 | {% endif %} 54 | {% endfor %} 55 | 56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/history_tab.jinja2: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 |
10 | 28 |
29 |
30 |
31 | {% for panel in panels %} 32 | {% if panel.has_content %} 33 | 43 | {% endif %} 44 | {% endfor %} 45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/redirect.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirect intercepted 4 | 5 | 6 |

Redirect ({{ redirect_code }})

7 |

Location: {{ redirect_to }}

8 |

9 | 10 | The Debug Toolbar has intercepted a redirect to the above URL for 11 | debug viewing purposes. You can click the above link to continue 12 | with the redirect as normal. If you'd like to disable this 13 | feature, you can set the settings 14 | variable debugtoolbar.intercept_redirects 15 | to False. 16 | 17 |

18 | 19 | 20 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/settings_tab.jinja2: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

Settings

6 | 7 |

Some aiohttp debug toolbar panels can be activated (this enables additional features)

8 | 9 | {% for panel in panels %} 10 | {% if panel.user_activate %} 11 |

12 | {{ panel.name }} 13 |

14 | Enable: 17 | {% endif %} 18 | {% endfor %} 19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/templates/toolbar.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Aiohttp Debug Toolbar 6 | 7 | 8 | 9 | 10 | 11 | {# #} 12 | 13 | 14 | 15 | {# include scripts here that should be included before pageload #} 16 | {# this *should* only be jquery, as we only need the `$` variable defined #} 17 | {# in order for other javascript to be run after the document is loaded #} 18 | 19 | 22 | 23 | 24 | 25 | 26 | 48 | 49 |
50 |
51 |
52 |
53 |
54 | {% include "history_tab.jinja2" %} 55 |
56 | 57 |
58 | {% include "global_tab.jinja2" %} 59 |
60 | 61 |
62 | {% include "settings_tab.jinja2" %} 63 |
64 |
65 |
66 |
67 |
68 | 69 | {# scripts that can be included after pageload #} 70 | 71 | 72 | 73 | 74 | 131 | {# #} 132 | {# #} 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/toolbar.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import unquote as url_unquote 2 | 3 | from aiohttp.web import Response 4 | 5 | from .utils import APP_KEY, STATIC_ROUTE_NAME, replace_insensitive 6 | 7 | __all__ = ["DebugToolbar"] 8 | 9 | 10 | class DebugToolbar: 11 | def __init__(self, request, panel_classes, global_panel_classes): 12 | self.panels = [] 13 | self.global_panels = [] 14 | self.request = request 15 | self.status = 200 16 | 17 | # Panels can be be activated (more features) (e.g. Performace panel) 18 | pdtb_active = url_unquote(request.cookies.get("pdtb_active", "")) 19 | 20 | activated = pdtb_active.split(";") 21 | # XXX 22 | for panel_class in panel_classes: 23 | panel_inst = panel_class(request) 24 | if panel_inst.dom_id in activated and panel_inst.has_content: 25 | panel_inst.is_active = True 26 | self.panels.append(panel_inst) 27 | 28 | for panel_class in global_panel_classes: 29 | panel_inst = panel_class(request) 30 | if panel_inst.dom_id in activated and panel_inst.has_content: 31 | panel_inst.is_active = True 32 | self.global_panels.append(panel_inst) 33 | 34 | @property 35 | def json(self): 36 | return { 37 | "method": self.request.method, 38 | "path": self.request.path, 39 | "scheme": "http", 40 | "status_code": self.status, 41 | } 42 | 43 | async def process_response(self, request, response): 44 | # if isinstance(response, WSGIHTTPException): 45 | # the body of a WSGIHTTPException needs to be "prepared" 46 | # response.prepare(request.environ) 47 | for panel in self.panels: 48 | await panel.process_response(response) 49 | for panel in self.global_panels: 50 | await panel.process_response(response) 51 | 52 | def inject(self, request, response): 53 | """ 54 | Inject the debug toolbar iframe into an HTML response. 55 | """ 56 | # called in host app 57 | if not isinstance(response, Response): 58 | return 59 | settings = request.app[APP_KEY]["settings"] 60 | response_html = response.body 61 | route = request.app.router["debugtoolbar.request"] 62 | toolbar_url = route.url_for(request_id=request["id"]) 63 | 64 | button_style = settings["button_style"] 65 | 66 | css_path = request.app.router[STATIC_ROUTE_NAME].url_for( 67 | filename="css/toolbar_button.css" 68 | ) 69 | 70 | toolbar_css = toolbar_css_template % {"css_path": css_path} 71 | toolbar_html = toolbar_html_template % { 72 | "button_style": button_style, 73 | "css_path": css_path, 74 | "toolbar_url": toolbar_url, 75 | } 76 | 77 | toolbar_html = toolbar_html.encode(response.charset or "utf-8") 78 | toolbar_css = toolbar_css.encode(response.charset or "utf-8") 79 | response_html = replace_insensitive( 80 | response_html, b"", toolbar_css + b"" 81 | ) 82 | response.body = replace_insensitive( 83 | response_html, b"", toolbar_html + b"" 84 | ) 85 | 86 | 87 | toolbar_css_template = """\ 88 | """ 89 | 90 | toolbar_html_template = """\ 91 |
92 | 97 |
98 | """ 99 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/utils.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import ipaddress 3 | import os 4 | import sys 5 | from collections import deque 6 | from itertools import islice 7 | from typing import Literal, Sequence, TYPE_CHECKING, Tuple, Type, TypedDict 8 | 9 | import jinja2 10 | from aiohttp.web import AppKey 11 | 12 | if TYPE_CHECKING: # pragma: no cover 13 | from .panels.base import DebugPanel 14 | else: 15 | DebugPanel = None 16 | 17 | REDIRECT_CODES = (300, 301, 302, 303, 305, 307, 308) 18 | STATIC_PATH = "static/" 19 | ROOT_ROUTE_NAME = "debugtoolbar.main" 20 | STATIC_ROUTE_NAME = "debugtoolbar.static" 21 | EXC_ROUTE_NAME = "debugtoolbar.exception" 22 | 23 | 24 | def hexlify(value): 25 | # value must be int or bytes 26 | if isinstance(value, int): 27 | value = bytes(str(value), encoding="utf-8") 28 | return str(binascii.hexlify(value), encoding="utf-8") 29 | 30 | 31 | # TODO: refactor to simpler container or change to ordered dict 32 | class ToolbarStorage(deque): 33 | """Deque for storing Toolbar objects.""" 34 | 35 | def __init__(self, max_elem): 36 | super().__init__([], max_elem) 37 | 38 | def get(self, request_id, default=None): 39 | dict_ = dict(self) 40 | return dict_.get(request_id, default) 41 | 42 | def put(self, request_id, request): 43 | self.appendleft((request_id, request)) 44 | 45 | def last(self, num_items): 46 | """Returns the last `num_items` Toolbar objects""" 47 | return list(islice(self, 0, num_items)) 48 | 49 | 50 | class ExceptionHistory: 51 | def __init__(self): 52 | self.frames = {} 53 | self.tracebacks = {} 54 | self.eval_exc = "show" 55 | 56 | 57 | class _Config(TypedDict): 58 | enabled: bool 59 | intercept_exc: Literal["debug", "display", False] 60 | intercept_redirects: bool 61 | panels: Tuple[Type[DebugPanel], ...] 62 | extra_panels: Tuple[Type[DebugPanel], ...] 63 | global_panels: Tuple[Type[DebugPanel], ...] 64 | hosts: Sequence[str] 65 | exclude_prefixes: Tuple[str, ...] 66 | check_host: bool 67 | button_style: str 68 | max_visible_requests: int 69 | path_prefix: str 70 | 71 | 72 | class AppState(TypedDict): 73 | exc_history: ExceptionHistory 74 | pdtb_token: str 75 | request_history: ToolbarStorage 76 | settings: _Config 77 | 78 | 79 | APP_KEY = AppKey("APP_KEY", AppState) 80 | TEMPLATE_KEY = AppKey("TEMPLATE_KEY", jinja2.Environment) 81 | 82 | 83 | def addr_in(addr, hosts): 84 | for host in hosts: 85 | if ipaddress.ip_address(addr) in ipaddress.ip_network(host): 86 | return True 87 | return False 88 | 89 | 90 | def replace_insensitive(string, target, replacement): 91 | """Similar to string.replace() but is case insensitive 92 | Code borrowed from: http://forums.devshed.com/python-programming-11/ 93 | case-insensitive-string-replace-490921.html 94 | """ 95 | no_case = string.lower() 96 | index = no_case.rfind(target.lower()) 97 | if index >= 0: 98 | start = index + len(target) 99 | return string[:index] + replacement + string[start:] 100 | else: # no results so return the original string 101 | return string 102 | 103 | 104 | def render(template_name, app, context, *, app_key=TEMPLATE_KEY, **kw): 105 | lookup = app[app_key] 106 | template = lookup.get_template(template_name) 107 | c = context.copy() 108 | c.update(kw) 109 | txt = template.render(**c) 110 | return txt 111 | 112 | 113 | def common_segment_count(path, value): 114 | """Return the number of path segments common to both""" 115 | i = 0 116 | if len(path) <= len(value): 117 | for x1, x2 in zip(path, value): 118 | if x1 == x2: 119 | i += 1 120 | else: 121 | return 0 122 | return i 123 | 124 | 125 | def format_fname(value, _sys_path=None): 126 | if _sys_path is None: 127 | _sys_path = sys.path # dependency injection 128 | # If the value is not an absolute path, the it is a builtin or 129 | # a relative file (thus a project file). 130 | if not os.path.isabs(value): 131 | if value.startswith(("{", "<")): 132 | return value 133 | if value.startswith("." + os.path.sep): 134 | return value 135 | return "." + os.path.sep + value 136 | 137 | # Loop through sys.path to find the longest match and return 138 | # the relative path from there. 139 | prefix_len = 0 140 | value_segs = value.split(os.path.sep) 141 | for path in _sys_path: 142 | count = common_segment_count(path.split(os.path.sep), value_segs) 143 | if count > prefix_len: 144 | prefix_len = count 145 | return "<%s>" % os.path.sep.join(value_segs[prefix_len:]) 146 | 147 | 148 | def escape(s, quote=False): 149 | """Replace special characters "&", "<" and ">" to HTML-safe sequences. If 150 | the optional flag `quote` is `True`, the quotation mark character is 151 | also translated. 152 | 153 | There is a special handling for `None` which escapes to an empty string. 154 | 155 | :param s: the string to escape. 156 | :param quote: set to true to also escape double quotes. 157 | """ 158 | if s is None: 159 | return "" 160 | 161 | if not isinstance(s, (str, bytes)): 162 | s = str(s) 163 | if isinstance(s, bytes): 164 | try: 165 | s.decode("ascii") 166 | except UnicodeDecodeError: 167 | s = s.decode("utf-8", "replace") 168 | s = s.replace("&", "&").replace("<", "<").replace(">", ">") 169 | if quote: 170 | s = s.replace('"', """) 171 | return s 172 | 173 | 174 | class ContextSwitcher: 175 | """This object is alternative to *await*. It is useful in cases 176 | when you need to track context switches inside coroutine. 177 | 178 | see: https://www.python.org/dev/peps/pep-0380/#formal-semantics 179 | """ 180 | 181 | def __init__(self): 182 | self._on_context_switch_out = [] 183 | self._on_context_switch_in = [] 184 | 185 | def add_context_in(self, callback): 186 | if not callable(callback): 187 | raise ValueError("callback should be callable") 188 | self._on_context_switch_in.append(callback) 189 | 190 | def add_context_out(self, callback): 191 | if not callable(callback): 192 | raise ValueError("callback should be callable") 193 | self._on_context_switch_out.append(callback) 194 | 195 | def __call__(self, expr): 196 | def iterate(): 197 | for callbale in self._on_context_switch_in: 198 | callbale() 199 | 200 | _i = iter(expr.__await__()) 201 | try: 202 | _y = next(_i) 203 | except StopIteration as _e: 204 | _r = _e.value 205 | else: 206 | while 1: 207 | try: 208 | for callbale in self._on_context_switch_out: 209 | callbale() 210 | _s = yield _y 211 | for callbale in self._on_context_switch_in: 212 | callbale() 213 | except GeneratorExit as _e: 214 | try: 215 | _m = _i.close 216 | except AttributeError: 217 | pass 218 | else: 219 | _m() 220 | raise _e 221 | except BaseException as _e: 222 | _x = sys.exc_info() 223 | try: 224 | _m = _i.throw 225 | except AttributeError: 226 | raise _e 227 | else: 228 | try: 229 | _y = _m(*_x) 230 | except StopIteration as _e: 231 | _r = _e.value 232 | break 233 | else: 234 | try: 235 | if _s is None: 236 | _y = next(_i) 237 | else: 238 | _y = _i.send(_s) 239 | except StopIteration as _e: 240 | _r = _e.value 241 | break 242 | result = _r 243 | for callbale in self._on_context_switch_out: 244 | callbale() 245 | return result 246 | 247 | return _Coro(iterate()) 248 | 249 | 250 | class _Coro: 251 | __slots__ = ("_it",) 252 | 253 | def __init__(self, it): 254 | self._it = it 255 | 256 | def __await__(self): 257 | return self._it 258 | -------------------------------------------------------------------------------- /aiohttp_debugtoolbar/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import aiohttp_jinja2 4 | from aiohttp import web 5 | 6 | # from .tbtools.console import _ConsoleFrame 7 | from .utils import APP_KEY, ROOT_ROUTE_NAME, STATIC_ROUTE_NAME, TEMPLATE_KEY 8 | 9 | 10 | @aiohttp_jinja2.template("toolbar.jinja2", app_key=TEMPLATE_KEY) 11 | async def request_view(request): 12 | settings = request.app[APP_KEY]["settings"] 13 | history = request.app[APP_KEY]["request_history"] 14 | 15 | try: 16 | last_request_pair = history.last(1)[0] 17 | except IndexError: 18 | last_request_id = None 19 | else: 20 | last_request_id = last_request_pair[0] 21 | 22 | request_id = request.match_info.get("request_id", last_request_id) 23 | 24 | toolbar = history.get(request_id, None) 25 | 26 | panels = toolbar.panels if toolbar else [] 27 | global_panels = toolbar.global_panels if toolbar else [] 28 | 29 | static_path = request.app.router[STATIC_ROUTE_NAME].canonical 30 | root_path = request.app.router[ROOT_ROUTE_NAME].url_for() 31 | 32 | button_style = settings.get("button_style", "") 33 | max_visible_requests = settings["max_visible_requests"] 34 | 35 | hist_toolbars = history.last(max_visible_requests) 36 | 37 | return { 38 | "panels": panels, 39 | "static_path": static_path, 40 | "root_path": root_path, 41 | "button_style": button_style, 42 | "history": hist_toolbars, 43 | "global_panels": global_panels, 44 | "request_id": request_id, 45 | "request": toolbar.request if toolbar else None, 46 | } 47 | 48 | 49 | class ExceptionDebugView: 50 | def _validate_token(self, request): 51 | exc_history = self._exception_history(request) 52 | token = request.query.get("token") 53 | 54 | if exc_history is None: 55 | raise web.HTTPBadRequest(text="No exception history") 56 | if not token: 57 | raise web.HTTPBadRequest(text="No token in request") 58 | if not (token == request.app[APP_KEY]["pdtb_token"]): 59 | raise web.HTTPBadRequest(text="Bad token in request") 60 | 61 | def _exception_history(self, request): 62 | return request.app[APP_KEY]["exc_history"] 63 | 64 | def _get_frame(self, request): 65 | frm = request.query.get("frm") 66 | if frm is not None: 67 | frm = int(frm) 68 | return frm 69 | 70 | async def _get_tb(self, request): 71 | await request.read() 72 | tb = request.query.get("tb") 73 | if not tb: 74 | await request.post() 75 | tb = request.POST.get("tb") 76 | if tb is not None: 77 | tb = int(tb) 78 | return tb 79 | 80 | async def _get_cmd(self, request): 81 | await request.read() 82 | cmd = request.query.get("cmd") 83 | if not cmd: 84 | await request.post() 85 | cmd = request.POST.get("cmd") 86 | return cmd 87 | 88 | async def exception(self, request): 89 | self._validate_token(request) 90 | tb_id = await self._get_tb(request) 91 | tb = self._exception_history(request).tracebacks[tb_id] 92 | body = tb.render_full(request).encode("utf-8", "replace") 93 | response = web.Response(status=200) 94 | response.body = body 95 | return response 96 | 97 | async def source(self, request): 98 | self._validate_token(request) 99 | exc_history = self._exception_history(request) 100 | _frame = self._get_frame(request) 101 | if _frame is not None: 102 | frame = exc_history.frames.get(_frame) 103 | if frame is not None: 104 | # text = frame.render_source() 105 | in_frame = frame.get_in_frame_range() 106 | text = json.dumps( 107 | { 108 | "line": frame.lineno, 109 | "inFrame": in_frame, 110 | "source": "\n".join(frame.sourcelines), 111 | } 112 | ) 113 | return web.Response(text=text, content_type="application/json") 114 | raise web.HTTPBadRequest() 115 | 116 | async def execute(self, request): 117 | self._validate_token(request) 118 | 119 | _exc_history = self._exception_history(request) 120 | if _exc_history.eval_exc: 121 | exc_history = _exc_history 122 | cmd = await self._get_cmd(request) 123 | frame = self._get_frame(request) 124 | if frame is not None and cmd is not None: 125 | frame = exc_history.frames.get(frame) 126 | if frame is not None: 127 | result = frame.console.eval(cmd) 128 | return web.Response(text=result, content_type="text/html") 129 | raise web.HTTPBadRequest() 130 | 131 | # TODO: figure out how to enable console mode on frontend 132 | # @aiohttp_jinja2.template('console.jinja2', app_key=TEMPLATE_KEY) 133 | # async def console(self, request): 134 | # self._validate_token(request) 135 | # static_path = request.app.router[STATIC_ROUTE_NAME].canonical 136 | # root_path = request.app.router[ROOT_ROUTE_NAME].url() 137 | # token = request.query.get('token') 138 | # tb = await self._get_tb(request) 139 | # 140 | # _exc_history = self._exception_history(request) 141 | # vars = { 142 | # 'evalex': _exc_history.eval_exc and 'true' or 'false', 143 | # 'console': 'true', 144 | # 'title': 'Console', 145 | # 'traceback_id': tb or -1, 146 | # 'root_path': root_path, 147 | # 'static_path': static_path, 148 | # 'token': token, 149 | # } 150 | # if 0 not in _exc_history.frames: 151 | # _exc_history.frames[0] = _ConsoleFrame({}) 152 | # return vars 153 | 154 | 155 | U_SSE_PAYLOAD = "id: {0}\nevent: new_request\ndata: {1}\n\n" 156 | 157 | 158 | async def sse(request): 159 | response = web.Response(status=200) 160 | response.content_type = "text/event-stream" 161 | history = request.app[APP_KEY]["request_history"] 162 | response.text = "" 163 | 164 | active_request_id = str(request.match_info.get("request_id")) 165 | client_last_request_id = str(request.headers.get("Last-Event-Id", 0)) 166 | 167 | settings = request.app[APP_KEY]["settings"] 168 | max_visible_requests = settings["max_visible_requests"] 169 | 170 | if history: 171 | last_request_pair = history.last(1)[0] 172 | last_request_id = last_request_pair[0] 173 | if not last_request_id == client_last_request_id: 174 | data = [] 175 | for _id, toolbar in history.last(max_visible_requests): 176 | req_type = "active" if active_request_id == _id else "" 177 | data.append([_id, toolbar.json, req_type]) 178 | 179 | if data: 180 | response.text = U_SSE_PAYLOAD.format(last_request_id, json.dumps(data)) 181 | return response 182 | -------------------------------------------------------------------------------- /demo/README.rst: -------------------------------------------------------------------------------- 1 | Play With Demo 2 | -------------- 3 | 4 | 5 | 1) clone repository:: 6 | 7 | $ git clone git@github.com:aio-libs/aiohttp_debugtoolbar.git 8 | 9 | 2) create virtual environment, for instance using *virtualenvwraper*:: 10 | 11 | $ cd aiohttp_debugtoolbar 12 | $ mkvirtualenv -p `which python3` aiohttp_debugtoolbar 13 | 14 | 3) install ``aiohttp_jinja2``:: 15 | 16 | $ pip install aiohttp_jinja2 17 | 18 | 4) install `aiohttp_debugtoolbar` and other dependencies:: 19 | 20 | $ pip install -e . 21 | 22 | 5) run demo.py:: 23 | 24 | $ python3 demo/demo.py 25 | 26 | Now you can play with `aiohttp_debugtoolbar` on http://127.0.0.1:9000 27 | -------------------------------------------------------------------------------- /demo/aiohttp_debugtoolba_sceenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aio-libs/aiohttp-debugtoolbar/f83b8438c6f3ea48a7de54f3fc88c97914a62c2e/demo/aiohttp_debugtoolba_sceenshot.png -------------------------------------------------------------------------------- /demo/demo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import aiohttp_jinja2 5 | import jinja2 6 | from aiohttp import web 7 | 8 | import aiohttp_debugtoolbar 9 | 10 | PROJECT_ROOT = Path(__file__).parent 11 | TEMPLATE_DIR = PROJECT_ROOT / "templates" 12 | 13 | 14 | @aiohttp_jinja2.template("index.jinja2") 15 | async def index(request): 16 | log.info("Info logger for index page") 17 | log.debug("Debug logger for index page") 18 | log.critical("Critical logger for index page") 19 | 20 | return {"title": "Aiohttp Debugtoolbar"} 21 | 22 | 23 | async def exception(request): 24 | log.error("NotImplementedError exception handler") 25 | raise NotImplementedError 26 | 27 | 28 | @aiohttp_jinja2.template("ajax.jinja2") 29 | async def ajax(request): 30 | if request.method == "POST": 31 | log.info("Ajax POST request received") 32 | return web.json_response({"ajax": "success"}) 33 | 34 | 35 | async def redirect(request): 36 | log.info("redirect handler") 37 | raise web.HTTPSeeOther(location="/") 38 | 39 | 40 | @aiohttp_jinja2.template("error.jinja2") 41 | async def jinja2_exception(request): 42 | return {"title": "Test jinja2 template exceptions"} 43 | 44 | 45 | async def init(): 46 | PROJECT_ROOT = Path(__file__).parent 47 | 48 | app = web.Application() 49 | aiohttp_debugtoolbar.setup(app, intercept_exc="debug") 50 | 51 | loader = jinja2.FileSystemLoader([str(TEMPLATE_DIR)]) 52 | aiohttp_jinja2.setup(app, loader=loader) 53 | 54 | routes = [ 55 | web.get("/", index, name="index"), 56 | web.get("/redirect", redirect, name="redirect"), 57 | web.get("/exception", exception, name="exception"), 58 | web.get("/jinja2_exc", jinja2_exception, name="jinja2_exception"), 59 | web.get("/ajax", ajax, name="ajax"), 60 | web.post("/ajax", ajax, name="ajax"), 61 | web.static("/static", PROJECT_ROOT / "static"), 62 | ] 63 | 64 | app.add_routes(routes) 65 | return app 66 | 67 | 68 | if __name__ == "__main__": 69 | log = logging.getLogger(__file__) 70 | logging.basicConfig(level=logging.DEBUG) 71 | 72 | web.run_app(init(), host="127.0.0.1", port=9000) 73 | -------------------------------------------------------------------------------- /demo/static/main.js: -------------------------------------------------------------------------------- 1 | 2 | require.config({ 3 | paths: { 4 | "jquery": "jquery-1.7.2.min", 5 | } 6 | }); 7 | 8 | require(["jquery"], function($) { 9 | $(function() { 10 | console.log('TEST REQUIREJS'); 11 | }); 12 | }); 13 | 14 | define("main", function(){}); 15 | -------------------------------------------------------------------------------- /demo/templates/ajax.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AJAX Test 5 | 6 | 7 | 8 | 9 |

AJAX Test

10 | This page makes an AJAX request every 5 seconds.

11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/templates/error.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | {{ 1/0 }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/templates/index.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 |

{{ title }}

9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/extra_panels/extra_pgsql.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import time 4 | 5 | from aiopg.cursor import Cursor 6 | 7 | from aiohttp_debugtoolbar.panels.base import DebugPanel 8 | 9 | __all__ = ["RequestPgDebugPanel"] 10 | 11 | 12 | class RequestHandler: 13 | def __init__(self): 14 | self._queries = [] 15 | self._total_time = 0 16 | # save original 17 | self._tmp_execute = Cursor.execute 18 | 19 | @property 20 | def queries(self): 21 | return self._queries 22 | 23 | @property 24 | def total_time(self): 25 | return self._total_time 26 | 27 | def _wrapper(self, func): 28 | @functools.wraps(func) 29 | async def wrapped(*args, **kwargs): 30 | start = time.time() 31 | 32 | context = await func(*args, **kwargs) 33 | 34 | called_from = [] 35 | for stack in inspect.stack()[1:]: 36 | called_from.append( 37 | "/{}:{}".format("/".join(stack[1].split("/")[-3:]), stack[2]) 38 | ) 39 | if len(called_from) >= 2: 40 | break 41 | 42 | elapsed = time.time() - start 43 | arg = { 44 | "query": args[1] 45 | .strip() 46 | .replace("\n", "
") 47 | .replace("\t", "  ") 48 | .replace(" ", "  "), 49 | "params": args[2] if len(args) > 2 else [], 50 | "other": dict(kwargs), 51 | "elapsed": "%0.3f sec" % elapsed, 52 | "called_from": "
".join(reversed(called_from)), 53 | } 54 | self._queries.append(arg) 55 | self._total_time += elapsed 56 | 57 | return context 58 | 59 | return wrapped 60 | 61 | def on(self): 62 | Cursor.execute = self._wrapper(Cursor.execute) 63 | 64 | def off(self): 65 | Cursor.execute = self._tmp_execute 66 | 67 | 68 | class RequestPgDebugPanel(DebugPanel): 69 | """ 70 | A panel to display SQL queries. 71 | """ 72 | 73 | name = "PgSQL" 74 | template = "request_pgsql.jinja2" 75 | title = "PgSQL Queries" 76 | nav_title = title 77 | 78 | def __init__(self, request): 79 | super().__init__(request) 80 | self._handler = RequestHandler() 81 | 82 | @property 83 | def has_content(self): 84 | if self.data.get("queries"): 85 | return True 86 | return False 87 | 88 | async def process_response(self, response): 89 | self.data = data = {} 90 | data.update( 91 | { 92 | "timing_rows": { 93 | "Total time": "%0.3f sec" % self._handler.total_time, 94 | "Total": len(self._handler.queries), 95 | }.items(), 96 | "queries": [(k, v) for k, v in enumerate(self._handler.queries)], 97 | } 98 | ) 99 | 100 | def _install_handler(self): 101 | self._handler.on() 102 | 103 | def _uninstall_handler(self): 104 | self._handler.off() 105 | 106 | def wrap_handler(self, handler, context_switcher): 107 | context_switcher.add_context_in(self._install_handler) 108 | context_switcher.add_context_out(self._uninstall_handler) 109 | return handler 110 | -------------------------------------------------------------------------------- /examples/extra_panels/extra_redis.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import time 4 | 5 | from aioredis import Redis 6 | 7 | from aiohttp_debugtoolbar.panels.base import DebugPanel 8 | 9 | __all__ = ["RequestRedisDebugPanel"] 10 | 11 | 12 | class RequestHandler: 13 | def __init__(self): 14 | self._queries = [] 15 | self._total_time = 0 16 | # save original 17 | self._tmp_execute = Redis.execute_command 18 | 19 | @property 20 | def queries(self): 21 | return self._queries 22 | 23 | @property 24 | def total_time(self): 25 | return self._total_time 26 | 27 | def _wrapper(self, func): 28 | @functools.wraps(func) 29 | async def wrapped(*args, **kwargs): 30 | start = time.time() 31 | 32 | context = await func(*args, **kwargs) 33 | 34 | called_from = [] 35 | for stack in inspect.stack()[1:]: 36 | called_from.append( 37 | "/{}:{}".format("/".join(stack[1].split("/")[-3:]), stack[2]) 38 | ) 39 | if len(called_from) >= 2: 40 | break 41 | 42 | elapsed = time.time() - start 43 | arg = { 44 | "command": ( 45 | args[1].decode("UTF-8").strip() 46 | if isinstance(args[1], bytes) 47 | else args[1] 48 | ), 49 | "return": bool(context), 50 | "key": args[2].strip(), 51 | "params": {**{"args": args[4:]}, **dict(kwargs)}, 52 | "elapsed": "%0.3f sec" % elapsed, 53 | "called_from": "
".join(reversed(called_from)), 54 | } 55 | self._queries.append(arg) 56 | self._total_time += elapsed 57 | 58 | return context 59 | 60 | return wrapped 61 | 62 | def on(self): 63 | Redis.execute_command = self._wrapper(Redis.execute_command) 64 | 65 | def off(self): 66 | Redis.execute_command = self._tmp_execute 67 | 68 | 69 | class RequestRedisDebugPanel(DebugPanel): 70 | """ 71 | A panel to display cache requests. 72 | """ 73 | 74 | name = "Redis" 75 | template = "request_redis.jinja2" 76 | title = "Redis" 77 | nav_title = title 78 | 79 | def __init__(self, request): 80 | super().__init__(request) 81 | self._handler = RequestHandler() 82 | 83 | @property 84 | def has_content(self): 85 | if self.data.get("queries"): 86 | return True 87 | return False 88 | 89 | async def process_response(self, response): 90 | self.data = data = {} 91 | data.update( 92 | { 93 | "timing_rows": { 94 | "Total time": "%0.3f sec" % self._handler.total_time, 95 | "Total": len(self._handler.queries), 96 | }.items(), 97 | "queries": [(k, v) for k, v in enumerate(self._handler.queries)], 98 | } 99 | ) 100 | 101 | def _install_handler(self): 102 | self._handler.on() 103 | 104 | def _uninstall_handler(self): 105 | self._handler.off() 106 | 107 | def wrap_handler(self, handler, context_switcher): 108 | context_switcher.add_context_in(self._install_handler) 109 | context_switcher.add_context_out(self._uninstall_handler) 110 | return handler 111 | -------------------------------------------------------------------------------- /examples/extra_panels/extra_tpl/request_pgsql.jinja2: -------------------------------------------------------------------------------- 1 | {% if timing_rows %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for key, value in timing_rows %} 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 | 21 |
ResourceValue
{{ key }}{{ value }}
22 | {% else %} 23 |

Query statistics are empty.

24 | {% endif %} 25 | 26 |

SQL Queries

27 | {% if queries %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for key, value in queries %} 43 | 44 | 45 | 46 | 48 | 49 | {% endfor %} 50 | 51 |
#TimeQuery & Params
#{{ (key + 1) }}{{ value['elapsed'] }}{{ value['called_from'] }}
47 | {{ value['query'] }}
{{ value['params'] }}
52 | {% else %} 53 |

No queries

54 | {% endif %} 55 | -------------------------------------------------------------------------------- /examples/extra_panels/extra_tpl/request_redis.jinja2: -------------------------------------------------------------------------------- 1 | {% if timing_rows %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for key, value in timing_rows %} 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 | 21 |
ResourceValue
{{ key }}{{ value }}
22 | {% else %} 23 |

Cache statistics are empty.

24 | {% endif %} 25 | 26 |

Commands

27 | {% if queries %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {% for key, value in queries %} 45 | 46 | 47 | 48 | 49 | 51 | 52 | {% endfor %} 53 | 54 |
#TimeKeyParams
#{{ (key + 1) }}{{ value['elapsed'] }}{{ value['command'] }}: {{ value['key'] }}{{ value['called_from'] }}
50 | {{ value['params'] }}
55 | {% else %} 56 |

No queries

57 | {% endif %} 58 | -------------------------------------------------------------------------------- /examples/extra_panels/server.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | 4 | import aiohttp_jinja2 5 | import jinja2 6 | from aiohttp import web 7 | 8 | import aiohttp_debugtoolbar 9 | 10 | try: 11 | import aiopg 12 | from extra_pgsql import RequestPgDebugPanel 13 | except ImportError: 14 | print("Module aiopg not installed") 15 | 16 | try: 17 | import aioredis 18 | from extra_redis import RequestRedisDebugPanel 19 | except ImportError: 20 | print("Module aioredis not installed") 21 | 22 | PATH_PARENT = pathlib.Path(__file__).parent 23 | 24 | db_key = web.AppKey["aiopg.Pool"]("db_key") 25 | redis_key = web.AppKey["aioredis.Redis"]("redis_key") 26 | 27 | 28 | @aiohttp_jinja2.template("index.html") 29 | async def basic_handler(request): 30 | # testing for PgSQL 31 | if "db" in request.app: 32 | conn = await request.app[db_key].acquire() 33 | cur = await conn.cursor() 34 | 35 | await cur.execute("SELECT 1") 36 | ret = [] 37 | for row in cur: 38 | ret.append(row) 39 | assert ret == [(1,)] # noqa: S101 40 | 41 | await request.app[db_key].release(conn) 42 | 43 | # testing for Redis 44 | if "redis" in request.app: 45 | with await request.app[redis_key] as redis: 46 | await redis.set("TEST", "VAR", expire=5) 47 | assert b"VAR" == (await redis.get("TEST")) # noqa: S101 48 | 49 | return { 50 | "title": "example aiohttp_debugtoolbar!", 51 | "text": "Hello aiohttp_debugtoolbar!", 52 | "app": request.app, 53 | } 54 | 55 | 56 | async def exception_handler(request): 57 | raise NotImplementedError 58 | 59 | 60 | async def close_pg(app): 61 | app[db_key].close() 62 | await app[db_key].wait_closed() 63 | 64 | 65 | async def close_redis(app): 66 | app[redis_key].close() 67 | await app[redis_key].wait_closed() 68 | 69 | 70 | async def init(): 71 | # add aiohttp_debugtoolbar middleware to you application 72 | app = web.Application() 73 | 74 | extra_panels = [] 75 | if "aiopg" in sys.modules: 76 | extra_panels.append(RequestPgDebugPanel) 77 | if "aioredis" in sys.modules: 78 | extra_panels.append(RequestRedisDebugPanel) 79 | 80 | # install aiohttp_debugtoolbar 81 | aiohttp_debugtoolbar.setup( 82 | app, extra_panels=extra_panels, extra_templates=str(PATH_PARENT / "extra_tpl") 83 | ) 84 | 85 | template = """ 86 | 87 | 88 | {{ title }} 89 | 90 | 91 |

{{ text }}

92 |

93 | 94 | Exception example 95 |

96 | 97 | 98 | """ 99 | # install jinja2 templates 100 | loader = jinja2.DictLoader({"index.html": template}) 101 | aiohttp_jinja2.setup(app, loader=loader) 102 | 103 | # init routes for index page, and page with error 104 | app.router.add_route("GET", "/", basic_handler, name="index") 105 | app.router.add_route("GET", "/exc", exception_handler, name="exc_example") 106 | 107 | if "aiopg" in sys.modules: 108 | # create connection to the database 109 | dsn = "host={host} dbname={db} user={user} password={passw} ".format( 110 | db="postgres", user="developer", passw="1", host="localhost" 111 | ) 112 | app[db_key] = await aiopg.create_pool(dsn, minsize=1, maxsize=2) 113 | # Correct PostgreSQL shutdown 114 | app.on_cleanup.append(close_pg) 115 | 116 | if "aioredis" in sys.modules: 117 | # create redis pool 118 | app[redis_key] = await aioredis.Redis() 119 | # Correct Redis shutdown 120 | app.on_cleanup.append(close_redis) 121 | 122 | return app 123 | 124 | 125 | web.run_app(init(), host="127.0.0.1", port=9000) 126 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | import aiohttp_jinja2 2 | import jinja2 3 | from aiohttp import web 4 | 5 | import aiohttp_debugtoolbar 6 | 7 | 8 | @aiohttp_jinja2.template("index.html") 9 | def basic_handler(request): 10 | return { 11 | "title": "example aiohttp_debugtoolbar!", 12 | "text": "Hello aiohttp_debugtoolbar!", 13 | "app": request.app, 14 | } 15 | 16 | 17 | async def exception_handler(request): 18 | raise NotImplementedError 19 | 20 | 21 | async def init(): 22 | # add aiohttp_debugtoolbar middleware to you application 23 | app = web.Application() 24 | # install aiohttp_debugtoolbar 25 | aiohttp_debugtoolbar.setup(app) 26 | 27 | template = """ 28 | 29 | 30 | {{ title }} 31 | 32 | 33 |

{{ text }}

34 |

35 | 36 | Exception example 37 |

38 | 39 | 40 | """ 41 | # install jinja2 templates 42 | loader = jinja2.DictLoader({"index.html": template}) 43 | aiohttp_jinja2.setup(app, loader=loader) 44 | 45 | # init routes for index page, and page with error 46 | app.router.add_route("GET", "/", basic_handler, name="index") 47 | app.router.add_route("GET", "/exc", exception_handler, name="exc_example") 48 | 49 | return app 50 | 51 | 52 | web.run_app(init(), host="127.0.0.1", port=9000) 53 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | # show 10 slowest invocations: 4 | --durations=10 5 | # a bit of verbosity doesn't hurt: 6 | -v 7 | # report all the things == -rxXs: 8 | -ra 9 | # show values of the local vars in errors: 10 | --showlocals 11 | # coverage reports 12 | --cov=aiohttp_debugtoolbar/ --cov=tests/ --cov-report term 13 | asyncio_mode = auto 14 | filterwarnings = 15 | error 16 | testpaths = tests/ 17 | xfail_strict = true 18 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | mypy==1.16.0 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | aiohttp==3.12.9 4 | aiohttp-jinja2==1.6 5 | aioredis==2.0.1 6 | coverage==7.8.2 7 | docutils==0.21.2 8 | jinja2==3.1.6 9 | multidict==6.4.4 10 | pygments==2.19.1 11 | pytest==8.4.0 12 | pytest-aiohttp==1.1.0 13 | pytest-cov==6.1.1 14 | pytest-sugar==1.0.0 15 | pytest-timeout==2.4.0 16 | yarl==1.20.0 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | ROOT_DIR = Path(__file__).parent 7 | 8 | contents = (ROOT_DIR / "aiohttp_debugtoolbar" / "__init__.py").read_text() 9 | version_match = re.search(r'^__version__ = "([^"]+)"$', contents, re.M) 10 | if version_match is None: 11 | raise RuntimeError("Unable to determine version.") 12 | version = version_match.group(1) 13 | 14 | 15 | def read(fname): 16 | return (ROOT_DIR / fname).read_text().strip() 17 | 18 | 19 | setup( 20 | name="aiohttp-debugtoolbar", 21 | version=version, 22 | description="debugtoolbar for aiohttp", 23 | long_description="\n\n".join((read("README.rst"), read("CHANGES.rst"))), 24 | classifiers=[ 25 | "License :: OSI Approved :: Apache Software License", 26 | "Intended Audience :: Developers", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: 3.13", 34 | "Topic :: Internet :: WWW/HTTP", 35 | "Framework :: AsyncIO", 36 | "Framework :: aiohttp", 37 | ], 38 | author="Nikolay Novik", 39 | author_email="nickolainovik@gmail.com", 40 | url="https://github.com/aio-libs/aiohttp_debugtoolbar", 41 | license="Apache 2", 42 | packages=find_packages(), 43 | install_requires=( 44 | "aiohttp>=3.9", 45 | "aiohttp_jinja2", 46 | ), 47 | include_package_data=True, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import aiohttp_jinja2 2 | import jinja2 3 | import pytest 4 | from aiohttp import web 5 | 6 | from aiohttp_debugtoolbar import setup 7 | 8 | pytest_plugins = ("pytester",) 9 | 10 | 11 | @pytest.fixture 12 | def create_server(unused_tcp_port_factory): 13 | async def create(*, debug=False, ssl_ctx=None, **kw): 14 | app = web.Application() 15 | setup(app, **kw) 16 | 17 | tplt = """ 18 | 19 | 20 | 21 |

{{ head }}

{{ text }} 22 | 23 | """ 24 | loader = jinja2.DictLoader({"tplt.html": tplt}) 25 | aiohttp_jinja2.setup(app, loader=loader) 26 | 27 | return app 28 | 29 | return create 30 | -------------------------------------------------------------------------------- /tests/pep492/test_await.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | 4 | async def test_handler_is_native_coroutine(create_server, aiohttp_client): 5 | async def handler(request): 6 | resp = web.Response( 7 | body=b"native coroutine", status=200, content_type="text/plain" 8 | ) 9 | return resp 10 | 11 | app = await create_server() 12 | app.router.add_route("GET", "/", handler) 13 | client = await aiohttp_client(app) 14 | resp = await client.get("/") 15 | assert 200 == resp.status 16 | 17 | body = await resp.read() 18 | assert b"native coroutine" == body 19 | -------------------------------------------------------------------------------- /tests/test_debug.py: -------------------------------------------------------------------------------- 1 | """werkzeug.debug test 2 | 3 | :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. 4 | :license: BSD license. 5 | """ 6 | 7 | import re 8 | import sys 9 | 10 | import aiohttp_jinja2 11 | 12 | from aiohttp_debugtoolbar.tbtools import text_ 13 | from aiohttp_debugtoolbar.tbtools.console import HTMLStringO 14 | from aiohttp_debugtoolbar.tbtools.repr import ( 15 | DebugReprGenerator, 16 | debug_repr, 17 | dump, 18 | helper, 19 | ) 20 | 21 | 22 | def test_debug_repr(): 23 | assert debug_repr([]) == "[]" 24 | assert ( 25 | debug_repr([1, 2]) == '[1, ' 26 | '2]' 27 | ) 28 | assert ( 29 | debug_repr([1, "test"]) == '[1' 30 | ', ' 31 | "'test']" 32 | ) 33 | assert debug_repr([None]) == '[None]' 34 | assert debug_repr(list(range(20))) == ( 35 | '[0, 1, ' 36 | '2, 3, ' 37 | '4, 5, ' 38 | '6, 7, ' 39 | '8, 9, ' 40 | '10, 11, ' 41 | '12, 13, ' 42 | '14, 15, ' 43 | '16, 17, ' 44 | '18, 19]' 45 | ) 46 | assert debug_repr({}) == "{}" 47 | assert ( 48 | debug_repr({"foo": 42}) == '{' 49 | "'foo'" 50 | ': 42' 51 | "}" 52 | ) 53 | result = debug_repr((1, b"zwei", text_("drei"))) 54 | expected = ( 55 | '(1, b\'' 56 | "zwei', 'drei')" 57 | ) 58 | 59 | assert result == expected 60 | 61 | class Foo: 62 | def __repr__(self): 63 | return "" 64 | 65 | assert debug_repr(Foo()) == '<Foo 42>' "" 66 | 67 | class MyList(list): 68 | pass 69 | 70 | tmp = debug_repr(MyList([1, 2])) 71 | assert ( 72 | tmp == 'test_debug.MyList([' 73 | '1, 2])' 74 | ) 75 | 76 | result = debug_repr(re.compile(r"foo\d")) 77 | assert result == "re.compile(r'foo\\d')" 78 | result = debug_repr(re.compile(text_(r"foo\d"))) 79 | assert result == 're.compile(r' "'foo\\d')" 80 | 81 | assert ( 82 | debug_repr(frozenset("x")) == "frozenset(['x'])" 83 | ) 84 | assert debug_repr(set("x")) == "set(['x'])" 85 | 86 | a = [1] 87 | a.append(a) 88 | assert debug_repr(a) == '[1, [...]]' 89 | 90 | class Foo: 91 | def __repr__(self): 92 | 1 / 0 93 | 94 | result = debug_repr(Foo()) 95 | 96 | assert "division" in result 97 | 98 | 99 | def test_object_dumping(): 100 | class Foo: 101 | x = 42 102 | y = 23 103 | 104 | def __init__(self): 105 | self.z = 15 106 | 107 | drg = DebugReprGenerator() 108 | out = drg.dump_object(Foo()) 109 | assert re.search("Details for", out) 110 | assert re.search('(?s)x.*42', out) 111 | assert re.search('(?s)y.*23', out) 112 | assert re.search('(?s)z.*15', out) 113 | 114 | out = drg.dump_object({"x": 42, "y": 23}) 115 | assert re.search("Contents of", out) 116 | assert re.search('(?s)x.*42', out) 117 | assert re.search('(?s)y.*23', out) 118 | 119 | out = drg.dump_object({"x": 42, "y": 23, 23: 11}) 120 | assert not re.search("Contents of", out) 121 | 122 | out = drg.dump_locals({"x": 42, "y": 23}) 123 | assert re.search("Local variables in frame", out) 124 | assert re.search('(?s)x.*42', out) 125 | assert re.search('(?s)y.*23', out) 126 | 127 | 128 | def test_debug_dump(): 129 | """Test debug dump""" 130 | old = sys.stdout 131 | sys.stdout = HTMLStringO() 132 | try: 133 | dump([1, 2, 3]) 134 | x = sys.stdout.reset() 135 | dump() 136 | y = sys.stdout.reset() 137 | finally: 138 | sys.stdout = old 139 | 140 | assert "Details for list object at" in x 141 | assert '1' in x 142 | assert "Local variables in frame" in y 143 | assert "x" in y 144 | assert "old" in y 145 | 146 | 147 | def test_debug_help(): 148 | """Test debug help""" 149 | old = sys.stdout 150 | sys.stdout = HTMLStringO() 151 | try: 152 | helper([1, 2, 3]) 153 | x = sys.stdout.reset() 154 | finally: 155 | sys.stdout = old 156 | 157 | assert "Help on list object" in x 158 | assert "__delitem__" in x 159 | 160 | 161 | async def test_alternate_debug_path(create_server, aiohttp_client): 162 | async def handler(request): 163 | return aiohttp_jinja2.render_template( 164 | "tplt.html", request, {"head": "HEAD", "text": "text"} 165 | ) 166 | 167 | path_prefix = "/arbitrary_path" 168 | app = await create_server(path_prefix=path_prefix) 169 | app.router.add_route("GET", "/", handler) 170 | 171 | cookie = {"pdtb_active": "pDebugPerformancePanel"} 172 | client = await aiohttp_client(app, cookies=cookie) 173 | resp = await client.get("/") 174 | 175 | resp = await client.get(path_prefix) 176 | await resp.text() 177 | assert 200 == resp.status 178 | -------------------------------------------------------------------------------- /tests/test_exception_views.py: -------------------------------------------------------------------------------- 1 | from aiohttp_debugtoolbar import APP_KEY 2 | 3 | 4 | async def test_view_source(create_server, aiohttp_client): 5 | async def handler(request): 6 | raise NotImplementedError 7 | 8 | app = await create_server() 9 | app.router.add_route("GET", "/", handler) 10 | client = await aiohttp_client(app) 11 | 12 | # make sure that exception page rendered 13 | resp = await client.get("/") 14 | txt = await resp.text() 15 | assert 500 == resp.status 16 | assert '
' in txt 17 | 18 | token = app[APP_KEY]["pdtb_token"] 19 | exc_history = app[APP_KEY]["exc_history"] 20 | 21 | for frame_id in exc_history.frames: 22 | source_url = f"/_debugtoolbar/source?frm={frame_id}&token={token}" 23 | exc_history = app[APP_KEY]["exc_history"] 24 | resp = await client.get(source_url) 25 | await resp.text() 26 | assert resp.status == 200 27 | 28 | 29 | async def test_view_execute(create_server, aiohttp_client): 30 | async def handler(request): 31 | raise NotImplementedError 32 | 33 | app = await create_server() 34 | app.router.add_route("GET", "/", handler) 35 | client = await aiohttp_client(app) 36 | # make sure that exception page rendered 37 | resp = await client.get("/") 38 | txt = await resp.text() 39 | assert 500 == resp.status 40 | assert '
' in txt 41 | 42 | token = app[APP_KEY]["pdtb_token"] 43 | exc_history = app[APP_KEY]["exc_history"] 44 | 45 | source_url = "/_debugtoolbar/source" 46 | execute_url = "/_debugtoolbar/execute" 47 | for frame_id in exc_history.frames: 48 | params = {"frm": frame_id, "token": token} 49 | resp = await client.get(source_url, params=params) 50 | await resp.text() 51 | assert resp.status == 200 52 | 53 | params = {"frm": frame_id, "token": token, "cmd": "dump(object)"} 54 | resp = await client.get(execute_url, params=params) 55 | await resp.text() 56 | assert resp.status == 200 57 | 58 | # wrong token 59 | params = {"frm": frame_id, "token": "x", "cmd": "dump(object)"} 60 | resp = await client.get(execute_url, params=params) 61 | assert resp.status == 400 62 | # no token at all 63 | params = {"frm": frame_id, "cmd": "dump(object)"} 64 | resp = await client.get(execute_url, params=params) 65 | assert resp.status == 400 66 | 67 | 68 | async def test_view_exception(create_server, aiohttp_client): 69 | async def handler(request): 70 | raise NotImplementedError 71 | 72 | app = await create_server() 73 | app.router.add_route("GET", "/", handler) 74 | client = await aiohttp_client(app) 75 | # make sure that exception page rendered 76 | resp = await client.get("/") 77 | txt = await resp.text() 78 | assert 500 == resp.status 79 | assert '
' in txt 80 | 81 | token = app[APP_KEY]["pdtb_token"] 82 | exc_history = app[APP_KEY]["exc_history"] 83 | 84 | tb_id = list(exc_history.tracebacks.keys())[0] 85 | url = f"/_debugtoolbar/exception?tb={tb_id}&token={token}" 86 | 87 | resp = await client.get(url) 88 | await resp.text() 89 | assert resp.status == 200 90 | assert '
' in txt 91 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | 4 | import pytest 5 | 6 | 7 | @pytest.mark.skipif( 8 | not sys.platform.startswith("linux") 9 | or platform.python_implementation() == "PyPy" 10 | or sys.version_info[:2] == (3, 12), 11 | reason="Unreliable", 12 | ) 13 | def test_import_time(pytester: pytest.Pytester) -> None: 14 | """Check that importing aiohttp-debugtoolbar doesn't take too long. 15 | Obviously, the time may vary on different machines and may need to be adjusted 16 | from time to time, but this should provide an early warning if something is 17 | added that significantly increases import time. 18 | """ 19 | best_time_ms = 1000 20 | cmd = "import timeit; print(int(timeit.timeit('import aiohttp_debugtoolbar', number=1)*1000))" 21 | for _ in range(3): 22 | r = pytester.run(sys.executable, "-We", "-c", cmd) 23 | 24 | assert not r.stderr.str() 25 | runtime_ms = int(r.stdout.str()) 26 | if runtime_ms < best_time_ms: 27 | best_time_ms = runtime_ms 28 | 29 | assert best_time_ms < 350 30 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import aiohttp_jinja2 4 | import pytest 5 | from aiohttp import web 6 | from aiohttp.test_utils import make_mocked_request 7 | 8 | import aiohttp_debugtoolbar 9 | 10 | 11 | async def test_render_toolbar_page(create_server, aiohttp_client): 12 | async def handler(request): 13 | return aiohttp_jinja2.render_template( 14 | "tplt.html", request, {"head": "HEAD", "text": "text"} 15 | ) 16 | 17 | app = await create_server() 18 | app.router.add_route("GET", "/", handler) 19 | cookie = {"pdtb_active": "pDebugPerformancePanel"} 20 | client = await aiohttp_client(app, cookies=cookie) 21 | 22 | # make sure that toolbar button present on apps page 23 | # add cookie to enforce performance panel measure time 24 | resp = await client.get("/") 25 | assert 200 == resp.status 26 | txt = await resp.text() 27 | assert "toolbar_button.css" in txt 28 | assert "pDebugToolbarHandle" in txt 29 | 30 | # make sure that debug toolbar page working 31 | url = "/_debugtoolbar" 32 | resp = await client.get(url) 33 | await resp.text() 34 | assert 200 == resp.status 35 | 36 | 37 | async def test_render_with_exception(create_server, aiohttp_client): 38 | async def handler(request): 39 | raise NotImplementedError 40 | 41 | app = await create_server() 42 | app.router.add_route("GET", "/", handler) 43 | client = await aiohttp_client(app) 44 | # make sure that exception page rendered 45 | resp = await client.get("/") 46 | txt = await resp.text() 47 | assert 500 == resp.status 48 | assert '
' in txt 49 | 50 | 51 | async def test_intercept_redirect(create_server, aiohttp_client): 52 | async def handler(request): 53 | raise web.HTTPMovedPermanently(location="/") 54 | 55 | app = await create_server() 56 | app.router.add_route("GET", "/", handler) 57 | client = await aiohttp_client(app) 58 | # make sure that exception page rendered 59 | resp = await client.get("/", allow_redirects=False) 60 | txt = await resp.text() 61 | assert 200 == resp.status 62 | assert "Redirect intercepted" in txt 63 | 64 | 65 | async def test_no_location_no_intercept(create_server, aiohttp_client): 66 | async def handler(request): 67 | return web.Response(text="no location", status=301) 68 | 69 | app = await create_server() 70 | app.router.add_route("GET", "/", handler) 71 | client = await aiohttp_client(app) 72 | 73 | resp = await client.get("/", allow_redirects=False) 74 | txt = await resp.text() 75 | assert 301 == resp.status 76 | assert "location" not in resp.headers 77 | assert "no location" in txt 78 | 79 | 80 | async def test_intercept_redirects_disabled(create_server, aiohttp_client): 81 | async def handler(request): 82 | raise web.HTTPMovedPermanently(location="/") 83 | 84 | app = await create_server(intercept_redirects=False) 85 | app.router.add_route("GET", "/", handler) 86 | client = await aiohttp_client(app) 87 | # make sure that exception page rendered 88 | resp = await client.get("/", allow_redirects=False) 89 | txt = await resp.text() 90 | assert 301 == resp.status 91 | assert "301: Moved Permanently" == txt 92 | 93 | 94 | async def test_toolbar_not_enabled(create_server, aiohttp_client): 95 | async def handler(request): 96 | return aiohttp_jinja2.render_template( 97 | "tplt.html", request, {"head": "HEAD", "text": "text"} 98 | ) 99 | 100 | app = await create_server(enabled=False) 101 | app.router.add_route("GET", "/", handler) 102 | client = await aiohttp_client(app) 103 | # make sure that toolbar button NOT present on apps page 104 | resp = await client.get("/") 105 | assert 200 == resp.status 106 | txt = await resp.text() 107 | assert "pDebugToolbarHandle" not in txt 108 | 109 | # make sure that debug toolbar page working 110 | url = "/_debugtoolbar" 111 | resp = await client.get(url) 112 | await resp.text() 113 | assert 200 == resp.status 114 | 115 | 116 | async def test_toolbar_content_type_json(create_server, aiohttp_client): 117 | async def handler(request): 118 | response = web.Response(status=200) 119 | response.content_type = "application/json" 120 | response.text = '{"a": 42}' 121 | return response 122 | 123 | app = await create_server() 124 | app.router.add_route("GET", "/", handler) 125 | client = await aiohttp_client(app) 126 | # make sure that toolbar button NOT present on apps page 127 | resp = await client.get("/") 128 | payload = await resp.json() 129 | assert 200 == resp.status 130 | assert payload == {"a": 42} 131 | 132 | 133 | async def test_do_not_intercept_exceptions(create_server, aiohttp_client): 134 | async def handler(request): 135 | raise NotImplementedError 136 | 137 | app = await create_server(intercept_exc=False) 138 | app.router.add_route("GET", "/", handler) 139 | client = await aiohttp_client(app) 140 | # make sure that exception page rendered 141 | resp = await client.get("/") 142 | txt = await resp.text() 143 | assert 500 == resp.status 144 | assert '
' not in txt 145 | 146 | 147 | async def test_setup_not_called_exception(): 148 | request = make_mocked_request("GET", "/path") 149 | with pytest.raises(RuntimeError): 150 | await aiohttp_debugtoolbar.middleware(request, lambda r: r) 151 | 152 | 153 | async def test_setup_only_adds_middleware_if_not_already_added(): 154 | app = web.Application(middlewares=[aiohttp_debugtoolbar.middleware]) 155 | aiohttp_debugtoolbar.setup(app) 156 | assert list(app.middlewares) == [aiohttp_debugtoolbar.middleware] 157 | 158 | 159 | async def test_process_stream_response(create_server, aiohttp_client): 160 | async def handler(request): 161 | response = web.StreamResponse(status=200) 162 | response.content_type = "text/html" 163 | await response.prepare(request) 164 | await response.write(b"text") 165 | return response 166 | 167 | app = await create_server() 168 | app.router.add_route("GET", "/", handler) 169 | client = await aiohttp_client(app) 170 | 171 | # make sure that toolbar button NOT present on apps page 172 | resp = await client.get("/") 173 | payload = await resp.read() 174 | assert 200 == resp.status 175 | assert payload == b"text" 176 | 177 | 178 | async def test_performance_panel_with_handler(create_server, aiohttp_client): 179 | async def handler(request): 180 | return web.Response() 181 | 182 | app = await create_server() 183 | app.router.add_route("GET", "/", handler) 184 | client = await aiohttp_client(app) 185 | 186 | resp = await client.get("/") 187 | assert 200 == resp.status 188 | 189 | 190 | async def test_performance_panel_with_cbv(create_server, aiohttp_client): 191 | class TestView(web.View): 192 | async def get(self): 193 | return web.Response() 194 | 195 | app = await create_server() 196 | app.router.add_view("/", TestView) 197 | client = await aiohttp_client(app) 198 | 199 | resp = await client.get("/") 200 | assert 200 == resp.status 201 | 202 | 203 | async def test_request_history(create_server, aiohttp_client): 204 | async def handler(request: web.Request) -> web.Response: 205 | return web.Response() 206 | 207 | app = await create_server(check_host=False) 208 | app.router.add_route("GET", "/", handler) 209 | app.router.add_route("GET", "/favicon.ico", handler) 210 | client = await aiohttp_client(app) 211 | 212 | # Should be logged 213 | async with client.get("/") as r: 214 | assert r.status == 200 215 | # Should not be logged 216 | async with client.get("/favicon.ico") as r: 217 | assert r.status == 200 218 | async with client.get("/_debugtoolbar/static/img/aiohttp.svg") as r: 219 | assert r.status == 200 220 | 221 | request_history = tuple(app[aiohttp_debugtoolbar.APP_KEY]["request_history"]) 222 | assert len(request_history) == 1 223 | assert request_history[0][1].request.path == "/" 224 | 225 | await asyncio.sleep(0) # Workaround, maybe fixed in aiohttp 4. 226 | -------------------------------------------------------------------------------- /tests/test_panel.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import aiohttp_jinja2 4 | 5 | from aiohttp_debugtoolbar.panels.base import DebugPanel 6 | from aiohttp_debugtoolbar.utils import TEMPLATE_KEY 7 | 8 | 9 | async def test_request_vars_panel(create_server, aiohttp_client): 10 | async def handler(request): 11 | return aiohttp_jinja2.render_template( 12 | "tplt.html", request, {"head": "HEAD", "text": "text"} 13 | ) 14 | 15 | app = await create_server() 16 | app.router.add_route("GET", "/", handler) 17 | # add cookie to request 18 | cookie = {"aiodtb_cookie": "aioDebugRequestPanel_Cookie"} 19 | client = await aiohttp_client(app, cookies=cookie) 20 | resp = await client.get("/") 21 | assert 200 == resp.status 22 | txt = await resp.text() 23 | # Toolbar Button exists on page 24 | assert "pDebugToolbarHandle" in txt 25 | 26 | # make sure that debug toolbar page working 27 | resp = await client.get("/_debugtoolbar") 28 | txt = await resp.text() 29 | assert "aioDebugRequestPanel_Cookie" in txt 30 | assert 200 == resp.status 31 | 32 | 33 | async def test_extra_panel(create_server, aiohttp_client): 34 | async def handler(request): 35 | return aiohttp_jinja2.render_template( 36 | "tplt.html", request, {"head": "HEAD", "text": "text"} 37 | ) 38 | 39 | class TestExtraPanel(DebugPanel): 40 | name = "aioTestExtraPanel" 41 | has_content = True 42 | template = "test.jinja2" 43 | title = name 44 | nav_title = title 45 | 46 | async def process_response(self, response): 47 | self.data = data = {} 48 | data.update( 49 | { 50 | "panel_test": self.name, 51 | } 52 | ) 53 | 54 | parent_path = pathlib.Path(__file__).parent 55 | app = await create_server( 56 | extra_panels=[TestExtraPanel], extra_templates=str(parent_path / "tpl") 57 | ) 58 | app.router.add_route("GET", "/", handler) 59 | # make sure that toolbar button present on apps page 60 | client = await aiohttp_client(app) 61 | resp = await client.get("/") 62 | assert 200 == resp.status 63 | txt = await resp.text() 64 | assert "pDebugToolbarHandle" in txt 65 | 66 | # check template from extra_templates 67 | assert "test.jinja2" in app[TEMPLATE_KEY].list_templates() 68 | 69 | # make sure that debug toolbar page working and extra panel exists 70 | resp = await client.get("/_debugtoolbar") 71 | txt = await resp.text() 72 | assert 200 == resp.status 73 | assert "aioExtraPanelTemplate" in txt 74 | assert "aioTestExtraPanel" in txt 75 | -------------------------------------------------------------------------------- /tests/test_panels_versions.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import create_autospec 2 | 3 | from aiohttp import web 4 | 5 | from aiohttp_debugtoolbar.panels import VersionDebugPanel 6 | 7 | 8 | async def test_packages(): 9 | request_mock = create_autospec(web.Request) 10 | panel = VersionDebugPanel(request_mock) 11 | 12 | jinja2_metadata = next(p for p in panel.data["packages"] if p["name"] == "Jinja2") 13 | assert "version" in jinja2_metadata 14 | assert jinja2_metadata["lowername"] == "jinja2" 15 | assert any("MarkupSafe" in d for d in jinja2_metadata["dependencies"]) 16 | assert "url" in jinja2_metadata 17 | -------------------------------------------------------------------------------- /tests/test_server_push.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from aiohttp_debugtoolbar import APP_KEY 4 | 5 | 6 | async def test_sse(create_server, aiohttp_client): 7 | async def handler(request): 8 | raise NotImplementedError 9 | 10 | app = await create_server() 11 | app.router.add_route("GET", "/", handler) 12 | client = await aiohttp_client(app) 13 | # make sure that exception page rendered 14 | resp = await client.get("/") 15 | txt = await resp.text() 16 | assert 500 == resp.status 17 | assert '
' in txt 18 | 19 | # get request id from history 20 | history = app[APP_KEY]["request_history"] 21 | request_id = history[0][0] 22 | 23 | url = "/_debugtoolbar/sse" 24 | resp = await client.get(url) 25 | data = await resp.text() 26 | data = data.strip() 27 | 28 | # split and check EventSource data 29 | event_id, event, payload_raw = data.split("\n") 30 | assert event_id == f"id: {request_id}" 31 | assert event == "event: new_request" 32 | 33 | payload_json = payload_raw.removeprefix("data: ") 34 | payload = json.loads(payload_json) 35 | expected = [ 36 | [ 37 | request_id, 38 | {"path": "/", "scheme": "http", "method": "GET", "status_code": 500}, 39 | "", 40 | ] 41 | ] 42 | 43 | assert payload == expected, payload 44 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from aiohttp_debugtoolbar.utils import addr_in, escape, format_fname 5 | 6 | 7 | def test_escape(): 8 | assert escape(None) == "" 9 | assert escape(42) == "42" 10 | assert escape("<>") == "<>" 11 | assert escape('"foo"') == '"foo"' 12 | assert escape('"foo"', True) == ""foo"" 13 | 14 | 15 | def test_format_fname(): 16 | # test_builtin 17 | assert format_fname("{a}") == "{a}" 18 | 19 | # test_relpath 20 | val = "." + os.path.sep + "foo" 21 | assert format_fname(val) == val 22 | 23 | # test_unknown 24 | val = ".." + os.path.sep + "foo" 25 | assert format_fname(val) == "./../foo".replace("/", os.path.sep) 26 | 27 | 28 | def test_module_file_path(): 29 | sys_path_l = ( 30 | "/foo/", 31 | "/foo/bar", 32 | "/usr/local/python/site-packages/", 33 | ) 34 | 35 | prefix = "c:" if sys.platform == "win32" else "" 36 | sys_path = map(lambda p: prefix + p.replace("/", os.path.sep), sys_path_l) 37 | modpath = format_fname( 38 | prefix 39 | + "/foo/bar/aiohttp_debugtoolbar/tests/debugfoo.py".replace("/", os.path.sep), 40 | sys_path, 41 | ) 42 | expected = "".replace("/", os.path.sep) 43 | assert modpath == expected 44 | 45 | 46 | def test_no_matching_sys_path(): 47 | prefix = "c:" if sys.platform == "win32" else "" 48 | val = prefix + "/foo/bar/aiohttp_debugtoolbar/foo.py".replace("/", os.path.sep) 49 | sys_path = [prefix + "/bar/baz".replace("/", os.path.sep)] 50 | expected = f"<{prefix}/foo/bar/aiohttp_debugtoolbar/foo.py>".replace( 51 | "/", os.path.sep 52 | ) 53 | assert format_fname(val, sys_path) == expected 54 | 55 | 56 | def test_addr_in(): 57 | assert not addr_in("127.0.0.1", []) 58 | assert addr_in("127.0.0.1", ["127.0.0.1"]) 59 | -------------------------------------------------------------------------------- /tests/tpl/test.jinja2: -------------------------------------------------------------------------------- 1 |

{{ panel_test }}

2 | --------------------------------------------------------------------------------