litestar-debug-toolbar 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. debug_toolbar/__init__.py +35 -0
  2. debug_toolbar/core/__init__.py +19 -0
  3. debug_toolbar/core/config.py +71 -0
  4. debug_toolbar/core/context.py +104 -0
  5. debug_toolbar/core/panel.py +147 -0
  6. debug_toolbar/core/panels/__init__.py +31 -0
  7. debug_toolbar/core/panels/alerts.py +379 -0
  8. debug_toolbar/core/panels/cache.py +425 -0
  9. debug_toolbar/core/panels/flamegraph.py +125 -0
  10. debug_toolbar/core/panels/headers.py +373 -0
  11. debug_toolbar/core/panels/logging.py +95 -0
  12. debug_toolbar/core/panels/memory/__init__.py +7 -0
  13. debug_toolbar/core/panels/memory/base.py +57 -0
  14. debug_toolbar/core/panels/memory/memray.py +224 -0
  15. debug_toolbar/core/panels/memory/panel.py +184 -0
  16. debug_toolbar/core/panels/memory/tracemalloc.py +142 -0
  17. debug_toolbar/core/panels/profiling.py +389 -0
  18. debug_toolbar/core/panels/request.py +50 -0
  19. debug_toolbar/core/panels/response.py +43 -0
  20. debug_toolbar/core/panels/settings.py +230 -0
  21. debug_toolbar/core/panels/templates.py +250 -0
  22. debug_toolbar/core/panels/timer.py +73 -0
  23. debug_toolbar/core/panels/versions.py +62 -0
  24. debug_toolbar/core/storage.py +93 -0
  25. debug_toolbar/core/toolbar.py +221 -0
  26. debug_toolbar/extras/__init__.py +5 -0
  27. debug_toolbar/extras/advanced_alchemy/__init__.py +10 -0
  28. debug_toolbar/extras/advanced_alchemy/panel.py +587 -0
  29. debug_toolbar/litestar/__init__.py +15 -0
  30. debug_toolbar/litestar/config.py +84 -0
  31. debug_toolbar/litestar/middleware.py +373 -0
  32. debug_toolbar/litestar/panels/__init__.py +8 -0
  33. debug_toolbar/litestar/panels/events.py +210 -0
  34. debug_toolbar/litestar/panels/routes.py +41 -0
  35. debug_toolbar/litestar/plugin.py +96 -0
  36. debug_toolbar/litestar/routes/__init__.py +7 -0
  37. debug_toolbar/litestar/routes/handlers.py +2422 -0
  38. debug_toolbar/py.typed +1 -0
  39. litestar_debug_toolbar-0.2.0.dist-info/METADATA +325 -0
  40. litestar_debug_toolbar-0.2.0.dist-info/RECORD +41 -0
  41. litestar_debug_toolbar-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,35 @@
