├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── publish.yml │ └── test-suite.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── debug_toolbar ├── __init__.py ├── api.py ├── dependencies.py ├── middleware.py ├── panels │ ├── __init__.py │ ├── headers.py │ ├── logging.py │ ├── profiling.py │ ├── redirects.py │ ├── request.py │ ├── routes.py │ ├── settings.py │ ├── sql.py │ ├── sqlalchemy.py │ ├── timer.py │ ├── tortoise.py │ └── versions.py ├── py.typed ├── responses.py ├── settings.py ├── statics │ ├── css │ │ ├── print.css │ │ └── toolbar.css │ ├── img │ │ ├── icon-green.svg │ │ └── icon-white.svg │ └── js │ │ ├── redirect.js │ │ ├── refresh.js │ │ ├── timer.js │ │ ├── toolbar.js │ │ ├── utils.js │ │ └── versions.js ├── templates │ ├── base.html │ ├── includes │ │ ├── panel_button.html │ │ └── panel_content.html │ ├── panels │ │ ├── headers.html │ │ ├── logging.html │ │ ├── profiling.html │ │ ├── request.html │ │ ├── routes.html │ │ ├── settings.html │ │ ├── sql.html │ │ ├── timer.html │ │ └── versions.html │ └── redirect.html ├── toolbar.py ├── types.py └── utils.py ├── docs ├── CNAME ├── changelog.md ├── css │ └── styles.css ├── img │ ├── Swagger.png │ ├── favicon.ico │ ├── logo.png │ ├── panels │ │ ├── Headers.png │ │ ├── Logging.png │ │ ├── Profiling.png │ │ ├── Request.png │ │ ├── Routes.png │ │ ├── SQLAlchemy.png │ │ ├── Settings.png │ │ ├── Timer.png │ │ └── Versions.png │ └── tab.png ├── index.md ├── overrides │ └── main.html ├── panels │ ├── default.md │ ├── dev.md │ ├── panel.md │ └── sql.md ├── settings.md └── src │ ├── dev │ ├── panels.py │ └── quickstart.py │ ├── panels │ ├── profiling.py │ ├── settings.py │ ├── sqlalchemy │ │ ├── add_engines.py │ │ └── panel.py │ └── tortoise.py │ └── quickstart.py ├── mkdocs.yml ├── pyproject.toml ├── scripts ├── lint └── test └── tests ├── __init__.py ├── conftest.py ├── mark.py ├── panels ├── __init__.py ├── sqlalchemy │ ├── __init__.py │ ├── conftest.py │ ├── crud.py │ ├── database.py │ ├── models.py │ └── test_sqlalchemy.py ├── test_headers.py ├── test_logging.py ├── test_profiling.py ├── test_redirects.py ├── test_request.py ├── test_routes.py ├── test_settings.py ├── test_timer.py ├── test_versions.py └── tortoise │ ├── __init__.py │ ├── conftest.py │ ├── crud.py │ ├── models.py │ └── test_tortoise.py ├── templates └── index.html ├── test_api.py ├── test_middleware.py └── testclient.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: pip 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | groups: 9 | python-packages: 10 | patterns: 11 | - '*' 12 | - package-ecosystem: github-actions 13 | directory: / 14 | schedule: 15 | interval: weekly 16 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-python@v5 14 | with: 15 | cache: pip 16 | python-version: "3.12" 17 | 18 | - name: Install dependencies 19 | run: pip install .[doc] 20 | 21 | - name: Deploy to GitHub Pages 22 | run: mkdocs gh-deploy --force 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | 17 | - name: Install dependencies 18 | run: pip install build 19 | 20 | - name: Build 21 | run: python -m build 22 | 23 | - name: Publish to PyPI 24 | uses: pypa/gh-action-pypi-publish@release/v1 25 | with: 26 | password: ${{ secrets.PYPI_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/test-suite.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | 3 | on: 4 | push: 5 | 6 | pull_request: 7 | branches: ['main'] 8 | 9 | jobs: 10 | tests: 11 | name: Python ${{ matrix.python-version }} 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-python@v5 21 | with: 22 | cache: pip 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | run: pip install -e .[test] 27 | 28 | - name: Tests 29 | run: scripts/test 30 | 31 | - name: Upload coverage 32 | uses: codecov/codecov-action@v4 33 | env: 34 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | *.DS_Store 3 | *.box 4 | *.log 5 | *.pyc 6 | 7 | __pycache__/ 8 | .coverage 9 | .mypy_cache/ 10 | .pytest_cache/ 11 | .vscode/ 12 | coverage.xml 13 | dist/ 14 | htmlcov/ 15 | site/ 16 | venv* 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.6.3 4 | 5 | * Added support to SQLAlchemy `AsyncSession` and multiple binds 6 | 7 | ### 0.6.2 8 | 9 | * **SQLAlchemyPanel**: Handled HTTPException from dependencies 10 | * Removed pydantic future annotations 11 | 12 | ### 0.6.1 13 | 14 | * Removed deprecated `fastapi_astack` scope 15 | * Fixed `0.6` release tag. 16 | 17 | ### 0.6.0 18 | 19 | * **SQLAlchemyPanel**: Added `async_exit_stack` arg to `solve_dependencies` function 20 | * **DebugToolbarMiddleware**: Removed `settings.ALLOWED_IPS` in favor of `settings.ALLOWED_HOSTS` 21 | * **VersionsPanel**: Removed `pkg_resources` in favor of `importlib.metadata` 22 | * Added ruff and bandit and removed black and isort 23 | * Removed deprecated `on_event` 24 | * Added minor improvements 25 | 26 | ### 0.5.0 27 | 28 | * Added Pydantic v2 support 29 | * Removed Pydantic v1 support 30 | * Removed `PydanticPanel` 31 | 32 | ### 0.4.0 33 | 34 | * Fixed middleware `url_path_for` 35 | * Improved SQLAlchemy panel 36 | 37 | ### 0.3.2 38 | 39 | * Fixed response body stream 40 | 41 | ### 0.3.1 42 | 43 | * Fixed pyproject.toml, added package data 44 | * Improved panel templates 45 | * Fixed profiling on Safari browser 46 | 47 | ### 0.3.0 48 | 49 | * Added refresh cookie system and `JSON.parse` swap removed 50 | * Fixed SQL query encoding 51 | * Fixed `SQLAlchemyPanel` , added missing `fastapi_astack` to scope (`fastapi >= 0.74.0`) 52 | * Added `SQLAlchemyPanel.add_engines` method 53 | * Added `tortoise-orm >= 0.19.0` support 54 | * Fixed `VersionsPanel` JS, package home can be null 55 | 56 | ### 0.2.1 57 | 58 | * Added `PydanticPanel` 59 | * Removed `current_thread` in favor of `get_ident` 60 | * Added anyio task groups 61 | * Removed `get_running_loop` in favor of `get_event_loop` 62 | * Improved tables styles 63 | 64 | ### 0.2.0 65 | 66 | * Fixed `ThreadPoolExecutor` for all sync endpoints 67 | * Added cookie-based refresh 68 | * Added exception handling for dependency resolution 69 | * Added minor improvements to `VersionPanel` 70 | 71 | ### 0.1.3 72 | 73 | * Added `TortoisePanel` 74 | 75 | ### 0.1.2 76 | 77 | * Removed SQL compiled query in favor of statement params 78 | * Added SQLAlchemy unregister 79 | * Added `SQLPanel` base class 80 | 81 | ### 0.1.1 82 | 83 | * Improved dependency resolution 84 | * Added minor improvements 85 | 86 | ### 0.1.0 87 | 88 | * Added `SQLAlchemyPanel` 89 | * Added `LOGGING_COLORS` to panel templates 90 | * Minor improvements 91 | 92 | ### 0.0.6 93 | 94 | * Improved `VersionsPanel` script 95 | * Added docs 96 | 97 | ### 0.0.5 98 | 99 | * Fixed multiple profilers on the same thread 100 | * Fixed `VersionsPanel` Pypi url 101 | 102 | ### 0.0.4 103 | 104 | * Added pypi details to `VersionsPanel` 105 | * Improved assets 106 | * Added `LOGGING_COLORS` 107 | * Highlighted matched endpoint 108 | 109 | ### 0.0.3 110 | 111 | * Sorted routes by path 112 | 113 | ### 0.0.2 114 | 115 | * Added mounted apps support (e.g. ariadne.asgi.GraphQL) 116 | 117 | ### 0.0.1 118 | 119 | * 📦 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Rob Hudson and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #  Debug Toolbar 2 | 3 |
4 |
5 |
6 |
7 |
9 | 🐞A debug toolbar for FastAPI based on the original django-debug-toolbar.🐞
10 |
Swagger UI & GraphQL are supported.
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
{html.escape(content)}
" 18 | scripts = [] 19 | else: 20 | panel = toolbar.get_panel_by_id(panel_id) 21 | content, scripts = panel.content, panel.scripts 22 | 23 | return {"content": content, "scripts": scripts} 24 | -------------------------------------------------------------------------------- /debug_toolbar/dependencies.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as t 4 | from contextlib import AsyncExitStack 5 | 6 | from fastapi import HTTPException, Request 7 | from fastapi.dependencies.utils import solve_dependencies 8 | 9 | 10 | async def get_dependencies(request: Request) -> dict[str, t.Any] | None: 11 | route = request["route"] 12 | 13 | if hasattr(route, "dependant"): 14 | try: 15 | solved_result = await solve_dependencies( 16 | request=request, 17 | dependant=route.dependant, 18 | dependency_overrides_provider=route.dependency_overrides_provider, 19 | async_exit_stack=AsyncExitStack(), 20 | ) 21 | except HTTPException: 22 | pass 23 | else: 24 | return solved_result[0] 25 | return None 26 | -------------------------------------------------------------------------------- /debug_toolbar/middleware.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | import re 4 | import typing as t 5 | from urllib import parse 6 | 7 | from anyio import CapacityLimiter 8 | from anyio.lowlevel import RunVar 9 | from fastapi import APIRouter, HTTPException, Request, Response, status 10 | from fastapi.responses import StreamingResponse 11 | from fastapi.staticfiles import StaticFiles 12 | from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint 13 | from starlette.routing import NoMatchFound 14 | from starlette.types import ASGIApp 15 | 16 | from debug_toolbar.api import render_panel 17 | from debug_toolbar.settings import DebugToolbarSettings 18 | from debug_toolbar.toolbar import DebugToolbar 19 | from debug_toolbar.utils import import_string, matched_route 20 | 21 | 22 | def show_toolbar(request: Request, settings: DebugToolbarSettings) -> bool: 23 | if request.client is not None and settings.ALLOWED_HOSTS is not None: 24 | return request.app.debug and request.client.host in settings.ALLOWED_HOSTS 25 | return request.app.debug 26 | 27 | 28 | class DebugToolbarMiddleware(BaseHTTPMiddleware): 29 | def __init__(self, app: ASGIApp, **settings: t.Any) -> None: 30 | super().__init__(app) 31 | self.settings = DebugToolbarSettings(**settings) 32 | self.show_toolbar = import_string(self.settings.SHOW_TOOLBAR_CALLBACK) 33 | self.router: APIRouter = app # type: ignore[assignment] 34 | 35 | while not isinstance(self.router, APIRouter): 36 | self.router = self.router.app 37 | try: 38 | self.router.url_path_for("debug_toolbar.render_panel") 39 | except NoMatchFound: 40 | self.init_toolbar() 41 | 42 | def init_toolbar(self) -> None: 43 | self.router.get( 44 | self.settings.API_URL, 45 | name="debug_toolbar.render_panel", 46 | include_in_schema=False, 47 | )(self.require_show_toolbar(render_panel)) 48 | 49 | self.router.mount( 50 | self.settings.STATIC_URL, 51 | StaticFiles(packages=[__package__]), 52 | name="debug_toolbar.static", 53 | ) 54 | 55 | _default_thread_limiter: RunVar[CapacityLimiter] = RunVar( 56 | "_default_thread_limiter" 57 | ) 58 | _default_thread_limiter.set(CapacityLimiter(1)) 59 | 60 | async def dispatch( 61 | self, 62 | request: Request, 63 | call_next: RequestResponseEndpoint, 64 | ) -> Response: 65 | request.scope["route"] = matched_route(request) 66 | 67 | if ( 68 | not request["route"] 69 | or not self.show_toolbar(request, self.settings) 70 | or self.settings.API_URL in request.url.path 71 | ): 72 | return await call_next(request) 73 | 74 | toolbar = DebugToolbar(request, call_next, self.settings) 75 | response = t.cast(StreamingResponse, await toolbar.process_request(request)) 76 | content_type = response.headers.get("Content-Type", "") 77 | is_html = content_type.startswith("text/html") 78 | 79 | if not (is_html or content_type == "application/json") or ( 80 | "gzip" in response.headers.get("Accept-Encoding", "") 81 | ): 82 | return response 83 | 84 | await toolbar.record_stats(response) 85 | await toolbar.record_server_timing(response) 86 | toolbar.generate_server_timing_header(response) 87 | 88 | if is_html: 89 | body = b"" 90 | 91 | async for chunk in response.body_iterator: 92 | if not isinstance(chunk, bytes): 93 | chunk = chunk.encode(response.charset) 94 | body += chunk 95 | 96 | decoded = body.decode(response.charset) 97 | pattern = re.escape(self.settings.INSERT_BEFORE) 98 | bits = re.split(pattern, decoded, flags=re.IGNORECASE) 99 | 100 | if len(bits) > 1: 101 | bits[-2] += toolbar.render_toolbar() 102 | body = self.settings.INSERT_BEFORE.join(bits).encode(response.charset) 103 | response.headers["Content-Length"] = str(len(body)) 104 | 105 | async def stream() -> t.AsyncGenerator[bytes, None]: 106 | yield body 107 | 108 | response.body_iterator = stream() 109 | else: 110 | data = parse.quote(json.dumps(toolbar.refresh())) 111 | response.set_cookie(key="dtRefresh", value=data) 112 | 113 | return response 114 | 115 | def require_show_toolbar( 116 | self, 117 | f: t.Callable[..., t.Any], 118 | ) -> t.Callable[[t.Any], t.Any]: 119 | @functools.wraps(f) 120 | def decorator(request: Request, *args: t.Any, **kwargs: t.Any) -> t.Any: 121 | if not self.show_toolbar(request, self.settings): 122 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) 123 | return f(request, *args, **kwargs) 124 | 125 | return decorator 126 | -------------------------------------------------------------------------------- /debug_toolbar/panels/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as t 4 | 5 | from fastapi import Request, Response 6 | from starlette.middleware.base import RequestResponseEndpoint 7 | 8 | from debug_toolbar.types import ServerTiming, Stats 9 | from debug_toolbar.utils import get_name_from_obj 10 | 11 | if t.TYPE_CHECKING: 12 | from debug_toolbar.toolbar import DebugToolbar 13 | 14 | 15 | class Panel: 16 | has_content: bool = True 17 | 18 | def __init__( 19 | self, 20 | toolbar: "DebugToolbar", 21 | call_next: RequestResponseEndpoint, 22 | ) -> None: 23 | self.toolbar = toolbar 24 | self.call_next = call_next 25 | 26 | @property 27 | def panel_id(self) -> str: 28 | return self.__class__.__name__ 29 | 30 | @property 31 | def enabled(self) -> bool: 32 | disabled_panels = self.toolbar.settings.DISABLE_PANELS 33 | panel_path = get_name_from_obj(self) 34 | 35 | disable_panel = ( 36 | panel_path in disabled_panels 37 | or panel_path.replace(".panel.", ".") in disabled_panels 38 | ) 39 | if disable_panel: 40 | default = "off" 41 | else: 42 | default = "on" 43 | 44 | return self.toolbar.request.cookies.get(f"dt{self.panel_id}", default) == "on" 45 | 46 | @property 47 | def nav_title(self) -> str: 48 | return self.title 49 | 50 | @property 51 | def nav_subtitle(self) -> str: 52 | return "" 53 | 54 | @property 55 | def title(self) -> str: 56 | raise NotImplementedError 57 | 58 | @property 59 | def template(self) -> str: 60 | raise NotImplementedError 61 | 62 | @property 63 | def content(self) -> str: 64 | if self.has_content: 65 | return self.render(**self.get_stats()) 66 | return "" 67 | 68 | def render(self, **context: t.Any) -> str: 69 | return self.toolbar.render(self.template, **context) 70 | 71 | def url_for(self, name: str, **path_params: t.Any) -> str: 72 | return str(self.toolbar.request.url_for(name, **path_params)) 73 | 74 | @property 75 | def scripts(self) -> list[str]: 76 | return [] 77 | 78 | async def process_request(self, request: Request) -> Response: 79 | return await self.call_next(request) 80 | 81 | async def generate_stats(self, request: Request, response: Response) -> Stats: 82 | return {} 83 | 84 | def get_stats(self) -> Stats: 85 | return self.toolbar.stats.get(self.panel_id, {}) 86 | 87 | async def record_stats(self, request: Request, response: Response) -> None: 88 | stats = await self.generate_stats(request, response) 89 | 90 | if stats is not None: 91 | self.toolbar.stats.setdefault(self.panel_id, {}).update(stats) 92 | 93 | async def generate_server_timing( 94 | self, 95 | request: Request, 96 | response: Response, 97 | ) -> ServerTiming: 98 | return [] 99 | 100 | def get_server_timing_stats(self) -> ServerTiming: 101 | return self.toolbar.server_timing_stats.get(self.panel_id, []) 102 | 103 | async def record_server_timing(self, request: Request, response: Response) -> None: 104 | stats = await self.generate_server_timing(request, response) 105 | 106 | if stats is not None: 107 | st_stats = self.toolbar.server_timing_stats.setdefault(self.panel_id, []) 108 | st_stats += list(stats) 109 | -------------------------------------------------------------------------------- /debug_toolbar/panels/headers.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, Response 2 | 3 | from debug_toolbar.panels import Panel 4 | from debug_toolbar.types import Stats 5 | 6 | 7 | class HeadersPanel(Panel): 8 | title = "Headers" 9 | template = "panels/headers.html" 10 | 11 | async def generate_stats(self, request: Request, response: Response) -> Stats: 12 | if "cookie" in (request_headers := dict(request.headers)): 13 | request_headers["cookie"] = "=> see Request panel" 14 | 15 | return { 16 | "request_headers": request_headers, 17 | "environ": request.scope.get("asgi", {}), 18 | "response_headers": response.headers, 19 | } 20 | -------------------------------------------------------------------------------- /debug_toolbar/panels/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | import logging 5 | import typing as t 6 | 7 | from fastapi import Request, Response 8 | from starlette.concurrency import run_in_threadpool 9 | 10 | from debug_toolbar.panels import Panel 11 | from debug_toolbar.types import Stats 12 | from debug_toolbar.utils import is_coroutine, pluralize 13 | 14 | try: 15 | import threading 16 | except ImportError: 17 | threading = None # type: ignore[assignment] 18 | 19 | 20 | class LogCollector: 21 | def __init__(self) -> None: 22 | if threading is None: 23 | raise NotImplementedError( 24 | "threading module is not available, " 25 | "this panel cannot be used without it" 26 | ) 27 | self.collections: dict[int, list[dict[str, t.Any]]] = {} 28 | 29 | def get_collection(self, thread_id: int | None = None) -> list[dict[str, t.Any]]: 30 | if thread_id is None: 31 | thread_id = threading.get_ident() 32 | if thread_id not in self.collections: 33 | self.collections[thread_id] = [] 34 | return self.collections[thread_id] 35 | 36 | def clear_collection(self, thread_id: int | None = None) -> None: 37 | if thread_id is None: 38 | thread_id = threading.get_ident() 39 | if thread_id in self.collections: 40 | del self.collections[thread_id] 41 | 42 | def collect(self, item: dict[str, t.Any], thread_id: int | None = None) -> None: 43 | self.get_collection(thread_id).append(item) 44 | 45 | 46 | class ThreadTrackingHandler(logging.Handler): 47 | def __init__(self, collector: LogCollector) -> None: 48 | logging.Handler.__init__(self) 49 | self.collector = collector 50 | 51 | def emit(self, record: logging.LogRecord) -> None: 52 | try: 53 | message = record.getMessage() 54 | except Exception: 55 | message = "[Could not get log message]" 56 | 57 | self.collector.collect( 58 | { 59 | "message": message, 60 | "time": datetime.datetime.fromtimestamp(record.created), 61 | "level": record.levelname, 62 | "file": record.pathname, 63 | "line": record.lineno, 64 | "channel": record.name, 65 | } 66 | ) 67 | 68 | 69 | collector = LogCollector() 70 | logging_handler = ThreadTrackingHandler(collector) 71 | logging.root.addHandler(logging_handler) 72 | 73 | 74 | class LoggingPanel(Panel): 75 | nav_title = "Logging" 76 | title = "Log messages" 77 | template = "panels/logging.html" 78 | 79 | def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: 80 | super().__init__(*args, **kwargs) 81 | self._records: dict[t.Any, list[dict[str, t.Any]]] = {} 82 | 83 | @property 84 | def nav_subtitle(self) -> str: 85 | stats = self.get_stats() 86 | record_count = len(stats["records"]) 87 | return f"{record_count} message{pluralize(record_count)}" 88 | 89 | async def process_request(self, request: Request) -> Response: 90 | if is_coroutine(request["route"].endpoint): 91 | self.thread_id = threading.get_ident() 92 | else: 93 | self.thread_id = await run_in_threadpool(threading.get_ident) 94 | 95 | collector.clear_collection(thread_id=self.thread_id) 96 | return await super().process_request(request) 97 | 98 | async def generate_stats(self, request: Request, response: Response) -> Stats: 99 | records = collector.get_collection(thread_id=self.thread_id) 100 | self._records[self.thread_id] = records 101 | collector.clear_collection(thread_id=self.thread_id) 102 | return {"records": records} 103 | -------------------------------------------------------------------------------- /debug_toolbar/panels/profiling.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from fastapi import Request, Response 4 | from pyinstrument import Profiler 5 | from starlette.concurrency import run_in_threadpool 6 | 7 | from debug_toolbar.panels import Panel 8 | from debug_toolbar.types import Stats 9 | from debug_toolbar.utils import is_coroutine 10 | 11 | 12 | class ProfilingPanel(Panel): 13 | title = "Profiling" 14 | template = "panels/profiling.html" 15 | 16 | async def process_request(self, request: Request) -> Response: 17 | self.profiler = Profiler(**self.toolbar.settings.PROFILER_OPTIONS) 18 | is_async = is_coroutine(request["route"].endpoint) 19 | 20 | async def call(f: t.Callable) -> None: 21 | await run_in_threadpool(f) if not is_async else f() 22 | 23 | await call(self.profiler.start) 24 | 25 | try: 26 | response = await super().process_request(request) 27 | finally: 28 | await call(self.profiler.stop) 29 | return response 30 | 31 | async def generate_stats(self, request: Request, response: Response) -> Stats: 32 | return {"content": self.profiler.output_html()} 33 | -------------------------------------------------------------------------------- /debug_toolbar/panels/redirects.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from fastapi import Request, Response, status 4 | 5 | from debug_toolbar.panels import Panel 6 | from debug_toolbar.responses import StreamingHTMLResponse 7 | 8 | 9 | class RedirectsPanel(Panel): 10 | has_content = False 11 | nav_title = "Intercept redirects" 12 | template = "redirect.html" 13 | 14 | async def process_request(self, request: Request) -> Response: 15 | response = await super().process_request(request) 16 | 17 | if 300 <= response.status_code < 400: 18 | if redirect_to := response.headers.get("Location"): 19 | 20 | async def content() -> t.AsyncGenerator[str, None]: 21 | yield self.render( 22 | redirect_to=redirect_to, 23 | status_code=response.status_code, 24 | ) 25 | 26 | response = StreamingHTMLResponse( 27 | content=content(), 28 | status_code=status.HTTP_200_OK, 29 | ) 30 | return response 31 | -------------------------------------------------------------------------------- /debug_toolbar/panels/request.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as t 4 | 5 | from fastapi import Request, Response 6 | 7 | from debug_toolbar.panels import Panel 8 | from debug_toolbar.types import Stats 9 | from debug_toolbar.utils import get_name_from_obj 10 | 11 | 12 | class RequestPanel(Panel): 13 | title = "Request" 14 | template = "panels/request.html" 15 | 16 | @property 17 | def nav_subtitle(self) -> str: 18 | return get_name_from_obj(self.endpoint) 19 | 20 | async def generate_stats(self, request: Request, response: Response) -> Stats: 21 | self.endpoint = request["endpoint"] 22 | stats: dict[str, t.Any] = {"request": request} 23 | 24 | if hasattr(self, "_form"): 25 | stats["form"] = await request.form() 26 | 27 | if "session" in request.scope: 28 | stats["session"] = request.session 29 | return stats 30 | -------------------------------------------------------------------------------- /debug_toolbar/panels/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, Response 2 | 3 | from debug_toolbar.panels import Panel 4 | from debug_toolbar.types import Stats 5 | 6 | 7 | class RoutesPanel(Panel): 8 | title = "Routes" 9 | template = "panels/routes.html" 10 | 11 | async def generate_stats(self, request: Request, response: Response) -> Stats: 12 | return { 13 | "routes": request.app.routes, 14 | "endpoint": request["endpoint"], 15 | } 16 | -------------------------------------------------------------------------------- /debug_toolbar/panels/settings.py: -------------------------------------------------------------------------------- 1 | from debug_toolbar.panels import Panel 2 | 3 | 4 | class SettingsPanel(Panel): 5 | title = "Settings" 6 | template = "panels/settings.html" 7 | -------------------------------------------------------------------------------- /debug_toolbar/panels/sql.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import html 4 | import json 5 | import re 6 | import typing as t 7 | from collections import defaultdict 8 | 9 | import sqlparse 10 | from fastapi import Request, Response 11 | from fastapi.encoders import jsonable_encoder 12 | from pydantic_extra_types.color import Color 13 | from sqlparse import tokens as T 14 | 15 | from debug_toolbar.panels import Panel 16 | from debug_toolbar.types import ServerTiming, Stats 17 | from debug_toolbar.utils import color_generator 18 | 19 | __all__ = ["SQLPanel", "parse_sql", "raw_sql"] 20 | 21 | Stream = t.Generator[t.Tuple[T._TokenType, str], None, None] 22 | 23 | 24 | class BoldKeywordFilter: 25 | def process(self, stream: Stream) -> Stream: 26 | for token_type, value in stream: 27 | if is_keyword := token_type in T.Keyword: 28 | yield T.Text, "" 29 | yield token_type, html.escape(value) 30 | if is_keyword: 31 | yield T.Text, "" 32 | 33 | 34 | class RawFilter: 35 | def process(self, stream: Stream) -> Stream: 36 | for token_type, value in stream: 37 | if token_type not in T.Number and token_type != T.String.Single: 38 | yield token_type, value 39 | 40 | 41 | def simplify(sql: str) -> str: 42 | expr = r"SELECT (...........*?) FROM" 43 | return re.sub(expr, "SELECT ••• FROM", sql) 44 | 45 | 46 | class FilterStack(sqlparse.engine.FilterStack): 47 | def run(self, sql: str) -> str: 48 | self.postprocess.append(sqlparse.filters.SerializerUnicode()) 49 | return "".join(super().run(sql)) 50 | 51 | 52 | def parse_sql(sql: str, aligned_indent: bool = False) -> str: 53 | stack = FilterStack() 54 | 55 | if aligned_indent: 56 | stack.enable_grouping() 57 | stack.stmtprocess.append( 58 | sqlparse.filters.AlignedIndentFilter(char=" ", n="11 | The Debug Toolbar has intercepted a redirect to the above URL for debug viewing purposes. You can click the above link to continue with the redirect as normal. 12 |
13 |", 89 | description=( 90 | "The toolbar searches for this string in the HTML " 91 | "and inserts itself just before." 92 | ), 93 | ) 94 | SHOW_COLLAPSE: bool = Field( 95 | False, 96 | description="If changed to `True`, the toolbar will be collapsed by default.", 97 | ) 98 | ROOT_TAG_EXTRA_ATTRS: str = Field( 99 | "", 100 | description=( 101 | "This setting is injected in the root template div " 102 | "in order to avoid conflicts with client-side frameworks" 103 | ), 104 | ) 105 | RESULTS_CACHE_SIZE: int = Field( 106 | 25, 107 | description="The toolbar keeps up to this many results in memory.", 108 | ) 109 | PROFILER_OPTIONS: t.Dict[str, t.Any] = Field( 110 | {"interval": 0.0001}, 111 | description="A list of arguments can be supplied to the Profiler.", 112 | ) 113 | SETTINGS: t.Sequence[BaseSettings] = Field( 114 | [], 115 | description=( 116 | "pydantic's `BaseSettings` instances to be " 117 | "displayed on the `SettingsPanel`." 118 | ), 119 | ) 120 | LOGGING_COLORS: t.Dict[str, Color] = Field( 121 | { 122 | "CRITICAL": Color("rgba(255, 0, 0, .4)"), 123 | "ERROR": Color("rgba(255, 0, 0, .2)"), 124 | "WARNING": Color("rgba(255, 165, 0, .2)"), 125 | "INFO": Color("rgba(135, 206, 235, .2)"), 126 | "DEBUG": Color("rgba(128, 128, 128, .2)"), 127 | }, 128 | description="Color palette used to apply colors based on the log level.", 129 | ) 130 | SQL_WARNING_THRESHOLD: int = Field( 131 | 500, 132 | description=( 133 | "The SQL panel highlights queries that took more that this amount of " 134 | "time, in milliseconds, to execute." 135 | ), 136 | ) 137 | 138 | def __init__(self, **settings: t.Any) -> None: 139 | super().__init__(**settings) 140 | loaders = self.JINJA_LOADERS + [PackageLoader("debug_toolbar", "templates")] 141 | self.JINJA_ENV.loader = ChoiceLoader(loaders) 142 | self.JINJA_ENV.trim_blocks = True 143 | self.JINJA_ENV.lstrip_blocks = True 144 | 145 | for extension in self.JINJA_EXTENSIONS: 146 | self.JINJA_ENV.add_extension(extension) 147 | 148 | @model_validator(mode="before") 149 | def ci(cls, data: dict): 150 | return {k.upper(): v for k, v in data.items()} 151 | -------------------------------------------------------------------------------- /debug_toolbar/statics/css/print.css: -------------------------------------------------------------------------------- 1 | #fastDebug { 2 | display: none !important; 3 | } 4 | -------------------------------------------------------------------------------- /debug_toolbar/statics/css/toolbar.css: -------------------------------------------------------------------------------- 1 | /* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */ 2 | #fastDebug { 3 | color: #000; 4 | background: #fff; 5 | } 6 | #fastDebug, 7 | #fastDebug div, 8 | #fastDebug span, 9 | #fastDebug applet, 10 | #fastDebug object, 11 | #fastDebug iframe, 12 | #fastDebug h1, 13 | #fastDebug h2, 14 | #fastDebug h3, 15 | #fastDebug h4, 16 | #fastDebug h5, 17 | #fastDebug h6, 18 | #fastDebug p, 19 | #fastDebug blockquote, 20 | #fastDebug pre, 21 | #fastDebug a, 22 | #fastDebug abbr, 23 | #fastDebug acronym, 24 | #fastDebug address, 25 | #fastDebug big, 26 | #fastDebug cite, 27 | #fastDebug code, 28 | #fastDebug del, 29 | #fastDebug dfn, 30 | #fastDebug em, 31 | #fastDebug font, 32 | #fastDebug img, 33 | #fastDebug ins, 34 | #fastDebug kbd, 35 | #fastDebug q, 36 | #fastDebug s, 37 | #fastDebug samp, 38 | #fastDebug small, 39 | #fastDebug strike, 40 | #fastDebug strong, 41 | #fastDebug sub, 42 | #fastDebug sup, 43 | #fastDebug tt, 44 | #fastDebug var, 45 | #fastDebug b, 46 | #fastDebug u, 47 | #fastDebug i, 48 | #fastDebug center, 49 | #fastDebug dl, 50 | #fastDebug dt, 51 | #fastDebug dd, 52 | #fastDebug ol, 53 | #fastDebug ul, 54 | #fastDebug li, 55 | #fastDebug fieldset, 56 | #fastDebug form, 57 | #fastDebug label, 58 | #fastDebug legend, 59 | #fastDebug table, 60 | #fastDebug caption, 61 | #fastDebug tbody, 62 | #fastDebug tfoot, 63 | #fastDebug thead, 64 | #fastDebug tr, 65 | #fastDebug th, 66 | #fastDebug td, 67 | #fastDebug button { 68 | margin: 0; 69 | padding: 0; 70 | min-width: 0; 71 | width: auto; 72 | border: 0; 73 | outline: 0; 74 | font-size: 12px; 75 | line-height: 1.5em; 76 | color: #000; 77 | vertical-align: baseline; 78 | background-color: transparent; 79 | font-family: sans-serif; 80 | text-align: left; 81 | text-shadow: none; 82 | white-space: normal; 83 | transition: none; 84 | } 85 | 86 | #fastDebug button { 87 | background-color: #eee; 88 | background-image: linear-gradient(to bottom, #eee, #cccccc); 89 | border: 1px solid #ccc; 90 | border-bottom: 1px solid #bbb; 91 | border-radius: 3px; 92 | color: #333; 93 | line-height: 1; 94 | padding: 0 8px; 95 | text-align: center; 96 | text-shadow: 0 1px 0 #eee; 97 | } 98 | 99 | #fastDebug button:hover { 100 | background-color: #ddd; 101 | background-image: linear-gradient(to bottom, #ddd, #bbb); 102 | border-color: #bbb; 103 | border-bottom-color: #999; 104 | cursor: pointer; 105 | text-shadow: 0 1px 0 #ddd; 106 | } 107 | 108 | #fastDebug button:active { 109 | border: 1px solid #aaa; 110 | border-bottom: 1px solid #888; 111 | box-shadow: 112 | inset 0 0 5px 2px #aaa, 113 | 0 1px 0 0 #eee; 114 | } 115 | 116 | #fastDebug #fastDebugToolbar { 117 | background-color: #111; 118 | width: 220px; 119 | z-index: 100000000; 120 | position: fixed; 121 | top: 0; 122 | bottom: 0; 123 | right: 0; 124 | opacity: 0.9; 125 | overflow-y: auto; 126 | } 127 | 128 | #fastDebug #fastDebugToolbar small { 129 | color: #999; 130 | } 131 | 132 | #fastDebug #fastDebugToolbar ul { 133 | margin: 0; 134 | padding: 0; 135 | list-style: none; 136 | } 137 | 138 | #fastDebug #fastDebugToolbar li { 139 | border-bottom: 1px solid #222; 140 | color: #fff; 141 | display: block; 142 | font-weight: bold; 143 | float: none; 144 | margin: 0; 145 | padding: 0; 146 | position: relative; 147 | width: auto; 148 | } 149 | 150 | #fastDebug #fastDebugToolbar input[type="checkbox"] { 151 | float: right; 152 | margin: 10px; 153 | } 154 | 155 | #fastDebug #fastDebugToolbar li > a, 156 | #fastDebug #fastDebugToolbar li > div.fastdt-contentless { 157 | font-weight: normal; 158 | font-style: normal; 159 | text-decoration: none; 160 | display: block; 161 | font-size: 16px; 162 | padding: 10px 10px 5px 25px; 163 | color: #fff; 164 | } 165 | #fastDebug #fastDebugToolbar li > div.fastdt-disabled { 166 | font-style: italic; 167 | color: #999; 168 | } 169 | 170 | #fastDebug #fastDebugToolbar li a:hover { 171 | color: #111; 172 | background-color: #ffc; 173 | } 174 | 175 | #fastDebug #fastDebugToolbar li.fastdt-active { 176 | background: #333; 177 | } 178 | 179 | #fastDebug #fastDebugToolbar li.fastdt-active:before { 180 | content: "▶"; 181 | position: absolute; 182 | left: 0; 183 | top: 50%; 184 | transform: translateY(-50%); 185 | color: #eee; 186 | font-size: 150%; 187 | } 188 | 189 | #fastDebug #fastDebugToolbar li.fastdt-active a:hover { 190 | color: #b36a60; 191 | background-color: transparent; 192 | } 193 | 194 | #fastDebug #fastDebugToolbar li small { 195 | font-size: 12px; 196 | color: #999; 197 | font-style: normal; 198 | text-decoration: none; 199 | } 200 | 201 | #fastDebug #fastDebugToolbarHandle { 202 | position: fixed; 203 | transform: translateY(-100%) rotate(-90deg); 204 | transform-origin: right bottom; 205 | background-color: #fff; 206 | border: 1px solid #111; 207 | border-bottom: 0; 208 | top: 0; 209 | right: 0; 210 | z-index: 100000000; 211 | opacity: 0.75; 212 | } 213 | 214 | #fastDebug #fastShowToolBarButton { 215 | padding: 0 5px; 216 | border: 4px solid #fff; 217 | border-bottom-width: 0; 218 | color: #fff; 219 | font-size: 22px; 220 | font-weight: bold; 221 | background: #000; 222 | opacity: 0.5; 223 | } 224 | 225 | #fastDebug #fastShowToolBarButton:hover { 226 | background-color: #111; 227 | border-color: #ffe761; 228 | cursor: move; 229 | opacity: 1; 230 | } 231 | 232 | #fastDebug #fastShowToolBarLogo img { 233 | height: 1.8em; 234 | vertical-align: text-bottom; 235 | } 236 | 237 | #fastDebug code { 238 | display: block; 239 | font-family: Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", monospace; 240 | font-size: 12px; 241 | white-space: pre; 242 | overflow: auto; 243 | } 244 | 245 | #fastDebug .fastdt-panelContent { 246 | position: fixed; 247 | margin: 0; 248 | top: 0; 249 | right: 220px; 250 | bottom: 0; 251 | left: 0px; 252 | background-color: #eee; 253 | color: #666; 254 | z-index: 100000000; 255 | } 256 | 257 | #fastDebug .fastdt-panelContent > div { 258 | border-bottom: 1px solid #ddd; 259 | } 260 | 261 | #fastDebug .fastDebugPanelTitle { 262 | position: absolute; 263 | background-color: #ffc; 264 | color: #666; 265 | padding-left: 20px; 266 | top: 0; 267 | right: 0; 268 | left: 0; 269 | height: 50px; 270 | } 271 | 272 | #fastDebug .fastDebugPanelTitle code { 273 | display: inline; 274 | font-size: inherit; 275 | } 276 | 277 | #fastDebug .fastDebugPanelContent { 278 | position: absolute; 279 | top: 50px; 280 | right: 0; 281 | bottom: 0; 282 | left: 0; 283 | height: auto; 284 | padding: 5px 0 0 20px; 285 | } 286 | 287 | #fastDebug .fastDebugPanelContent .fastdt-loader { 288 | margin: 80px auto; 289 | border: 6px solid white; 290 | border-radius: 50%; 291 | border-top: 6px solid #ffe761; 292 | width: 38px; 293 | height: 38px; 294 | animation: spin 2s linear infinite; 295 | } 296 | 297 | @keyframes spin { 298 | 0% { 299 | transform: rotate(0deg); 300 | } 301 | 100% { 302 | transform: rotate(360deg); 303 | } 304 | } 305 | 306 | #fastDebug .fastDebugPanelContent .fastdt-scroll { 307 | height: 100%; 308 | overflow: auto; 309 | display: block; 310 | padding: 0 10px 0 0; 311 | } 312 | 313 | #fastDebug h3 { 314 | font-size: 24px; 315 | font-weight: normal; 316 | line-height: 50px; 317 | } 318 | 319 | #fastDebug h4 { 320 | font-size: 20px; 321 | font-weight: bold; 322 | margin-top: 0.8em; 323 | } 324 | 325 | #fastDebug .fastdt-panelContent table { 326 | border: 1px solid #ccc; 327 | border-collapse: collapse; 328 | width: 100%; 329 | background-color: #fff; 330 | display: table; 331 | margin-top: 0.8em; 332 | overflow: auto; 333 | } 334 | #fastDebug .fastdt-panelContent tbody > tr:nth-child(odd) { 335 | background-color: #f5f5f5; 336 | } 337 | #fastDebug .fastdt-panelContent tbody td, 338 | #fastDebug .fastdt-panelContent tbody th { 339 | vertical-align: top; 340 | padding: 2px 3px; 341 | } 342 | #fastDebug .fastdt-panelContent tbody td.fastdt-time { 343 | text-align: center; 344 | } 345 | 346 | #fastDebug .fastdt-panelContent thead th { 347 | padding: 1px 6px 1px 3px; 348 | text-align: left; 349 | font-weight: bold; 350 | font-size: 14px; 351 | white-space: nowrap; 352 | } 353 | #fastDebug .fastdt-panelContent tbody th { 354 | width: 12em; 355 | text-align: right; 356 | color: #666; 357 | padding-right: 0.5em; 358 | } 359 | 360 | #fastDebug .fastTemplateContext { 361 | background-color: #fff; 362 | } 363 | 364 | #fastDebug .fastdt-panelContent .fastDebugClose { 365 | position: absolute; 366 | top: 4px; 367 | right: 15px; 368 | height: 16px; 369 | width: 16px; 370 | line-height: 16px; 371 | padding: 5px; 372 | border: 6px solid #ddd; 373 | border-radius: 50%; 374 | background: #fff; 375 | color: #ddd; 376 | text-align: center; 377 | font-weight: 900; 378 | font-size: 20px; 379 | box-sizing: content-box; 380 | } 381 | 382 | #fastDebug .fastdt-panelContent .fastDebugClose:hover { 383 | background: #c0695d; 384 | } 385 | 386 | #fastDebug .fastdt-panelContent dt, 387 | #fastDebug .fastdt-panelContent dd { 388 | display: block; 389 | } 390 | 391 | #fastDebug .fastdt-panelContent dt { 392 | margin-top: 0.75em; 393 | } 394 | 395 | #fastDebug .fastdt-panelContent dd { 396 | margin-left: 10px; 397 | } 398 | 399 | #fastDebug a.toggleTemplate { 400 | padding: 4px; 401 | background-color: #bbb; 402 | border-radius: 3px; 403 | } 404 | 405 | #fastDebug a.toggleTemplate:hover { 406 | padding: 4px; 407 | background-color: #444; 408 | color: #ffe761; 409 | border-radius: 3px; 410 | } 411 | 412 | #fastDebug .fastDebugCollapsed { 413 | color: #333; 414 | } 415 | 416 | #fastDebug .fastDebugUncollapsed { 417 | color: #333; 418 | } 419 | 420 | #fastDebug .fastUnselected { 421 | display: none; 422 | } 423 | 424 | #fastDebug tr.fastSelected { 425 | display: table-row; 426 | } 427 | 428 | #fastDebug .fastDebugSql { 429 | overflow-wrap: anywhere; 430 | } 431 | 432 | #fastDebug .fastSQLDetailsDiv tbody th { 433 | text-align: left; 434 | } 435 | 436 | #fastDebug span.fastDebugLineChart { 437 | background-color: #777; 438 | height: 3px; 439 | position: absolute; 440 | bottom: 0; 441 | top: 0; 442 | left: 0; 443 | display: block; 444 | z-index: 1000000001; 445 | } 446 | #fastDebug span.fastDebugLineChartWarning { 447 | background-color: #900; 448 | } 449 | 450 | #fastDebug .highlight { 451 | color: #000; 452 | } 453 | #fastDebug .highlight .err { 454 | color: #000; 455 | } /* Error */ 456 | #fastDebug .highlight .g { 457 | color: #000; 458 | } /* Generic */ 459 | #fastDebug .highlight .k { 460 | color: #000; 461 | font-weight: bold; 462 | } /* Keyword */ 463 | #fastDebug .highlight .o { 464 | color: #000; 465 | } /* Operator */ 466 | #fastDebug .highlight .n { 467 | color: #000; 468 | } /* Name */ 469 | #fastDebug .highlight .mi { 470 | color: #000; 471 | font-weight: bold; 472 | } /* Literal.Number.Integer */ 473 | #fastDebug .highlight .l { 474 | color: #000; 475 | } /* Literal */ 476 | #fastDebug .highlight .x { 477 | color: #000; 478 | } /* Other */ 479 | #fastDebug .highlight .p { 480 | color: #000; 481 | } /* Punctuation */ 482 | #fastDebug .highlight .m { 483 | color: #000; 484 | font-weight: bold; 485 | } /* Literal.Number */ 486 | #fastDebug .highlight .s { 487 | color: #333; 488 | } /* Literal.String */ 489 | #fastDebug .highlight .w { 490 | color: #888888; 491 | } /* Text.Whitespace */ 492 | #fastDebug .highlight .il { 493 | color: #000; 494 | font-weight: bold; 495 | } /* Literal.Number.Integer.Long */ 496 | #fastDebug .highlight .na { 497 | color: #333; 498 | } /* Name.Attribute */ 499 | #fastDebug .highlight .nt { 500 | color: #000; 501 | font-weight: bold; 502 | } /* Name.Tag */ 503 | #fastDebug .highlight .nv { 504 | color: #333; 505 | } /* Name.Variable */ 506 | #fastDebug .highlight .s2 { 507 | color: #333; 508 | } /* Literal.String.Double */ 509 | #fastDebug .highlight .cp { 510 | color: #333; 511 | } /* Comment.Preproc */ 512 | 513 | #fastDebug svg.fastDebugLineChart { 514 | width: 100%; 515 | height: 1.5em; 516 | } 517 | 518 | #fastDebug svg.fastDebugLineChartWarning rect { 519 | fill: #900; 520 | } 521 | 522 | #fastDebug svg.fastDebugLineChart line { 523 | stroke: #94b24d; 524 | } 525 | 526 | #fastDebug .fastDebugRowWarning .fastdt-time { 527 | color: red; 528 | } 529 | #fastDebug .fastdt-panelContent table .fastdt-toggle { 530 | width: 14px; 531 | padding-top: 3px; 532 | } 533 | #fastDebug .fastdt-panelContent table .fastdt-actions { 534 | min-width: 70px; 535 | white-space: nowrap; 536 | } 537 | #fastDebug .fastdt-color:after { 538 | content: "\00a0"; 539 | } 540 | #fastDebug .fastToggleSwitch { 541 | box-sizing: content-box; 542 | padding: 0; 543 | border: 1px solid #999; 544 | border-radius: 0; 545 | width: 12px; 546 | color: #777; 547 | background: linear-gradient(to bottom, #fff, #dcdcdc); 548 | } 549 | #fastDebug .fastNoToggleSwitch { 550 | height: 14px; 551 | width: 14px; 552 | display: inline-block; 553 | } 554 | 555 | #fastDebug .fastSQLDetailsDiv { 556 | margin-top: 0.8em; 557 | } 558 | #fastDebug pre { 559 | white-space: pre-wrap; 560 | color: #555; 561 | border: 1px solid #ccc; 562 | border-collapse: collapse; 563 | background-color: #fff; 564 | display: block; 565 | overflow: auto; 566 | padding: 2px 3px; 567 | margin-bottom: 3px; 568 | font-family: Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", monospace; 569 | } 570 | #fastDebug .fastdt-stack span { 571 | color: #000; 572 | font-weight: bold; 573 | } 574 | #fastDebug .fastdt-stack span.fastdt-type, 575 | #fastDebug .fastdt-stack pre.fastdt-locals, 576 | #fastDebug .fastdt-stack pre.fastdt-locals span { 577 | color: #777; 578 | font-weight: normal; 579 | } 580 | #fastDebug .fastdt-stack span.fastdt-code { 581 | font-weight: normal; 582 | } 583 | #fastDebug .fastdt-stack pre.fastdt-locals { 584 | margin: 0 27px 27px 27px; 585 | } 586 | 587 | #fastDebug .fastdt-width-20 { 588 | width: 20%; 589 | } 590 | #fastDebug .fastdt-width-30 { 591 | width: 30%; 592 | } 593 | #fastDebug .fastdt-width-60 { 594 | width: 60%; 595 | } 596 | #fastDebug .fastdt-max-height-100 { 597 | max-height: 100%; 598 | } 599 | #fastDebug .fastdt-highlighted { 600 | background-color: lightgrey; 601 | } 602 | .fastdt-hidden { 603 | display: none; 604 | } 605 | 606 | #ProfilingPanel .fastDebugPanelContent, 607 | #ProfilingPanel .fastDebugPanelContent .fastdt-scroll { 608 | padding: 0; 609 | } 610 | iframe#profilingContent { 611 | width: 100%; 612 | height: 100%; 613 | } 614 | 615 | #VersionsPanel tbody td code { 616 | display: inline; 617 | } 618 | #VersionsPanel tbody td span.fast-date { 619 | font-size: 10px; 620 | } 621 | #VersionsPanel .fastdt-hidden { 622 | display: none !important; 623 | } 624 | 625 | #fastDebug .fastSQLDetailsDiv code { 626 | display: inline; 627 | font-size: 11px; 628 | } 629 | 630 | #HeadersPanel table { 631 | table-layout: fixed; 632 | } 633 | #HeadersPanel tbody td { 634 | overflow-wrap: break-word; 635 | } 636 | 637 | #RequestPanel tbody td:last-child code { 638 | word-break: break-word; 639 | white-space: normal; 640 | } 641 | 642 | #RoutesPanel tbody td code, 643 | #LoggingPanel tbody td code { 644 | word-break: break-word; 645 | white-space: pre-wrap; 646 | } 647 | -------------------------------------------------------------------------------- /debug_toolbar/statics/img/icon-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | -------------------------------------------------------------------------------- /debug_toolbar/statics/img/icon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | -------------------------------------------------------------------------------- /debug_toolbar/statics/js/redirect.js: -------------------------------------------------------------------------------- 1 | document.getElementById("redirect_to").focus(); 2 | -------------------------------------------------------------------------------- /debug_toolbar/statics/js/refresh.js: -------------------------------------------------------------------------------- 1 | const refresh = (function () { 2 | function getCookie(name) { 3 | const parts = `; ${document.cookie}`.split(`; ${name}=`); 4 | if (parts.length === 2) return parts.pop().split(";").shift(); 5 | } 6 | let lastCookie = getCookie("dtRefresh"); 7 | const fastDebug = document.getElementById("fastDebug"); 8 | 9 | return function () { 10 | const dtCookie = getCookie("dtRefresh"); 11 | 12 | if (dtCookie && dtCookie !== lastCookie) { 13 | lastCookie = dtCookie; 14 | const toolbar = JSON.parse(decodeURIComponent(lastCookie)); 15 | 16 | Object.entries(toolbar.panels).map(([id, subtitle]) => { 17 | document.getElementById(`fastdt-${id}`).querySelector("small").textContent = subtitle; 18 | }); 19 | fastDebug.querySelectorAll(".fastDebugPanelContent").forEach(function (e) { 20 | e.querySelector(".fastdt-scroll").innerHTML = ""; 21 | 22 | if (e.querySelector(".fastdt-loader") === null) { 23 | const loader = document.createElement("div"); 24 | loader.className = "fastdt-loader"; 25 | e.prepend(loader); 26 | } 27 | }); 28 | fastDebug.setAttribute("data-store-id", toolbar.storeId); 29 | } 30 | }; 31 | })(); 32 | 33 | window.setInterval(refresh, 100); 34 | -------------------------------------------------------------------------------- /debug_toolbar/statics/js/timer.js: -------------------------------------------------------------------------------- 1 | import { $$ } from "./utils.js"; 2 | 3 | function insertBrowserTiming() { 4 | const timing = performance.timing, 5 | timingOffset = timing.navigationStart, 6 | totalTime = timing.loadEventEnd - timingOffset; 7 | 8 | function getLeft(stat) { 9 | return ((timing[stat] - timingOffset) / totalTime) * 100.0; 10 | } 11 | function getCSSWidth(stat, endStat) { 12 | let width = ((timing[endStat] - timing[stat]) / totalTime) * 100.0; 13 | // Calculate relative percent 14 | width = (100.0 * width) / (100.0 - getLeft(stat)); 15 | return width < 1 ? "2px" : `${width || 0}%`; 16 | } 17 | function addRow(tbody, stat, endStat) { 18 | const row = document.createElement("tr"); 19 | if (endStat) { 20 | // Render a start through end bar 21 | row.innerHTML = ` 22 |
23 |
31 |
`; 35 | row.querySelector("rect").setAttribute("width", getCSSWidth(stat, endStat)); 36 | } else { 37 | // Render a point in time 38 | row.innerHTML = ` 39 |
40 |
48 |
`; 49 | row.querySelector("rect").setAttribute("width", 2); 50 | } 51 | row.querySelector("rect").setAttribute("x", getLeft(stat)); 52 | tbody.appendChild(row); 53 | } 54 | 55 | const browserTiming = document.getElementById("fastDebugBrowserTiming"); 56 | // Determine if the browser timing section has already been rendered. 57 | if (browserTiming.classList.contains("fastdt-hidden")) { 58 | const tbody = document.getElementById("fastDebugBrowserTimingTableBody"); 59 | // This is a reasonably complete and ordered set of 60 | // timing periods (2 params) and events (1 param) 61 | addRow(tbody, "domainLookupStart", "domainLookupEnd"); 62 | addRow(tbody, "connectStart", "connectEnd"); 63 | addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd 64 | addRow(tbody, "responseStart", "responseEnd"); 65 | addRow(tbody, "domLoading", "domComplete"); // Spans the events below 66 | addRow(tbody, "domInteractive"); 67 | addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); 68 | addRow(tbody, "loadEventStart", "loadEventEnd"); 69 | browserTiming.classList.remove("fastdt-hidden"); 70 | } 71 | } 72 | 73 | const fastDebug = document.getElementById("fastDebug"); 74 | // Insert the browser timing now since it's possible for this 75 | // script to miss the initial panel load event. 76 | insertBrowserTiming(); 77 | $$.onPanelRender(fastDebug, "TimerPanel", insertBrowserTiming); 78 | -------------------------------------------------------------------------------- /debug_toolbar/statics/js/toolbar.js: -------------------------------------------------------------------------------- 1 | import { $$, ajax } from "./utils.js"; 2 | 3 | function onKeyDown(event) { 4 | if (event.keyCode === 27) { 5 | fastdt.hide_one_level(); 6 | } 7 | } 8 | 9 | const fastdt = { 10 | handleDragged: false, 11 | init() { 12 | const fastDebug = document.getElementById("fastDebug"); 13 | $$.show(fastDebug); 14 | $$.on(document.getElementById("fastDebugPanelList"), "click", "li a", function (event) { 15 | event.preventDefault(); 16 | if (!this.className) { 17 | return; 18 | } 19 | const panelId = this.className; 20 | const current = document.getElementById(panelId); 21 | if ($$.visible(current)) { 22 | fastdt.hide_panels(); 23 | } else { 24 | fastdt.hide_panels(); 25 | 26 | $$.show(current); 27 | this.parentElement.classList.add("fastdt-active"); 28 | 29 | const inner = current.querySelector(".fastDebugPanelContent .fastdt-scroll"), 30 | store_id = fastDebug.dataset.storeId; 31 | if (store_id && inner.children.length === 0) { 32 | const url = new URL(fastDebug.dataset.renderPanelUrl, window.location); 33 | url.searchParams.append("store_id", store_id); 34 | url.searchParams.append("panel_id", panelId); 35 | ajax(url).then(function (data) { 36 | inner.previousElementSibling.remove(); // Remove AJAX loader 37 | inner.innerHTML = data.content; 38 | $$.executeScripts(data.scripts); 39 | $$.applyStyles(inner); 40 | $$.loadScripts(inner); 41 | fastDebug.dispatchEvent( 42 | new CustomEvent("fastdt.panel.render", { 43 | detail: { panelId: panelId }, 44 | }), 45 | ); 46 | }); 47 | } else { 48 | fastDebug.dispatchEvent( 49 | new CustomEvent("fastdt.panel.render", { 50 | detail: { panelId: panelId }, 51 | }), 52 | ); 53 | } 54 | } 55 | }); 56 | $$.on(fastDebug, "click", ".fastDebugClose", function () { 57 | fastdt.hide_one_level(); 58 | }); 59 | $$.on(fastDebug, "click", ".fastDebugPanelButton input[type=checkbox]", function () { 60 | fastdt.cookie.set(this.dataset.cookie, this.checked ? "on" : "off", { 61 | path: "/", 62 | expires: 10, 63 | }); 64 | }); 65 | 66 | // Used by the SQL and template panels 67 | $$.on(fastDebug, "click", ".remoteCall", function (event) { 68 | event.preventDefault(); 69 | 70 | let url; 71 | const ajax_data = {}; 72 | 73 | if (this.tagName === "BUTTON") { 74 | const form = this.closest("form"); 75 | url = this.formAction; 76 | ajax_data.method = form.method.toUpperCase(); 77 | ajax_data.body = new FormData(form); 78 | } else if (this.tagName === "A") { 79 | url = this.href; 80 | } 81 | 82 | ajax(url, ajax_data).then(function (data) { 83 | const win = document.getElementById("fastDebugWindow"); 84 | win.innerHTML = data.content; 85 | $$.show(win); 86 | }); 87 | }); 88 | 89 | // Used by the cache and SQL panels 90 | $$.on(fastDebug, "click", ".fastToggleSwitch", function () { 91 | const id = this.dataset.toggleId; 92 | const toggleOpen = "+"; 93 | const toggleClose = "-"; 94 | const open_me = this.textContent === toggleOpen; 95 | const name = this.dataset.toggleName; 96 | const container = document.getElementById(name + "_" + id); 97 | container.querySelectorAll(".fastDebugCollapsed").forEach(function (e) { 98 | $$.toggle(e, open_me); 99 | }); 100 | container.querySelectorAll(".fastDebugUncollapsed").forEach(function (e) { 101 | $$.toggle(e, !open_me); 102 | }); 103 | const self = this; 104 | this.closest(".fastDebugPanelContent") 105 | .querySelectorAll(".fastToggleDetails_" + id) 106 | .forEach(function (e) { 107 | if (open_me) { 108 | e.classList.add("fastSelected"); 109 | e.classList.remove("fastUnselected"); 110 | self.textContent = toggleClose; 111 | } else { 112 | e.classList.remove("fastSelected"); 113 | e.classList.add("fastUnselected"); 114 | self.textContent = toggleOpen; 115 | } 116 | const switch_ = e.querySelector(".fastToggleSwitch"); 117 | if (switch_) { 118 | switch_.textContent = self.textContent; 119 | } 120 | }); 121 | }); 122 | 123 | document.getElementById("fastHideToolBarButton").addEventListener("click", function (event) { 124 | event.preventDefault(); 125 | fastdt.hide_toolbar(); 126 | }); 127 | document.getElementById("fastShowToolBarButton").addEventListener("click", function () { 128 | if (!fastdt.handleDragged) { 129 | fastdt.show_toolbar(); 130 | } 131 | }); 132 | let startPageY, baseY; 133 | const handle = document.getElementById("fastDebugToolbarHandle"); 134 | function onHandleMove(event) { 135 | // Chrome can send spurious mousemove events, so don't do anything unless the 136 | // cursor really moved. Otherwise, it will be impossible to expand the toolbar 137 | // due to fastdt.handleDragged being set to true. 138 | if (fastdt.handleDragged || event.pageY !== startPageY) { 139 | let top = baseY + event.pageY; 140 | 141 | if (top < 0) { 142 | top = 0; 143 | } else if (top + handle.offsetHeight > window.innerHeight) { 144 | top = window.innerHeight - handle.offsetHeight; 145 | } 146 | 147 | handle.style.top = top + "px"; 148 | fastdt.handleDragged = true; 149 | } 150 | } 151 | document.getElementById("fastShowToolBarButton").addEventListener("mousedown", function (event) { 152 | event.preventDefault(); 153 | startPageY = event.pageY; 154 | baseY = handle.offsetTop - startPageY; 155 | document.addEventListener("mousemove", onHandleMove); 156 | }); 157 | document.addEventListener("mouseup", function (event) { 158 | document.removeEventListener("mousemove", onHandleMove); 159 | if (fastdt.handleDragged) { 160 | event.preventDefault(); 161 | localStorage.setItem("fastdt.top", handle.offsetTop); 162 | requestAnimationFrame(function () { 163 | fastdt.handleDragged = false; 164 | }); 165 | fastdt.ensure_handle_visibility(); 166 | } 167 | }); 168 | const show = localStorage.getItem("fastdt.show") || fastDebug.dataset.defaultShow; 169 | if (show === "true") { 170 | fastdt.show_toolbar(); 171 | } else { 172 | fastdt.hide_toolbar(); 173 | } 174 | }, 175 | hide_panels() { 176 | const fastDebug = document.getElementById("fastDebug"); 177 | $$.hide(document.getElementById("fastDebugWindow")); 178 | fastDebug.querySelectorAll(".fastdt-panelContent").forEach(function (e) { 179 | $$.hide(e); 180 | }); 181 | document.querySelectorAll("#fastDebugToolbar li").forEach(function (e) { 182 | e.classList.remove("fastdt-active"); 183 | }); 184 | }, 185 | ensure_handle_visibility() { 186 | const handle = document.getElementById("fastDebugToolbarHandle"); 187 | // set handle position 188 | const handleTop = Math.min(localStorage.getItem("fastdt.top") || 0, window.innerHeight - handle.offsetWidth); 189 | handle.style.top = handleTop + "px"; 190 | }, 191 | hide_toolbar() { 192 | fastdt.hide_panels(); 193 | 194 | $$.hide(document.getElementById("fastDebugToolbar")); 195 | 196 | const handle = document.getElementById("fastDebugToolbarHandle"); 197 | $$.show(handle); 198 | fastdt.ensure_handle_visibility(); 199 | window.addEventListener("resize", fastdt.ensure_handle_visibility); 200 | document.removeEventListener("keydown", onKeyDown); 201 | 202 | localStorage.setItem("fastdt.show", "false"); 203 | }, 204 | hide_one_level() { 205 | const win = document.getElementById("fastDebugWindow"); 206 | if ($$.visible(win)) { 207 | $$.hide(win); 208 | } else { 209 | const toolbar = document.getElementById("fastDebugToolbar"); 210 | if (toolbar.querySelector("li.fastdt-active")) { 211 | fastdt.hide_panels(); 212 | } else { 213 | fastdt.hide_toolbar(); 214 | } 215 | } 216 | }, 217 | show_toolbar() { 218 | document.addEventListener("keydown", onKeyDown); 219 | $$.hide(document.getElementById("fastDebugToolbarHandle")); 220 | $$.show(document.getElementById("fastDebugToolbar")); 221 | localStorage.setItem("fastdt.show", "true"); 222 | window.removeEventListener("resize", fastdt.ensure_handle_visibility); 223 | }, 224 | cookie: { 225 | set(key, value, options) { 226 | options = options || {}; 227 | 228 | if (typeof options.expires === "number") { 229 | const days = options.expires, 230 | t = (options.expires = new Date()); 231 | t.setDate(t.getDate() + days); 232 | } 233 | 234 | document.cookie = [ 235 | encodeURIComponent(key) + "=" + String(value), 236 | options.expires ? "; expires=" + options.expires.toUTCString() : "", 237 | options.path ? "; path=" + options.path : "", 238 | options.domain ? "; domain=" + options.domain : "", 239 | options.secure ? "; secure" : "", 240 | "sameSite" in options ? "; sameSite=" + options.samesite : "; sameSite=Lax", 241 | ].join(""); 242 | 243 | return value; 244 | }, 245 | }, 246 | }; 247 | window.fastdt = { 248 | show_toolbar: fastdt.show_toolbar, 249 | hide_toolbar: fastdt.hide_toolbar, 250 | init: fastdt.init, 251 | close: fastdt.hide_one_level, 252 | cookie: fastdt.cookie, 253 | }; 254 | 255 | if (document.readyState !== "loading") { 256 | fastdt.init(); 257 | } else { 258 | document.addEventListener("DOMContentLoaded", fastdt.init); 259 | } 260 | -------------------------------------------------------------------------------- /debug_toolbar/statics/js/utils.js: -------------------------------------------------------------------------------- 1 | const $$ = { 2 | on(root, eventName, selector, fn) { 3 | root.addEventListener(eventName, function (event) { 4 | const target = event.target.closest(selector); 5 | if (root.contains(target)) { 6 | fn.call(target, event); 7 | } 8 | }); 9 | }, 10 | onPanelRender(root, panelId, fn) { 11 | /* 12 | This is a helper function to attach a handler for a `fastdt.panel.render` 13 | event of a specific panel. 14 | 15 | root: The container element that the listener should be attached to. 16 | panelId: The Id of the panel. 17 | fn: A function to execute when the event is triggered. 18 | */ 19 | root.addEventListener("fastdt.panel.render", function (event) { 20 | if (event.detail.panelId === panelId) { 21 | fn.call(event); 22 | } 23 | }); 24 | }, 25 | show(element) { 26 | element.classList.remove("fastdt-hidden"); 27 | }, 28 | hide(element) { 29 | element.classList.add("fastdt-hidden"); 30 | }, 31 | toggle(element, value) { 32 | if (value) { 33 | $$.show(element); 34 | } else { 35 | $$.hide(element); 36 | } 37 | }, 38 | visible(element) { 39 | return !element.classList.contains("fastdt-hidden"); 40 | }, 41 | executeScripts(scripts) { 42 | scripts.forEach(function (script) { 43 | const el = document.createElement("script"); 44 | el.type = "module"; 45 | el.src = script; 46 | el.async = true; 47 | document.head.appendChild(el); 48 | }); 49 | }, 50 | applyStyles(container) { 51 | /* 52 | * Given a container element, apply styles set via data-fastdt-styles attribute. 53 | * The format is data-fastdt-styles="styleName1:value;styleName2:value2" 54 | * The style names should use the CSSStyleDeclaration camel cased names. 55 | */ 56 | container.querySelectorAll("[data-fastdt-styles]").forEach(function (element) { 57 | const styles = element.dataset.fastdtStyles || ""; 58 | styles.split(";").forEach(function (styleText) { 59 | const styleKeyPair = styleText.split(":"); 60 | if (styleKeyPair.length === 2) { 61 | const name = styleKeyPair[0].trim(); 62 | const value = styleKeyPair[1].trim(); 63 | element.style[name] = value; 64 | } 65 | }); 66 | }); 67 | }, 68 | loadScripts(container) { 69 | container.querySelectorAll("script").forEach(function (element) { 70 | const script = document.createElement("script"); 71 | Array.from(element.attributes).forEach((attr) => script.setAttribute(attr.name, attr.value)); 72 | script.appendChild(document.createTextNode(element.innerHTML)); 73 | element.parentNode.replaceChild(script, element); 74 | }); 75 | }, 76 | truncatechars(text, n) { 77 | return text.length > n ? `${text.slice(0, n)}...` : text; 78 | }, 79 | }; 80 | 81 | function ajax(url, init) { 82 | init = Object.assign({ credentials: "same-origin" }, init); 83 | return fetch(url, init) 84 | .then(function (response) { 85 | if (response.ok) { 86 | return response.json(); 87 | } 88 | return Promise.reject(new Error(response.status + ": " + response.statusText)); 89 | }) 90 | .catch(function (error) { 91 | const win = document.getElementById("fastDebugWindow"); 92 | win.innerHTML = ` 93 |
`;
97 | $$.show(win);
98 | throw error;
99 | });
100 | }
101 |
102 | function ajaxForm(element) {
103 | const form = element.closest("form");
104 | const url = new URL(form.action);
105 | const formData = new FormData(form);
106 | for (const [name, value] of formData.entries()) {
107 | url.searchParams.append(name, value);
108 | }
109 | const ajaxData = {
110 | method: form.method.toUpperCase(),
111 | };
112 | return ajax(url, ajaxData);
113 | }
114 |
115 | export { $$, ajax, ajaxForm };
116 |
--------------------------------------------------------------------------------
/debug_toolbar/statics/js/versions.js:
--------------------------------------------------------------------------------
1 | import { $$ } from "./utils.js";
2 |
3 | function pypiIndex() {
4 | const loader = document.getElementById("VersionsPanel").querySelector(".fastdt-loader");
5 |
6 | function versionInfo(releases, version) {
7 | return `
8 | ${version}
9 |
10 | ${new Date(releases[version]).toLocaleDateString()}
11 | `;
12 | }
13 | function link(url) {
14 | return `
15 |
16 | ${$$.truncatechars(url, 40)}
17 | `;
18 | }
19 | function render(rowVersion, data) {
20 | if (rowVersion.textContent !== data.version) {
21 | rowVersion.parentNode.style.backgroundColor = loader.nextElementSibling.dataset.warningColor;
22 | }
23 | if (data.releases[rowVersion.textContent] !== null) {
24 | rowVersion.innerHTML = versionInfo(data.releases, rowVersion.textContent);
25 | }
26 | const lastVersion = rowVersion.nextElementSibling;
27 | lastVersion.innerHTML = versionInfo(data.releases, data.version);
28 |
29 | const python = lastVersion.nextElementSibling;
30 | python.innerHTML = data.requires_python;
31 |
32 | const status = python.nextElementSibling;
33 | status.innerHTML = data.status ? data.status.slice(26) : "";
34 |
35 | if (data.home_page) {
36 | status.nextElementSibling.innerHTML = link(data.home_page);
37 | }
38 | }
39 | function getData(pypi) {
40 | return {
41 | version: pypi.info.version,
42 | requires_python: pypi.info.requires_python,
43 | home_page: pypi.info.home_page,
44 | releases: Object.fromEntries(
45 | Object.entries(pypi.releases).map(function ([k, v], i) {
46 | return [k, v.length ? v[0].upload_time : null];
47 | }),
48 | ),
49 | status: pypi.info.classifiers.find(function (classifier) {
50 | return classifier.startsWith("Development Status");
51 | }),
52 | };
53 | }
54 | function updateRow(row) {
55 | return new Promise((resolve) => {
56 | const name = row.firstElementChild.textContent.trim();
57 | const data = JSON.parse(localStorage.getItem(`pypi-${name}`));
58 | const rowVersion = row.children.item(1);
59 |
60 | if (data === null || !(rowVersion.textContent in data.releases)) {
61 | fetch(`https://pypi.org/pypi/${name}/json`).then(function (response) {
62 | if (response.ok) {
63 | response.json().then(function (pypi) {
64 | const data = getData(pypi);
65 |
66 | if (!(rowVersion.textContent in data.releases)) {
67 | data.releases[rowVersion.textContent] = null;
68 | }
69 | localStorage.setItem(`pypi-${name}`, JSON.stringify(data));
70 | render(rowVersion, data);
71 | resolve();
72 | });
73 | }
74 | });
75 | } else {
76 | render(rowVersion, data);
77 | resolve();
78 | }
79 | });
80 | }
81 | if (loader && !loader.getAttribute("lock")) {
82 | loader.setAttribute("lock", true);
83 |
84 | const table = loader.nextElementSibling;
85 | const queryResult = table.querySelectorAll("tbody > tr");
86 | const promises = [];
87 |
88 | for (let i = 0; i < queryResult.length; i++) {
89 | promises.push(updateRow(queryResult[i]));
90 | }
91 | Promise.all(promises).then(() => {
92 | loader.remove();
93 | table.classList.remove("fastdt-hidden");
94 | });
95 | }
96 | }
97 |
98 | const fastDebug = document.getElementById("fastDebug");
99 | pypiIndex();
100 | $$.onPanelRender(fastDebug, "VersionsPanel", pypiIndex);
101 |
--------------------------------------------------------------------------------
/debug_toolbar/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
34 | -------------------------------------------------------------------------------- /debug_toolbar/templates/includes/panel_button.html: -------------------------------------------------------------------------------- 1 |
20 | -------------------------------------------------------------------------------- /debug_toolbar/templates/includes/panel_content.html: -------------------------------------------------------------------------------- 1 | {% if panel.has_content and panel.enabled %} 2 |
12 | {% endif %} 13 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/headers.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Key | 11 |Value | 12 |
---|---|
{{ key|escape }} | 18 |{{ value|escape }} | 19 |
23 | 24 |
25 | 26 |
Key | 34 |Value | 35 |
---|---|
{{ key|escape }} | 41 |{{ value|escape }} | 42 |
46 | 47 |
48 | 49 |
Key | 53 |Value | 54 |
---|---|
{{ key|escape }} | 60 |{{ value|escape }} | 61 |
65 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/logging.html: -------------------------------------------------------------------------------- 1 | {% if records %} 2 |
Level | 6 |Time | 7 |Channel | 8 |Message | 9 |Location | 10 |
---|---|---|---|---|
{{ record.level }} | 16 |{{ record.time.strftime('%H:%M:%S %m/%d/%Y') }} | 17 |{{ record.channel|default('-') }} | 18 |{{ record.message }} |
19 | {{ record.file }}:{{ record.line }} | 20 |
24 | {% else %} 25 |
No messages logged.
26 | {% endif %} 27 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/profiling.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/request.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Name | 7 |Endpoint | 8 |Path params | 9 |
---|---|---|
{{ request.route.name }} | 14 |{{ get_name_from_obj(request.endpoint) }} | 15 |16 | {% if request.path_params %} 17 | {% for k, v in request.path_params.items() %} 18 | {{ k }}={{ v }}{% if not loop.last %}, {% endif %} 19 | {% endfor %} 20 | {% else %} 21 | - 22 | {% endif %} 23 | | 24 |
27 | 28 | {% macro pprint_vars(variables) %} 29 |
Variable | 37 |Value | 38 |
---|---|
{{ key|pprint }} |
44 | {{ value|pprint }} |
45 |
49 | {% endmacro %} 50 | 51 | 52 |
53 | {% if request.cookies %} 54 | {{ pprint_vars(request.cookies.items()) }} 55 | {% else %} 56 |
No cookies
57 | {% endif %} 58 | 59 |
60 | {% if session %} 61 | {{ pprint_vars(session.items()) }} 62 | {% else %} 63 |
No session data
64 | {% endif %} 65 | 66 |
67 | {% if request.query_params %} 68 | {{ pprint_vars(request.query_params.multi_items()) }} 69 | {% else %} 70 |
No GET data
71 | {% endif %} 72 | 73 |
74 | {% if form %} 75 | {{ pprint_vars(form.multi_items()) }} 76 | {% else %} 77 |
No POST data
78 | {% endif %} 79 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/routes.html: -------------------------------------------------------------------------------- 1 |
Name | 5 |Methods | 6 |Path | 7 |Endpoint | 8 |Description | 9 |
---|---|---|---|---|
{{ route.name|default('', true) }} | 15 |{% if route.methods %}{{ route.methods|sort|join(', ') }}{% endif %} | 16 |{{ route.path }} | 17 |{{ get_name_from_obj(route.endpoint) }} | 18 |{{ route.description }} |
19 |
23 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/settings.html: -------------------------------------------------------------------------------- 1 | {% macro pprint_settings(settings, exclude=None) %} 2 | {% if settings.model_config.title %}
{% endif %} 3 | 4 |
Key | 8 |Value | 9 |
---|---|
{{ key|escape }} | 15 |{{ value|pprint|escape }} |
16 |
20 | {% endmacro %} 21 | 22 | {% for settings in toolbar.settings.SETTINGS %} 23 | {{ pprint_settings(settings) }} 24 | {% endfor %} 25 | 26 | {{ pprint_settings(toolbar.settings, exclude=to_set(['SETTINGS'])) }} 27 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/sql.html: -------------------------------------------------------------------------------- 1 | {% if queries %} 2 |
16 | 17 |
27 | | Query | 28 |Timeline | 29 |Time (ms) | 30 | {# TODO:Action | #} 31 ||
---|---|---|---|---|---|
37 | | 38 | 39 | | 40 |
41 |
42 | {{ query.sql_formatted|safe }}
43 | {{ query.sql_simple|safe }}
44 |
45 | {% if query.sim_count %}
46 |
47 |
48 | {{ query.sim_count }} similar queries.
49 |
50 | {% endif %}
51 | {% if query.dup_count %}
52 |
53 |
54 | Duplicated {{ query.dup_count }} times.
55 |
56 | {% endif %}
57 | |
58 | 59 | 62 | | 63 |64 | {{ '%0.2f'|format(query.duration|float) }} 65 | | 66 ||
69 | |
70 |
71 | {% if query.params %}
74 | Params: Connection: {{ alias }} 73 | |
75 |
79 | {% else %} 80 |
No SQL queries were recorded during this request.
81 | {% endif %} 82 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/timer.html: -------------------------------------------------------------------------------- 1 |
2 |
Resource | 10 |Value | 11 |
---|---|
{{ key|escape }} | 17 |{{ value|escape }} | 18 |
22 | 23 |
Timing attribute | 34 |Timeline | 35 |Milliseconds since navigation start (+length) | 36 |
---|
41 | -------------------------------------------------------------------------------- /debug_toolbar/templates/panels/versions.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Package | 7 |Version | 8 |Latest version | 9 |Python | 10 |Status | 11 |Home page | 12 |
---|---|---|---|---|---|
18 | 19 | {{ package.metadata.name }} 20 | 21 | | 22 |{{ package.version }} |
23 | 24 | | 25 | | 26 | | 27 | |
31 | -------------------------------------------------------------------------------- /debug_toolbar/templates/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | 7 |
14 |