django-devbar 0.3.0__tar.gz

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.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-devbar
3
+ Version: 0.3.0
4
+ Summary: Lightweight performance devbar for Django
5
+ Keywords: django,debug,performance,monitoring,development
6
+ Author: amureki
7
+ Author-email: amureki <oss@amureki.com>
8
+ License-Expression: MIT
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Framework :: Django
18
+ Classifier: Framework :: Django :: 4.2
19
+ Classifier: Framework :: Django :: 5.0
20
+ Classifier: Framework :: Django :: 5.1
21
+ Classifier: Framework :: Django :: 5.2
22
+ Classifier: Framework :: Django :: 6.0
23
+ Classifier: Topic :: Software Development
24
+ Classifier: Intended Audience :: Developers
25
+ Requires-Dist: django>=4.2
26
+ Requires-Python: >=3.10
27
+ Project-URL: Homepage, https://github.com/amureki/django-devbar
28
+ Project-URL: Issues, https://github.com/amureki/django-devbar/issues
29
+ Project-URL: Repository, https://github.com/amureki/django-devbar
30
+ Description-Content-Type: text/markdown
31
+
32
+ # django-devbar
33
+
34
+ Lightweight performance devbar for Django. Shows DB query count, query duration, application time, and detects duplicate queries with visual severity indicators.
35
+
36
+ ![devbar example](https://github.com/amureki/django-devbar/raw/3c6118d4283c211a5d84510de52e1d5c3e5e46e4/docs/devbar-example.svg)
37
+
38
+ ![devbar warning example](https://github.com/amureki/django-devbar/raw/3c6118d4283c211a5d84510de52e1d5c3e5e46e4/docs/devbar-example-warning.svg)
39
+
40
+ > **Note:** This package is experimental and may introduce breaking changes in minor versions.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ # Using uv (recommended)
46
+ uv add --dev django-devbar
47
+
48
+ # Or using pip
49
+ pip install django-devbar
50
+ ```
51
+
52
+ Add to your middleware as early as possible, but after any middleware that encodes the response (e.g., `GZipMiddleware`):
53
+
54
+ ```python
55
+ MIDDLEWARE = [
56
+ "django.middleware.security.SecurityMiddleware",
57
+ "django_devbar.DevBarMiddleware",
58
+ # ...
59
+ ]
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ All settings are optional. Configure via a `DEVBAR` dict in your Django settings:
65
+
66
+ ```python
67
+ DEVBAR = {
68
+ 'POSITION': 'bottom-right', # bottom-right, bottom-left, top-right, top-left
69
+ 'SHOW_BAR': None, # None (default) = follows DEBUG, or True/False to override
70
+ 'SHOW_HEADERS': False, # Add DevBar-* headers to responses
71
+ }
72
+ ```
73
+
74
+ ## Response Headers
75
+
76
+ When `DEVBAR = {'SHOW_HEADERS': True}`, performance metrics are added as HTTP response headers. This is useful for:
77
+
78
+ - **API endpoints** where the HTML overlay can't be displayed
79
+ - **Automated testing** to assert performance metrics (e.g., fail CI if query count exceeds a limit)
80
+ - **Monitoring tools** that can capture and aggregate header values
81
+
82
+ Headers included:
83
+
84
+ | Header | Example | Description |
85
+ |--------|---------|-------------|
86
+ | `DevBar-Query-Count` | `12` | Number of database queries executed |
87
+ | `DevBar-DB-Time` | `87ms` | Total time spent in database queries |
88
+ | `DevBar-App-Time` | `41ms` | Application time (total time minus DB time) |
89
+ | `DevBar-Duplicates` | `3` | Number of duplicate queries detected (only present if duplicates found) |
90
+
91
+ ## Chrome Extension
92
+
93
+ ![Chrome DevTools extension](https://github.com/user-attachments/assets/4288f507-0bd5-4b8c-a3dc-0c4d5b93305f)
94
+
95
+ View Django DevBar metrics directly in Chrome DevTools with the [official extension](https://chromewebstore.google.com/detail/django-devbar/fehcaaopchkbknbdhjadnmehiifdmeid).
96
+ Requires `DEVBAR = {'SHOW_HEADERS': True}`.
97
+
98
+ See [chrome-extension/README.md](chrome-extension/README.md) for more details.
@@ -0,0 +1,67 @@
1
+ # django-devbar
2
+
3
+ Lightweight performance devbar for Django. Shows DB query count, query duration, application time, and detects duplicate queries with visual severity indicators.
4
+
5
+ ![devbar example](https://github.com/amureki/django-devbar/raw/3c6118d4283c211a5d84510de52e1d5c3e5e46e4/docs/devbar-example.svg)
6
+
7
+ ![devbar warning example](https://github.com/amureki/django-devbar/raw/3c6118d4283c211a5d84510de52e1d5c3e5e46e4/docs/devbar-example-warning.svg)
8
+
9
+ > **Note:** This package is experimental and may introduce breaking changes in minor versions.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ # Using uv (recommended)
15
+ uv add --dev django-devbar
16
+
17
+ # Or using pip
18
+ pip install django-devbar
19
+ ```
20
+
21
+ Add to your middleware as early as possible, but after any middleware that encodes the response (e.g., `GZipMiddleware`):
22
+
23
+ ```python
24
+ MIDDLEWARE = [
25
+ "django.middleware.security.SecurityMiddleware",
26
+ "django_devbar.DevBarMiddleware",
27
+ # ...
28
+ ]
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ All settings are optional. Configure via a `DEVBAR` dict in your Django settings:
34
+
35
+ ```python
36
+ DEVBAR = {
37
+ 'POSITION': 'bottom-right', # bottom-right, bottom-left, top-right, top-left
38
+ 'SHOW_BAR': None, # None (default) = follows DEBUG, or True/False to override
39
+ 'SHOW_HEADERS': False, # Add DevBar-* headers to responses
40
+ }
41
+ ```
42
+
43
+ ## Response Headers
44
+
45
+ When `DEVBAR = {'SHOW_HEADERS': True}`, performance metrics are added as HTTP response headers. This is useful for:
46
+
47
+ - **API endpoints** where the HTML overlay can't be displayed
48
+ - **Automated testing** to assert performance metrics (e.g., fail CI if query count exceeds a limit)
49
+ - **Monitoring tools** that can capture and aggregate header values
50
+
51
+ Headers included:
52
+
53
+ | Header | Example | Description |
54
+ |--------|---------|-------------|
55
+ | `DevBar-Query-Count` | `12` | Number of database queries executed |
56
+ | `DevBar-DB-Time` | `87ms` | Total time spent in database queries |
57
+ | `DevBar-App-Time` | `41ms` | Application time (total time minus DB time) |
58
+ | `DevBar-Duplicates` | `3` | Number of duplicate queries detected (only present if duplicates found) |
59
+
60
+ ## Chrome Extension
61
+
62
+ ![Chrome DevTools extension](https://github.com/user-attachments/assets/4288f507-0bd5-4b8c-a3dc-0c4d5b93305f)
63
+
64
+ View Django DevBar metrics directly in Chrome DevTools with the [official extension](https://chromewebstore.google.com/detail/django-devbar/fehcaaopchkbknbdhjadnmehiifdmeid).
65
+ Requires `DEVBAR = {'SHOW_HEADERS': True}`.
66
+
67
+ See [chrome-extension/README.md](chrome-extension/README.md) for more details.
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "django-devbar"
3
+ version = "0.3.0"
4
+ description = "Lightweight performance devbar for Django"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "amureki", email = "oss@amureki.com" },
9
+ ]
10
+ requires-python = ">=3.10"
11
+ dependencies = ["django>=4.2"]
12
+ keywords = ["django", "debug", "performance", "monitoring", "development"]
13
+ classifiers = [
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Framework :: Django",
23
+ "Framework :: Django :: 4.2",
24
+ "Framework :: Django :: 5.0",
25
+ "Framework :: Django :: 5.1",
26
+ "Framework :: Django :: 5.2",
27
+ "Framework :: Django :: 6.0",
28
+ "Topic :: Software Development",
29
+ "Intended Audience :: Developers",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/amureki/django-devbar"
34
+ Repository = "https://github.com/amureki/django-devbar"
35
+ Issues = "https://github.com/amureki/django-devbar/issues"
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.9.16,<0.10.0"]
39
+ build-backend = "uv_build"
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest",
44
+ "pytest-django",
45
+ "ruff",
46
+ ]
47
+
48
+ [tool.pytest]
49
+ pythonpath = ["src", "."]
50
+ testpaths = ["tests"]
51
+ norecursedirs = [".venv", ".git"]
52
+ DJANGO_SETTINGS_MODULE = "tests.settings"
@@ -0,0 +1,3 @@
1
+ from .middleware import DevBarMiddleware
2
+
3
+ __all__ = ["DevBarMiddleware"]
@@ -0,0 +1,33 @@
1
+ from django.conf import settings
2
+
3
+ POSITIONS = {
4
+ "bottom-right": "bottom:0;right:0",
5
+ "bottom-left": "bottom:0;left:0",
6
+ "top-right": "top:0;right:0",
7
+ "top-left": "top:0;left:0",
8
+ }
9
+
10
+
11
+ def get_config():
12
+ return {
13
+ "POSITION": "bottom-right",
14
+ "SHOW_BAR": None, # None = use settings.DEBUG
15
+ "SHOW_HEADERS": False,
16
+ **getattr(settings, "DEVBAR", {}),
17
+ }
18
+
19
+
20
+ def get_position():
21
+ config = get_config()
22
+ key = config["POSITION"]
23
+ return POSITIONS.get(key, POSITIONS["bottom-right"])
24
+
25
+
26
+ def get_show_bar():
27
+ config = get_config()
28
+ show_bar = config["SHOW_BAR"]
29
+ return settings.DEBUG if show_bar is None else show_bar
30
+
31
+
32
+ def get_show_headers():
33
+ return get_config()["SHOW_HEADERS"]
@@ -0,0 +1,123 @@
1
+ import json
2
+ import re
3
+ from contextlib import ExitStack
4
+ from pathlib import Path
5
+ from time import perf_counter
6
+
7
+ from django.db import connections
8
+ from django.template import Context, Engine
9
+
10
+ from . import tracker
11
+ from .conf import (
12
+ get_position,
13
+ get_show_bar,
14
+ get_show_headers,
15
+ )
16
+
17
+ BODY_CLOSE_RE = re.compile(rb"</body\s*>", re.IGNORECASE)
18
+
19
+ _template_engine = Engine(
20
+ dirs=[Path(__file__).parent / "templates"],
21
+ autoescape=True,
22
+ )
23
+
24
+
25
+ class DevBarMiddleware:
26
+ def __init__(self, get_response):
27
+ self.get_response = get_response
28
+
29
+ def __call__(self, request):
30
+ tracker.reset()
31
+ request_start = perf_counter()
32
+
33
+ with ExitStack() as stack:
34
+ for alias in connections:
35
+ stack.enter_context(
36
+ connections[alias].execute_wrapper(tracker.tracking_wrapper)
37
+ )
38
+ response = self.get_response(request)
39
+
40
+ total_time = (perf_counter() - request_start) * 1000
41
+ stats = tracker.get_stats()
42
+
43
+ db_time = stats["duration"]
44
+ python_time = max(0, total_time - db_time)
45
+
46
+ stats["python_time"] = python_time
47
+ stats["total_time"] = total_time
48
+
49
+ level = "warn" if stats["has_duplicates"] else "ok"
50
+
51
+ if get_show_headers():
52
+ self._add_headers(response, stats)
53
+
54
+ if get_show_bar() and self._can_inject(response):
55
+ self._inject_devbar(response, stats, level)
56
+
57
+ return response
58
+
59
+ def _add_headers(self, response, stats):
60
+ response["DevBar-Query-Count"] = str(stats["count"])
61
+ response["DevBar-DB-Time"] = f"{stats['duration']:.0f}ms"
62
+ response["DevBar-App-Time"] = f"{stats['python_time']:.0f}ms"
63
+ if stats["has_duplicates"]:
64
+ response["DevBar-Duplicates"] = str(len(stats["duplicate_queries"]))
65
+
66
+ # Add comprehensive JSON data for browser DevTools extension
67
+ extension_data = {
68
+ "count": stats["count"],
69
+ "db_time": stats["duration"],
70
+ "app_time": stats["python_time"],
71
+ "total_time": stats["total_time"],
72
+ "has_duplicates": stats["has_duplicates"],
73
+ }
74
+ if stats.get("duplicate_queries"):
75
+ extension_data["duplicates"] = stats["duplicate_queries"]
76
+
77
+ response["DevBar-Data"] = json.dumps(extension_data)
78
+
79
+ def _can_inject(self, response):
80
+ if getattr(response, "streaming", False):
81
+ return False
82
+ content_type = response.get("Content-Type", "").lower()
83
+ if "html" not in content_type:
84
+ return False
85
+ if response.get("Content-Encoding"):
86
+ return False
87
+ return hasattr(response, "content")
88
+
89
+ def _inject_devbar(self, response, stats, level):
90
+ content = response.content
91
+ matches = list(BODY_CLOSE_RE.finditer(content))
92
+ if not matches:
93
+ return
94
+
95
+ duplicates_html = self._build_duplicates_html(
96
+ stats.get("duplicate_queries", [])
97
+ )
98
+
99
+ template = _template_engine.get_template("django_devbar/devbar.html")
100
+ html = template.render(
101
+ Context(
102
+ {
103
+ "position": get_position(),
104
+ "level": level,
105
+ "db_time": stats["duration"],
106
+ "app_time": stats["python_time"],
107
+ "query_count": stats["count"],
108
+ "duplicates_html": duplicates_html,
109
+ }
110
+ )
111
+ )
112
+
113
+ payload = html.encode(response.charset or "utf-8")
114
+
115
+ idx = matches[-1].start()
116
+ response.content = content[:idx] + payload + content[idx:]
117
+ response["Content-Length"] = str(len(response.content))
118
+
119
+ def _build_duplicates_html(self, duplicates):
120
+ if not duplicates:
121
+ return ""
122
+ template = _template_engine.get_template("django_devbar/duplicates.html")
123
+ return template.render(Context({"duplicates": duplicates}))
@@ -0,0 +1,92 @@
1
+ <style>
2
+ #django-devbar {
3
+ position: fixed; {{ position }}; z-index: 999999999;
4
+ display: flex; align-items: center; gap: 5px;
5
+ font-family: -apple-system, system-ui, sans-serif;
6
+ font-size: 11px; font-weight: 500;
7
+ padding: 4px 8px; margin: 8px; border-radius: 4px;
8
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
9
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
10
+ transition: all 0.2s ease;
11
+ cursor: default;
12
+ line-height: 1.3;
13
+ background: rgba(20, 20, 20, 0.92);
14
+ color: #f5f5f5;
15
+ }
16
+ #django-devbar.level-warn { border-left: 3px solid #f59e0b; }
17
+ #django-devbar span { opacity: 0.7; }
18
+ #django-devbar strong { opacity: 1; font-weight: 600; }
19
+ #django-devbar details { display: inline; position: relative; }
20
+ #django-devbar summary {
21
+ all: revert;
22
+ color: #f59e0b; font-weight: 600; cursor: pointer; list-style: none;
23
+ }
24
+ #django-devbar summary::-webkit-details-marker { display: none; }
25
+ #django-devbar details[open] > .dup-list {
26
+ position: absolute; bottom: 100%; right: -8px; margin-bottom: 8px;
27
+ background: rgba(20, 20, 20, 0.92); border-radius: 4px; padding: 6px;
28
+ width: min(500px, 90vw); max-height: 200px; overflow-y: auto; overflow-x: hidden;
29
+ box-sizing: border-box;
30
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
31
+ }
32
+ #django-devbar .dup-list details {
33
+ display: block;
34
+ margin: 0 0 6px 0;
35
+ font-size: 11px;
36
+ color: #f5f5f5;
37
+ }
38
+ #django-devbar .dup-list details:last-child { margin-bottom: 0; }
39
+ #django-devbar .dup-list summary {
40
+ all: revert;
41
+ cursor: pointer;
42
+ list-style: none;
43
+ padding: 6px 8px;
44
+ background: rgba(251, 191, 36, 0.1);
45
+ border-radius: 3px;
46
+ border-left: 2px solid #fbbf24;
47
+ transition: background 0.15s ease;
48
+ }
49
+ #django-devbar .dup-list summary:hover {
50
+ background: rgba(251, 191, 36, 0.2);
51
+ }
52
+ #django-devbar .dup-list summary code {
53
+ white-space: nowrap;
54
+ overflow: hidden;
55
+ text-overflow: ellipsis;
56
+ display: inline-block;
57
+ max-width: 420px;
58
+ vertical-align: bottom;
59
+ }
60
+ #django-devbar .dup-list summary::before {
61
+ content: '▶ ';
62
+ display: inline-block;
63
+ transition: transform 0.15s ease;
64
+ color: #fbbf24;
65
+ font-size: 8px;
66
+ margin-right: 6px;
67
+ }
68
+ #django-devbar .dup-list details[open] > summary::before {
69
+ transform: rotate(90deg);
70
+ }
71
+ #django-devbar .dup-list code {
72
+ all: revert;
73
+ color: #fbbf24; font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
74
+ overflow-wrap: break-word;
75
+ }
76
+ #django-devbar .dup-list details[open] > code {
77
+ display: block;
78
+ padding: 8px;
79
+ background: rgba(0, 0, 0, 0.3);
80
+ border-radius: 3px;
81
+ margin-top: 6px;
82
+ white-space: pre-wrap;
83
+ line-height: 1.5;
84
+ font-size: 10px;
85
+ }
86
+ @media (max-width: 640px) { #django-devbar { display: none; } }
87
+ </style>
88
+ <div id="django-devbar" class="level-{{ level }}">
89
+ <span>db</span> <strong>{{ db_time|floatformat:0 }}ms</strong> <span>·</span>
90
+ <span>app</span> <strong>{{ app_time|floatformat:0 }}ms</strong> <span>·</span>
91
+ <span>queries</span> <strong>{{ query_count }}</strong>{{ duplicates_html|safe }}
92
+ </div>
@@ -0,0 +1 @@
1
+ {% if duplicates %}<details><summary>({{ duplicates|length }}d)</summary><div class="dup-list">{% for dup in duplicates %}<details><summary><code>{{ dup.sql }}</code></summary><code>{{ dup.sql }}</code></details>{% endfor %}</div></details>{% endif %}
@@ -0,0 +1,61 @@
1
+ from contextvars import ContextVar
2
+ from time import perf_counter
3
+
4
+ _query_count: ContextVar[int] = ContextVar("query_count", default=0)
5
+ _query_duration: ContextVar[float] = ContextVar("query_duration", default=0.0)
6
+ _seen_queries: ContextVar[dict] = ContextVar("seen_queries", default={})
7
+ _duplicate_log: ContextVar[list] = ContextVar("duplicate_log", default=[])
8
+
9
+
10
+ def reset():
11
+ _query_count.set(0)
12
+ _query_duration.set(0.0)
13
+ _seen_queries.set({})
14
+ _duplicate_log.set([])
15
+
16
+
17
+ def get_stats():
18
+ return {
19
+ "count": _query_count.get(),
20
+ "duration": _query_duration.get(),
21
+ "has_duplicates": bool(_duplicate_log.get()),
22
+ "duplicate_queries": _duplicate_log.get(),
23
+ }
24
+
25
+
26
+ def _hash_params(params):
27
+ try:
28
+ return hash(tuple(params)) if params else 0
29
+ except TypeError:
30
+ return hash(str(params))
31
+
32
+
33
+ def _record(sql, params, duration):
34
+ _query_count.set(_query_count.get() + 1)
35
+ _query_duration.set(_query_duration.get() + duration)
36
+
37
+ seen = _seen_queries.get()
38
+ params_hash = _hash_params(params)
39
+
40
+ if sql in seen:
41
+ if params_hash in seen[sql]:
42
+ param_str = str(params)
43
+ if len(param_str) > 200:
44
+ param_str = param_str[:200] + "..."
45
+
46
+ duplicates = _duplicate_log.get()
47
+ duplicates.append(
48
+ {"sql": sql, "params": param_str, "duration": round(duration, 2)}
49
+ )
50
+ else:
51
+ seen[sql].add(params_hash)
52
+ else:
53
+ seen[sql] = {params_hash}
54
+
55
+
56
+ def tracking_wrapper(execute, sql, params, many, context):
57
+ start = perf_counter()
58
+ try:
59
+ return execute(sql, params, many, context)
60
+ finally:
61
+ _record(sql, params, (perf_counter() - start) * 1000)