1
+ """Debug Toolbar - Async-native debug toolbar for Python ASGI applications.
2
+
3
+ This package provides a framework-agnostic debug toolbar with optional integrations
4
+ for popular frameworks like Litestar.
5
+
6
+ Basic usage with core components:
7
+ from debug_toolbar import DebugToolbar, DebugToolbarConfig
8
+
9
+ For Litestar integration:
10
+ from debug_toolbar.litestar import DebugToolbarPlugin, LitestarDebugToolbarConfig
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from debug_toolbar.core import (
16
+ DebugToolbar,
17
+ DebugToolbarConfig,
18
+ Panel,
19
+ RequestContext,
20
+ ToolbarStorage,
21
+ get_request_context,
22
+ set_request_context,
23
+ )
24
+
25
+ __all__ = [
26
+ "DebugToolbar",
27
+ "DebugToolbarConfig",
28
+ "Panel",
29
+ "RequestContext",
30
+ "ToolbarStorage",
31
+ "get_request_context",
32
+ "set_request_context",
33
+ ]
34
+
35
+ __version__ = "0.1.0"
@@ -0,0 +1,19 @@
1
+ """Core debug toolbar components - Framework-agnostic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from debug_toolbar.core.config import DebugToolbarConfig
6
+ from debug_toolbar.core.context import RequestContext, get_request_context, set_request_context
7
+ from debug_toolbar.core.panel import Panel
8
+ from debug_toolbar.core.storage import ToolbarStorage
9
+ from debug_toolbar.core.toolbar import DebugToolbar
10
+
11
+ __all__ = [
12
+ "DebugToolbar",
13
+ "DebugToolbarConfig",
14
+ "Panel",
15
+ "RequestContext",
16
+ "ToolbarStorage",
17
+ "get_request_context",
18
+ "set_request_context",
19
+ ]
@@ -0,0 +1,71 @@
1
+ """Configuration system for the debug toolbar."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Literal
8
+
9
+ if TYPE_CHECKING:
10
+ from debug_toolbar.core.panel import Panel
11
+
12
+
13
+ @dataclass
14
+ class DebugToolbarConfig:
15
+ """Configuration for the debug toolbar.
16
+
17
+ Attributes:
18
+ enabled: Whether the toolbar is enabled. Defaults to True.
19
+ panels: List of panel classes or import paths to include.
20
+ intercept_redirects: Whether to intercept redirects for debugging.
21
+ show_toolbar_callback: Optional callback to determine if toolbar should be shown.
22
+ insert_before: HTML tag to insert toolbar before. Defaults to "</body>".
23
+ max_request_history: Maximum number of requests to store in history.
24
+ api_path: URL path prefix for toolbar API endpoints.
25
+ static_path: URL path prefix for static assets.
26
+ allowed_hosts: List of allowed hosts. Empty list means all hosts.
27
+ extra_panels: Additional panels to add beyond defaults.
28
+ exclude_panels: Panel names to exclude from defaults.
29
+ memory_backend: Memory profiling backend. "auto" selects best available.
30
+ panel_display_depth: Max depth for nested data rendering. Defaults to 10.
31
+ panel_display_max_items: Max items to show in arrays/objects. Defaults to 100.
32
+ panel_display_max_string: Max string length before truncation. Defaults to 1000.
33
+ """
34
+
35
+ enabled: bool = True
36
+ panels: Sequence[str | type[Panel]] = field(
37
+ default_factory=lambda: [
38
+ "debug_toolbar.core.panels.timer.TimerPanel",
39
+ "debug_toolbar.core.panels.request.RequestPanel",
40
+ "debug_toolbar.core.panels.response.ResponsePanel",
41
+ "debug_toolbar.core.panels.logging.LoggingPanel",
42
+ "debug_toolbar.core.panels.versions.VersionsPanel",
43
+ ]
44
+ )
45
+ intercept_redirects: bool = False
46
+ show_toolbar_callback: Callable[..., bool] | None = None
47
+ insert_before: str = "</body>"
48
+ max_request_history: int = 50
49
+ api_path: str = "/_debug_toolbar"
50
+ static_path: str = "/_debug_toolbar/static"
51
+ allowed_hosts: Sequence[str] = field(default_factory=list)
52
+ extra_panels: Sequence[str | type[Panel]] = field(default_factory=list)
53
+ exclude_panels: Sequence[str] = field(default_factory=list)
54
+ memory_backend: Literal["tracemalloc", "memray", "auto"] = "auto"
55
+ panel_display_depth: int = 10
56
+ panel_display_max_items: int = 100
57
+ panel_display_max_string: int = 1000
58
+
59
+ def get_all_panels(self) -> list[str | type[Panel]]:
60
+ """Get all panels including extras, excluding excluded panels."""
61
+ all_panels = list(self.panels) + list(self.extra_panels)
62
+ if not self.exclude_panels:
63
+ return all_panels
64
+
65
+ excluded = set(self.exclude_panels)
66
+ return [
67
+ p
68
+ for p in all_panels
69
+ if (isinstance(p, str) and p.split(".")[-1] not in excluded)
70
+ or (isinstance(p, type) and p.__name__ not in excluded)
71
+ ]
@@ -0,0 +1,104 @@
1
+ """Request context management using contextvars for async-safe data propagation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+ from uuid import UUID, uuid4
9
+
10
+ _request_context: ContextVar[RequestContext | None] = ContextVar("request_context", default=None)
11
+
12
+
13
+ @dataclass
14
+ class RequestContext:
15
+ """Request-scoped context for debug toolbar data collection.
16
+
17
+ This context is stored in a contextvar and is accessible throughout the
18
+ request lifecycle without passing it explicitly through the call stack.
19
+
20
+ Attributes:
21
+ request_id: Unique identifier for this request.
22
+ panel_data: Dictionary of data collected by panels, keyed by panel_id.
23
+ timing_data: Dictionary of timing measurements.
24
+ metadata: Additional metadata about the request.
25
+ """
26
+
27
+ request_id: UUID = field(default_factory=uuid4)
28
+ panel_data: dict[str, dict[str, Any]] = field(default_factory=dict)
29
+ timing_data: dict[str, float] = field(default_factory=dict)
30
+ metadata: dict[str, Any] = field(default_factory=dict)
31
+
32
+ def store_panel_data(self, panel_id: str, key: str, value: Any) -> None:
33
+ """Store data for a specific panel.
34
+
35
+ Args:
36
+ panel_id: The panel's identifier.
37
+ key: The data key.
38
+ value: The data value.
39
+ """
40
+ if panel_id not in self.panel_data:
41
+ self.panel_data[panel_id] = {}
42
+ self.panel_data[panel_id][key] = value
43
+
44
+ def get_panel_data(self, panel_id: str) -> dict[str, Any]:
45
+ """Get all data for a specific panel.
46
+
47
+ Args:
48
+ panel_id: The panel's identifier.
49
+
50
+ Returns:
51
+ Dictionary of panel data, or empty dict if no data exists.
52
+ """
53
+ return self.panel_data.get(panel_id, {})
54
+
55
+ def record_timing(self, name: str, duration: float) -> None:
56
+ """Record a timing measurement.
57
+
58
+ Args:
59
+ name: The name of the timing measurement.
60
+ duration: The duration in seconds.
61
+ """
62
+ self.timing_data[name] = duration
63
+
64
+ def get_timing(self, name: str) -> float | None:
65
+ """Get a timing measurement.
66
+
67
+ Args:
68
+ name: The name of the timing measurement.
69
+
70
+ Returns:
71
+ The duration in seconds, or None if not recorded.
72
+ """
73
+ return self.timing_data.get(name)
74
+
75
+
76
+ def get_request_context() -> RequestContext | None:
77
+ """Get the current request context.
78
+
79
+ Returns:
80
+ The current RequestContext, or None if no context is set.
81
+ """
82
+ return _request_context.get()
83
+
84
+
85
+ def set_request_context(context: RequestContext | None) -> None:
86
+ """Set the current request context.
87
+
88
+ Args:
89
+ context: The RequestContext to set, or None to clear.
90
+ """
91
+ _request_context.set(context)
92
+
93
+
94
+ def ensure_request_context() -> RequestContext:
95
+ """Get or create a request context.
96
+
97
+ Returns:
98
+ The current RequestContext, creating a new one if none exists.
99
+ """
100
+ ctx = get_request_context()
101
+ if ctx is None:
102
+ ctx = RequestContext()
103
+ set_request_context(ctx)
104
+ return ctx
@@ -0,0 +1,147 @@
1
+ """Base panel class for debug toolbar panels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
+
8
+ if TYPE_CHECKING:
9
+ from debug_toolbar.core.context import RequestContext
10
+ from debug_toolbar.core.toolbar import DebugToolbar
11
+
12
+
13
+ class Panel(ABC):
14
+ """Abstract base class for debug toolbar panels.
15
+
16
+ Panels are responsible for collecting and displaying specific types of
17
+ debug information. Each panel should override the abstract methods to
18
+ provide its functionality.
19
+
20
+ Class Attributes:
21
+ panel_id: Unique identifier for the panel. Defaults to class name.
22
+ title: Display title shown in the toolbar.
23
+ template: Template name for rendering the panel content.
24
+ has_content: Whether this panel has detailed content.
25
+ nav_title: Short title for the toolbar navigation.
26
+ nav_subtitle: Subtitle shown in the toolbar navigation.
27
+ """
28
+
29
+ panel_id: ClassVar[str] = ""
30
+ title: ClassVar[str] = ""
31
+ template: ClassVar[str] = ""
32
+ has_content: ClassVar[bool] = True
33
+ nav_title: ClassVar[str] = ""
34
+ nav_subtitle: ClassVar[str] = ""
35
+
36
+ __slots__ = ("_enabled", "_toolbar")
37
+
38
+ def __init__(self, toolbar: DebugToolbar) -> None:
39
+ """Initialize the panel.
40
+
41
+ Args:
42
+ toolbar: The parent DebugToolbar instance.
43
+ """
44
+ self._toolbar = toolbar
45
+ self._enabled = True
46
+
47
+ @classmethod
48
+ def get_panel_id(cls) -> str:
49
+ """Get the panel's unique identifier.
50
+
51
+ Returns:
52
+ The panel_id class variable, or the class name if not set.
53
+ """
54
+ return cls.panel_id or cls.__name__
55
+
56
+ @property
57
+ def enabled(self) -> bool:
58
+ """Check if the panel is enabled."""
59
+ return self._enabled
60
+
61
+ @enabled.setter
62
+ def enabled(self, value: bool) -> None:
63
+ """Set whether the panel is enabled."""
64
+ self._enabled = value
65
+
66
+ @abstractmethod
67
+ async def generate_stats(self, context: RequestContext) -> dict[str, Any]:
68
+ """Generate statistics for this panel.
69
+
70
+ This method is called during request processing to collect data.
71
+
72
+ Args:
73
+ context: The current request context.
74
+
75
+ Returns:
76
+ Dictionary of statistics data.
77
+ """
78
+ ...
79
+
80
+ async def process_request(self, context: RequestContext) -> None:
81
+ """Process the request phase.
82
+
83
+ Override this to perform actions at the start of request processing.
84
+
85
+ Args:
86
+ context: The current request context.
87
+ """
88
+
89
+ async def process_response(self, context: RequestContext) -> None:
90
+ """Process the response phase.
91
+
92
+ Override this to perform actions at the end of request processing.
93
+
94
+ Args:
95
+ context: The current request context.
96
+ """
97
+
98
+ def get_stats(self, context: RequestContext) -> dict[str, Any]:
99
+ """Get stored stats from context.
100
+
101
+ Args:
102
+ context: The current request context.
103
+
104
+ Returns:
105
+ Dictionary of panel stats.
106
+ """
107
+ return context.get_panel_data(self.get_panel_id())
108
+
109
+ def record_stats(self, context: RequestContext, stats: dict[str, Any]) -> None:
110
+ """Record stats to the context.
111
+
112
+ Args:
113
+ context: The current request context.
114
+ stats: Dictionary of stats to store.
115
+ """
116
+ panel_id = self.get_panel_id()
117
+ for key, value in stats.items():
118
+ context.store_panel_data(panel_id, key, value)
119
+
120
+ def generate_server_timing(self, context: RequestContext) -> dict[str, float]:
121
+ """Generate Server-Timing header data.
122
+
123
+ Override this to contribute to the Server-Timing header.
124
+
125
+ Args:
126
+ context: The current request context.
127
+
128
+ Returns:
129
+ Dictionary mapping metric names to durations in seconds.
130
+ """
131
+ return {}
132
+
133
+ def get_nav_title(self) -> str:
134
+ """Get the navigation title.
135
+
136
+ Returns:
137
+ The nav_title class variable, or title if not set.
138
+ """
139
+ return self.nav_title or self.title
140
+
141
+ def get_nav_subtitle(self) -> str:
142
+ """Get the navigation subtitle.
143
+
144
+ Returns:
145
+ The nav_subtitle class variable.
146
+ """
147
+ return self.nav_subtitle
@@ -0,0 +1,31 @@
1
+ """Built-in panels for the async debug toolbar."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from debug_toolbar.core.panels.alerts import AlertsPanel
6
+ from debug_toolbar.core.panels.cache import CachePanel
7
+ from debug_toolbar.core.panels.headers import HeadersPanel
8
+ from debug_toolbar.core.panels.logging import LoggingPanel
9
+ from debug_toolbar.core.panels.memory import MemoryPanel
10
+ from debug_toolbar.core.panels.profiling import ProfilingPanel
11
+ from debug_toolbar.core.panels.request import RequestPanel
12
+ from debug_toolbar.core.panels.response import ResponsePanel
13
+ from debug_toolbar.core.panels.settings import SettingsPanel
14
+ from debug_toolbar.core.panels.templates import TemplatesPanel
15
+ from debug_toolbar.core.panels.timer import TimerPanel
16
+ from debug_toolbar.core.panels.versions import VersionsPanel
17
+
18
+ __all__ = [
19
+ "AlertsPanel",
20
+ "CachePanel",
21
+ "HeadersPanel",
22
+ "LoggingPanel",
23
+ "MemoryPanel",
24
+ "ProfilingPanel",
25
+ "RequestPanel",
26
+ "ResponsePanel",
27
+ "SettingsPanel",
28
+ "TemplatesPanel",
29
+ "TimerPanel",
30
+ "VersionsPanel",
31
+ ]