django-explain-errors 0.8.0__tar.gz → 0.9.1__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.
- {django_explain_errors-0.8.0/django_explain_errors.egg-info → django_explain_errors-0.9.1}/PKG-INFO +35 -36
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/README.md +34 -35
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1/django_explain_errors.egg-info}/PKG-INFO +35 -36
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/debug_page.py +94 -5
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/rag/indexer.py +14 -8
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/setup.py +1 -1
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_debug_page.py +62 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_rag.py +143 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/LICENSE +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/MANIFEST.in +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/SOURCES.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/dependency_links.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/requires.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/top_level.txt +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/client.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/management/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/management/commands/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/management/commands/build_error_index.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/middleware.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/rag/__init__.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/rag/retriever.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/rag/store.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/sanitize.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/throttle.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/tracebacks.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/setup.cfg +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_build_error_index.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_client.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_eval_fixtures.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_eval_judge.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_eval_run.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_language.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_middleware.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_print_stdout.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_sanitize.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_signals.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_throttle.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_tracebacks.py +0 -0
- {django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/tests/test_truncation.py +0 -0
{django_explain_errors-0.8.0/django_explain_errors.egg-info → django_explain_errors-0.9.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.
|
|
5
5
|
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
6
|
Author: topunix
|
|
@@ -42,37 +42,31 @@ Dynamic: requires-dist
|
|
|
42
42
|
Dynamic: requires-python
|
|
43
43
|
Dynamic: summary
|
|
44
44
|
|
|
45
|
-
# Django Explain Errors
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
rate limited.
|
|
71
|
-
|
|
72
|
-

|
|
73
|
-
|
|
74
|
-
*Real `gpt-4o-mini` output with RAG enabled, recorded by the eval harness
|
|
75
|
-
on its `missing_fk` fixture.*
|
|
45
|
+
# Django Explain Errors
|
|
46
|
+
|
|
47
|
+
Explains Django exceptions on the debug page, pointing at the real fix in your own source code.
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/v/django-explain-errors" alt="PyPI version"></a>
|
|
51
|
+
<a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/pyversions/django-explain-errors" alt="Python versions"></a>
|
|
52
|
+
<a href="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml"><img src="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
|
|
53
|
+
<a href="https://github.com/topunix/django-explain-errors/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/django-explain-errors" alt="License"></a>
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<p align="center">
|
|
57
|
+
<img src="https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png" width="800"
|
|
58
|
+
alt="Django debug page with the explanation banner under the exception headline">
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
*The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
|
|
62
|
+
failing function (`clone_latest_post`) and gives the one-line fix. Real `gpt-4o-mini` output from
|
|
63
|
+
the eval harness.*
|
|
64
|
+
|
|
65
|
+
- **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
|
|
66
|
+
- Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
|
|
67
|
+
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with code and identifiers kept in English
|
|
68
|
+
- Sync and async views, traceback redaction, rate limiting
|
|
69
|
+
- Local development only: requires `DEBUG = True`
|
|
76
70
|
|
|
77
71
|
## Scope
|
|
78
72
|
|
|
@@ -85,19 +79,20 @@ If a coding agent is doing the debugging, it does not need this. Agents read tra
|
|
|
85
79
|
and tools that expose live runtime state (debugger-over-MCP servers, `mcp-django`) serve that case
|
|
86
80
|
better. This package is not trying to compete there.
|
|
87
81
|
|
|
88
|
-
Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
89
|
-
|
|
90
82
|
## Features
|
|
91
83
|
|
|
92
84
|
- Captures Django errors and exceptions
|
|
93
85
|
- Shows the explanation on Django's debug page, directly under the
|
|
94
86
|
exception headline (`EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, on by default)
|
|
87
|
+
- Copy button on each code block in the explanation, so a suggested fix
|
|
88
|
+
copies in one click
|
|
95
89
|
- Prints the explanation to stdout, for terminal workflows and logs
|
|
96
90
|
(`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
|
|
97
91
|
- Optional JSON 500 response instead of the debug page
|
|
98
92
|
(`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False`)
|
|
99
|
-
- Explains errors using OpenAI, Anthropic's Claude models, or any other
|
|
100
|
-
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL
|
|
93
|
+
- Explains errors using OpenAI out of the box, Anthropic's Claude models, or any other
|
|
94
|
+
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`,
|
|
95
|
+
including fully local models, so no code has to leave your machine
|
|
101
96
|
- Codebase-aware explanations via RAG over a local sqlite-vec index
|
|
102
97
|
(recommended; see "Codebase-aware explanations" below)
|
|
103
98
|
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
|
|
@@ -223,6 +218,10 @@ background) so it reads on Django's page. The explanation is HTML-escaped first,
|
|
|
223
218
|
Markdown subset (inline code, code blocks, bold, lists) is rendered as HTML. Nothing from the
|
|
224
219
|
model is ever marked safe.
|
|
225
220
|
|
|
221
|
+
Each fenced code block gets a "Copy" button that copies the exact code (via
|
|
222
|
+
`navigator.clipboard` on secure contexts such as `localhost`, or a text-selection fallback
|
|
223
|
+
otherwise, for example when `runserver` is reached by IP over plain HTTP).
|
|
224
|
+
|
|
226
225
|
**Fails open.** Any problem building or inserting the banner (a missing request, an
|
|
227
226
|
unexpected debug page layout, anything else) logs one warning and leaves Django's normal
|
|
228
227
|
debug page untouched. This code can never turn a working debug page into a broken one.
|
|
@@ -1,34 +1,28 @@
|
|
|
1
|
-
# Django Explain Errors
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
rate limited.
|
|
27
|
-
|
|
28
|
-

|
|
29
|
-
|
|
30
|
-
*Real `gpt-4o-mini` output with RAG enabled, recorded by the eval harness
|
|
31
|
-
on its `missing_fk` fixture.*
|
|
1
|
+
# Django Explain Errors
|
|
2
|
+
|
|
3
|
+
Explains Django exceptions on the debug page, pointing at the real fix in your own source code.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/v/django-explain-errors" alt="PyPI version"></a>
|
|
7
|
+
<a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/pyversions/django-explain-errors" alt="Python versions"></a>
|
|
8
|
+
<a href="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml"><img src="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
|
|
9
|
+
<a href="https://github.com/topunix/django-explain-errors/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/django-explain-errors" alt="License"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png" width="800"
|
|
14
|
+
alt="Django debug page with the explanation banner under the exception headline">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
*The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
|
|
18
|
+
failing function (`clone_latest_post`) and gives the one-line fix. Real `gpt-4o-mini` output from
|
|
19
|
+
the eval harness.*
|
|
20
|
+
|
|
21
|
+
- **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
|
|
22
|
+
- Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
|
|
23
|
+
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with code and identifiers kept in English
|
|
24
|
+
- Sync and async views, traceback redaction, rate limiting
|
|
25
|
+
- Local development only: requires `DEBUG = True`
|
|
32
26
|
|
|
33
27
|
## Scope
|
|
34
28
|
|
|
@@ -41,19 +35,20 @@ If a coding agent is doing the debugging, it does not need this. Agents read tra
|
|
|
41
35
|
and tools that expose live runtime state (debugger-over-MCP servers, `mcp-django`) serve that case
|
|
42
36
|
better. This package is not trying to compete there.
|
|
43
37
|
|
|
44
|
-
Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
45
|
-
|
|
46
38
|
## Features
|
|
47
39
|
|
|
48
40
|
- Captures Django errors and exceptions
|
|
49
41
|
- Shows the explanation on Django's debug page, directly under the
|
|
50
42
|
exception headline (`EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, on by default)
|
|
43
|
+
- Copy button on each code block in the explanation, so a suggested fix
|
|
44
|
+
copies in one click
|
|
51
45
|
- Prints the explanation to stdout, for terminal workflows and logs
|
|
52
46
|
(`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
|
|
53
47
|
- Optional JSON 500 response instead of the debug page
|
|
54
48
|
(`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False`)
|
|
55
|
-
- Explains errors using OpenAI, Anthropic's Claude models, or any other
|
|
56
|
-
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL
|
|
49
|
+
- Explains errors using OpenAI out of the box, Anthropic's Claude models, or any other
|
|
50
|
+
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`,
|
|
51
|
+
including fully local models, so no code has to leave your machine
|
|
57
52
|
- Codebase-aware explanations via RAG over a local sqlite-vec index
|
|
58
53
|
(recommended; see "Codebase-aware explanations" below)
|
|
59
54
|
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
|
|
@@ -179,6 +174,10 @@ background) so it reads on Django's page. The explanation is HTML-escaped first,
|
|
|
179
174
|
Markdown subset (inline code, code blocks, bold, lists) is rendered as HTML. Nothing from the
|
|
180
175
|
model is ever marked safe.
|
|
181
176
|
|
|
177
|
+
Each fenced code block gets a "Copy" button that copies the exact code (via
|
|
178
|
+
`navigator.clipboard` on secure contexts such as `localhost`, or a text-selection fallback
|
|
179
|
+
otherwise, for example when `runserver` is reached by IP over plain HTTP).
|
|
180
|
+
|
|
182
181
|
**Fails open.** Any problem building or inserting the banner (a missing request, an
|
|
183
182
|
unexpected debug page layout, anything else) logs one warning and leaves Django's normal
|
|
184
183
|
debug page untouched. This code can never turn a working debug page into a broken one.
|
{django_explain_errors-0.8.0 → django_explain_errors-0.9.1/django_explain_errors.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.
|
|
5
5
|
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
6
|
Author: topunix
|
|
@@ -42,37 +42,31 @@ Dynamic: requires-dist
|
|
|
42
42
|
Dynamic: requires-python
|
|
43
43
|
Dynamic: summary
|
|
44
44
|
|
|
45
|
-
# Django Explain Errors
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
rate limited.
|
|
71
|
-
|
|
72
|
-

|
|
73
|
-
|
|
74
|
-
*Real `gpt-4o-mini` output with RAG enabled, recorded by the eval harness
|
|
75
|
-
on its `missing_fk` fixture.*
|
|
45
|
+
# Django Explain Errors
|
|
46
|
+
|
|
47
|
+
Explains Django exceptions on the debug page, pointing at the real fix in your own source code.
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/v/django-explain-errors" alt="PyPI version"></a>
|
|
51
|
+
<a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/pyversions/django-explain-errors" alt="Python versions"></a>
|
|
52
|
+
<a href="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml"><img src="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
|
|
53
|
+
<a href="https://github.com/topunix/django-explain-errors/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/django-explain-errors" alt="License"></a>
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<p align="center">
|
|
57
|
+
<img src="https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png" width="800"
|
|
58
|
+
alt="Django debug page with the explanation banner under the exception headline">
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
*The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
|
|
62
|
+
failing function (`clone_latest_post`) and gives the one-line fix. Real `gpt-4o-mini` output from
|
|
63
|
+
the eval harness.*
|
|
64
|
+
|
|
65
|
+
- **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
|
|
66
|
+
- Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
|
|
67
|
+
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with code and identifiers kept in English
|
|
68
|
+
- Sync and async views, traceback redaction, rate limiting
|
|
69
|
+
- Local development only: requires `DEBUG = True`
|
|
76
70
|
|
|
77
71
|
## Scope
|
|
78
72
|
|
|
@@ -85,19 +79,20 @@ If a coding agent is doing the debugging, it does not need this. Agents read tra
|
|
|
85
79
|
and tools that expose live runtime state (debugger-over-MCP servers, `mcp-django`) serve that case
|
|
86
80
|
better. This package is not trying to compete there.
|
|
87
81
|
|
|
88
|
-
Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
89
|
-
|
|
90
82
|
## Features
|
|
91
83
|
|
|
92
84
|
- Captures Django errors and exceptions
|
|
93
85
|
- Shows the explanation on Django's debug page, directly under the
|
|
94
86
|
exception headline (`EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, on by default)
|
|
87
|
+
- Copy button on each code block in the explanation, so a suggested fix
|
|
88
|
+
copies in one click
|
|
95
89
|
- Prints the explanation to stdout, for terminal workflows and logs
|
|
96
90
|
(`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
|
|
97
91
|
- Optional JSON 500 response instead of the debug page
|
|
98
92
|
(`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False`)
|
|
99
|
-
- Explains errors using OpenAI, Anthropic's Claude models, or any other
|
|
100
|
-
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL
|
|
93
|
+
- Explains errors using OpenAI out of the box, Anthropic's Claude models, or any other
|
|
94
|
+
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`,
|
|
95
|
+
including fully local models, so no code has to leave your machine
|
|
101
96
|
- Codebase-aware explanations via RAG over a local sqlite-vec index
|
|
102
97
|
(recommended; see "Codebase-aware explanations" below)
|
|
103
98
|
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
|
|
@@ -223,6 +218,10 @@ background) so it reads on Django's page. The explanation is HTML-escaped first,
|
|
|
223
218
|
Markdown subset (inline code, code blocks, bold, lists) is rendered as HTML. Nothing from the
|
|
224
219
|
model is ever marked safe.
|
|
225
220
|
|
|
221
|
+
Each fenced code block gets a "Copy" button that copies the exact code (via
|
|
222
|
+
`navigator.clipboard` on secure contexts such as `localhost`, or a text-selection fallback
|
|
223
|
+
otherwise, for example when `runserver` is reached by IP over plain HTTP).
|
|
224
|
+
|
|
226
225
|
**Fails open.** Any problem building or inserting the banner (a missing request, an
|
|
227
226
|
unexpected debug page layout, anything else) logs one warning and leaves Django's normal
|
|
228
227
|
debug page untouched. This code can never turn a working debug page into a broken one.
|
|
@@ -21,10 +21,78 @@ _PRE_STYLE = (
|
|
|
21
21
|
"font-family: monospace; background: #eee; padding: 8px; "
|
|
22
22
|
"overflow-x: auto; white-space: pre-wrap; margin: 4px 0;"
|
|
23
23
|
)
|
|
24
|
+
_PRE_WITH_BUTTON_STYLE = (
|
|
25
|
+
"font-family: monospace; background: #eee; padding: 8px 60px 8px 8px; "
|
|
26
|
+
"overflow-x: auto; white-space: pre-wrap; margin: 0;"
|
|
27
|
+
)
|
|
24
28
|
_P_STYLE = "margin: 6px 0;"
|
|
25
29
|
_UL_STYLE = "margin: 6px 0; padding-inline-start: 1.5em; list-style: disc;"
|
|
26
30
|
_OL_STYLE = "margin: 6px 0; padding-inline-start: 1.5em; list-style: decimal;"
|
|
27
31
|
_LI_STYLE = "margin: 2px 0;"
|
|
32
|
+
_CODE_WRAPPER_STYLE = "position: relative; margin: 4px 0;"
|
|
33
|
+
_COPY_BUTTON_STYLE = (
|
|
34
|
+
"position: absolute; top: 4px; right: 4px; font-family: sans-serif; "
|
|
35
|
+
"font-size: 11px; line-height: 1; padding: 3px 7px; cursor: pointer; "
|
|
36
|
+
"background: #fff; border: 1px solid #999; border-radius: 3px; color: #333;"
|
|
37
|
+
)
|
|
38
|
+
_COPY_BUTTON_CLASS = "explain-errors-copy-btn"
|
|
39
|
+
_CODE_WRAPPER_CLASS = "explain-errors-code-wrapper"
|
|
40
|
+
|
|
41
|
+
_COPY_SCRIPT = """
|
|
42
|
+
<script>
|
|
43
|
+
(function () {{
|
|
44
|
+
var container = document.currentScript && document.currentScript.closest("#explain-errors");
|
|
45
|
+
if (!container) {{
|
|
46
|
+
return;
|
|
47
|
+
}}
|
|
48
|
+
container.addEventListener("click", function (event) {{
|
|
49
|
+
var btn = event.target.closest("button[data-copy]");
|
|
50
|
+
if (!btn) {{
|
|
51
|
+
return;
|
|
52
|
+
}}
|
|
53
|
+
var wrapper = btn.closest(".{wrapper_class}");
|
|
54
|
+
var code = wrapper && wrapper.querySelector("code");
|
|
55
|
+
if (!code) {{
|
|
56
|
+
return;
|
|
57
|
+
}}
|
|
58
|
+
var text = code.textContent;
|
|
59
|
+
var showCopied = function () {{
|
|
60
|
+
if (btn._explainErrorsTimer) {{
|
|
61
|
+
clearTimeout(btn._explainErrorsTimer);
|
|
62
|
+
}}
|
|
63
|
+
btn.textContent = "Copied";
|
|
64
|
+
btn._explainErrorsTimer = setTimeout(function () {{
|
|
65
|
+
btn.textContent = "Copy";
|
|
66
|
+
btn._explainErrorsTimer = null;
|
|
67
|
+
}}, 1500);
|
|
68
|
+
}};
|
|
69
|
+
var fallbackCopy = function () {{
|
|
70
|
+
try {{
|
|
71
|
+
var range = document.createRange();
|
|
72
|
+
range.selectNodeContents(code);
|
|
73
|
+
var selection = window.getSelection();
|
|
74
|
+
selection.removeAllRanges();
|
|
75
|
+
selection.addRange(range);
|
|
76
|
+
var ok = document.execCommand("copy");
|
|
77
|
+
selection.removeAllRanges();
|
|
78
|
+
if (ok) {{
|
|
79
|
+
showCopied();
|
|
80
|
+
}}
|
|
81
|
+
}} catch (err) {{
|
|
82
|
+
/* both copy paths failed; do nothing */
|
|
83
|
+
}}
|
|
84
|
+
}};
|
|
85
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {{
|
|
86
|
+
navigator.clipboard.writeText(text).then(showCopied, fallbackCopy);
|
|
87
|
+
}} else {{
|
|
88
|
+
fallbackCopy();
|
|
89
|
+
}}
|
|
90
|
+
}});
|
|
91
|
+
}})();
|
|
92
|
+
</script>
|
|
93
|
+
""".format(
|
|
94
|
+
wrapper_class=_CODE_WRAPPER_CLASS
|
|
95
|
+
)
|
|
28
96
|
|
|
29
97
|
_LANG_TAG_RE = re.compile(r"^[A-Za-z0-9_+-]*$")
|
|
30
98
|
_INLINE_CODE_RE = re.compile(r"(`[^`\n]*`)")
|
|
@@ -55,12 +123,18 @@ def render_explanation_html(text):
|
|
|
55
123
|
def _render(text):
|
|
56
124
|
parts = text.split("```")
|
|
57
125
|
rendered = []
|
|
126
|
+
has_copy_button = False
|
|
58
127
|
for index, part in enumerate(parts):
|
|
59
128
|
if index % 2 == 1:
|
|
60
|
-
|
|
129
|
+
block_html, has_button = _render_code_block(part)
|
|
130
|
+
rendered.append(block_html)
|
|
131
|
+
has_copy_button = has_copy_button or has_button
|
|
61
132
|
else:
|
|
62
133
|
rendered.append(_render_prose(escape(part)))
|
|
63
|
-
|
|
134
|
+
html = "".join(rendered)
|
|
135
|
+
if has_copy_button:
|
|
136
|
+
html += _COPY_SCRIPT
|
|
137
|
+
return html
|
|
64
138
|
|
|
65
139
|
|
|
66
140
|
def _render_code_block(segment):
|
|
@@ -71,9 +145,24 @@ def _render_code_block(segment):
|
|
|
71
145
|
code = rest
|
|
72
146
|
if code.endswith("\n"):
|
|
73
147
|
code = code[:-1]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
148
|
+
if not code.strip():
|
|
149
|
+
return (
|
|
150
|
+
'<pre dir="ltr" style="{pre_style}"><code dir="ltr">{code}</code></pre>'
|
|
151
|
+
).format(pre_style=_PRE_STYLE, code=escape(code)), False
|
|
152
|
+
return (
|
|
153
|
+
'<div class="{wrapper_class}" style="{wrapper_style}">'
|
|
154
|
+
'<button type="button" class="{btn_class}" data-copy '
|
|
155
|
+
'aria-label="Copy code" style="{btn_style}">Copy</button>'
|
|
156
|
+
'<pre dir="ltr" style="{pre_style}"><code dir="ltr">{code}</code></pre>'
|
|
157
|
+
"</div>"
|
|
158
|
+
).format(
|
|
159
|
+
wrapper_class=_CODE_WRAPPER_CLASS,
|
|
160
|
+
wrapper_style=_CODE_WRAPPER_STYLE,
|
|
161
|
+
btn_class=_COPY_BUTTON_CLASS,
|
|
162
|
+
btn_style=_COPY_BUTTON_STYLE,
|
|
163
|
+
pre_style=_PRE_WITH_BUTTON_STYLE,
|
|
164
|
+
code=escape(code),
|
|
165
|
+
), True
|
|
77
166
|
|
|
78
167
|
|
|
79
168
|
def _render_prose(escaped_text):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""File discovery, chunking, and embedding for the RAG source index."""
|
|
2
2
|
import ast
|
|
3
|
+
import contextlib
|
|
3
4
|
import os
|
|
4
5
|
|
|
5
6
|
from django.conf import settings
|
|
@@ -167,16 +168,21 @@ def build_index():
|
|
|
167
168
|
os.remove(tmp_path)
|
|
168
169
|
|
|
169
170
|
dimensions = len(embeddings[0]) if embeddings else 1536
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
(
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
try:
|
|
172
|
+
with VectorStore(tmp_path) as store:
|
|
173
|
+
store.create(dimensions)
|
|
174
|
+
store.add(
|
|
175
|
+
(file_path, start_line, end_line, sanitized_text, embedding)
|
|
176
|
+
for (file_path, start_line, end_line, _text), sanitized_text, embedding in zip(
|
|
177
|
+
raw_chunks, sanitized_texts, embeddings
|
|
178
|
+
)
|
|
176
179
|
)
|
|
177
|
-
)
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
os.replace(tmp_path, index_path)
|
|
182
|
+
except BaseException:
|
|
183
|
+
with contextlib.suppress(OSError):
|
|
184
|
+
os.remove(tmp_path)
|
|
185
|
+
raise
|
|
180
186
|
|
|
181
187
|
return {
|
|
182
188
|
"files_scanned": len(files),
|
|
@@ -10,7 +10,7 @@ with open('README.md', encoding='utf-8') as f:
|
|
|
10
10
|
|
|
11
11
|
setup(
|
|
12
12
|
name='django-explain-errors',
|
|
13
|
-
version='0.
|
|
13
|
+
version='0.9.1',
|
|
14
14
|
packages=find_packages(exclude=['tests', 'tests.*', 'evals', 'evals.*']),
|
|
15
15
|
description='Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.',
|
|
16
16
|
long_description_content_type='text/markdown',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import html as html_module
|
|
2
3
|
from unittest.mock import MagicMock, patch
|
|
3
4
|
|
|
4
5
|
from django.test import (
|
|
@@ -357,6 +358,67 @@ class RenderExplanationHtmlTest(SimpleTestCase):
|
|
|
357
358
|
self.assertNotIn("<a ", html)
|
|
358
359
|
self.assertIn("[text](url)", html)
|
|
359
360
|
|
|
361
|
+
def test_single_code_block_emits_one_copy_button_and_script(self):
|
|
362
|
+
html = render_explanation_html("```\nx = 1\n```")
|
|
363
|
+
|
|
364
|
+
self.assertEqual(html.count("<button"), 1)
|
|
365
|
+
self.assertEqual(html.count("<script>"), 1)
|
|
366
|
+
|
|
367
|
+
def test_multiple_code_blocks_emit_one_button_each_and_exactly_one_script(self):
|
|
368
|
+
html = render_explanation_html("```\nx = 1\n```\ntext\n```\ny = 2\n```")
|
|
369
|
+
|
|
370
|
+
self.assertEqual(html.count("<button"), 2)
|
|
371
|
+
self.assertEqual(html.count("<script>"), 1)
|
|
372
|
+
|
|
373
|
+
def test_copy_button_has_type_button_and_aria_label(self):
|
|
374
|
+
html = render_explanation_html("```\nx = 1\n```")
|
|
375
|
+
|
|
376
|
+
self.assertIn('type="button"', html)
|
|
377
|
+
self.assertIn('aria-label="Copy code"', html)
|
|
378
|
+
|
|
379
|
+
def test_no_code_blocks_emit_no_button_and_no_script(self):
|
|
380
|
+
html = render_explanation_html("Just prose, no code here.")
|
|
381
|
+
|
|
382
|
+
self.assertNotIn("data-copy", html)
|
|
383
|
+
self.assertNotIn("explain-errors-copy-btn", html)
|
|
384
|
+
self.assertNotIn("<script>", html)
|
|
385
|
+
|
|
386
|
+
def test_copy_source_round_trips_html_special_characters(self):
|
|
387
|
+
raw_code = """<script>alert("x & 'y'")</script>"""
|
|
388
|
+
html = render_explanation_html("```\n{}\n```".format(raw_code))
|
|
389
|
+
|
|
390
|
+
start = html.index("<code dir=\"ltr\">") + len('<code dir="ltr">')
|
|
391
|
+
end = html.index("</code>", start)
|
|
392
|
+
escaped_code = html[start:end]
|
|
393
|
+
|
|
394
|
+
self.assertEqual(html_module.unescape(escaped_code), raw_code)
|
|
395
|
+
|
|
396
|
+
def test_empty_code_block_emits_pre_without_button(self):
|
|
397
|
+
html = render_explanation_html("```\n\n```")
|
|
398
|
+
|
|
399
|
+
self.assertIn("<pre", html)
|
|
400
|
+
self.assertNotIn("<button", html)
|
|
401
|
+
self.assertNotIn("explain-errors-code-wrapper", html)
|
|
402
|
+
|
|
403
|
+
def test_whitespace_only_code_block_emits_pre_without_button(self):
|
|
404
|
+
html = render_explanation_html("```\n \n```")
|
|
405
|
+
|
|
406
|
+
self.assertIn("<pre", html)
|
|
407
|
+
self.assertNotIn("<button", html)
|
|
408
|
+
self.assertNotIn("explain-errors-code-wrapper", html)
|
|
409
|
+
|
|
410
|
+
def test_all_empty_code_blocks_emit_no_script(self):
|
|
411
|
+
html = render_explanation_html("```\n\n```\ntext\n```\n \n```")
|
|
412
|
+
|
|
413
|
+
self.assertNotIn("<button", html)
|
|
414
|
+
self.assertNotIn("<script>", html)
|
|
415
|
+
|
|
416
|
+
def test_mix_of_empty_and_nonempty_blocks_emits_button_only_for_nonempty(self):
|
|
417
|
+
html = render_explanation_html("```\n\n```\ntext\n```\nx = 1\n```")
|
|
418
|
+
|
|
419
|
+
self.assertEqual(html.count("<button"), 1)
|
|
420
|
+
self.assertEqual(html.count("<script>"), 1)
|
|
421
|
+
|
|
360
422
|
def test_raising_transform_falls_back_to_escaped_plain_text_and_logs(self):
|
|
361
423
|
with patch(
|
|
362
424
|
"explain_errors.debug_page._render", side_effect=RuntimeError("boom")
|
|
@@ -174,6 +174,76 @@ class IndexerBuildTest(SimpleTestCase):
|
|
|
174
174
|
]
|
|
175
175
|
self.assertEqual(leftovers, [])
|
|
176
176
|
|
|
177
|
+
def test_failed_rebuild_keeps_previous_index_and_removes_temp_file(self):
|
|
178
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
179
|
+
with open(os.path.join(tmp, "app.py"), "w") as f:
|
|
180
|
+
f.write(SAMPLE_MODULE)
|
|
181
|
+
|
|
182
|
+
index_path = os.path.join(tmp, "index.db")
|
|
183
|
+
|
|
184
|
+
with override_settings(
|
|
185
|
+
EXPLAIN_ERRORS_RAG_INCLUDE=[tmp],
|
|
186
|
+
EXPLAIN_ERRORS_RAG_INDEX_PATH=index_path,
|
|
187
|
+
):
|
|
188
|
+
with patch(
|
|
189
|
+
"explain_errors.rag.indexer.get_openai_client",
|
|
190
|
+
return_value=_mock_embed_client([0.1, 0.2, 0.3]),
|
|
191
|
+
):
|
|
192
|
+
build_index()
|
|
193
|
+
|
|
194
|
+
with open(index_path, "rb") as f:
|
|
195
|
+
original_bytes = f.read()
|
|
196
|
+
with VectorStore(index_path) as store:
|
|
197
|
+
original_count = store.count()
|
|
198
|
+
self.assertGreater(original_count, 0)
|
|
199
|
+
|
|
200
|
+
with patch.object(
|
|
201
|
+
VectorStore, "add", side_effect=RuntimeError("add failed")
|
|
202
|
+
):
|
|
203
|
+
with self.assertRaisesRegex(RuntimeError, "add failed"):
|
|
204
|
+
build_index()
|
|
205
|
+
|
|
206
|
+
with open(index_path, "rb") as f:
|
|
207
|
+
self.assertEqual(f.read(), original_bytes)
|
|
208
|
+
with VectorStore(index_path) as store:
|
|
209
|
+
self.assertEqual(store.count(), original_count)
|
|
210
|
+
|
|
211
|
+
leftovers = [
|
|
212
|
+
name for name in os.listdir(tmp) if name.startswith("index.db.tmp-")
|
|
213
|
+
]
|
|
214
|
+
self.assertEqual(leftovers, [])
|
|
215
|
+
|
|
216
|
+
def test_build_index_redacts_secrets_in_stored_chunk_text(self):
|
|
217
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
218
|
+
src = os.path.join(tmp, "src")
|
|
219
|
+
os.makedirs(src)
|
|
220
|
+
with open(os.path.join(src, "config.py"), "w") as f:
|
|
221
|
+
f.write(
|
|
222
|
+
"def load():\n"
|
|
223
|
+
" api_key = 'sk-index-secret-value'\n"
|
|
224
|
+
" return api_key\n"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
index_path = os.path.join(tmp, "index.db")
|
|
228
|
+
embed_client = _mock_embed_client([0.1, 0.2, 0.3])
|
|
229
|
+
|
|
230
|
+
with override_settings(
|
|
231
|
+
EXPLAIN_ERRORS_RAG_INCLUDE=[src],
|
|
232
|
+
EXPLAIN_ERRORS_RAG_INDEX_PATH=index_path,
|
|
233
|
+
):
|
|
234
|
+
with patch(
|
|
235
|
+
"explain_errors.rag.indexer.get_openai_client",
|
|
236
|
+
return_value=embed_client,
|
|
237
|
+
):
|
|
238
|
+
build_index()
|
|
239
|
+
|
|
240
|
+
with VectorStore(index_path) as store:
|
|
241
|
+
rows = store.query([0.1, 0.2, 0.3], 10)
|
|
242
|
+
|
|
243
|
+
self.assertEqual(len(rows), 1)
|
|
244
|
+
self.assertNotIn("sk-index-secret-value", rows[0]["chunk_text"])
|
|
245
|
+
self.assertIn("[REDACTED]", rows[0]["chunk_text"])
|
|
246
|
+
|
|
177
247
|
|
|
178
248
|
def _raise_json_error():
|
|
179
249
|
return json.loads("{not valid json")
|
|
@@ -192,6 +262,35 @@ class RetrieverFrameExtractionTest(SimpleTestCase):
|
|
|
192
262
|
self.assertTrue(any(name.endswith("test_rag.py") for name in filenames))
|
|
193
263
|
self.assertFalse(any("json" in os.path.basename(name) for name in filenames))
|
|
194
264
|
|
|
265
|
+
def test_extract_project_frames_excludes_site_packages_frame(self):
|
|
266
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
267
|
+
site_dir = os.path.join(tmp, "venv-lib", "site-packages")
|
|
268
|
+
os.makedirs(site_dir)
|
|
269
|
+
lib_path = os.path.join(site_dir, "thirdparty.py")
|
|
270
|
+
app_path = os.path.join(tmp, "app.py")
|
|
271
|
+
with open(lib_path, "w") as f:
|
|
272
|
+
f.write("def boom():\n raise ValueError('lib failure')\n")
|
|
273
|
+
with open(app_path, "w") as f:
|
|
274
|
+
f.write("def call(fn):\n return fn()\n")
|
|
275
|
+
|
|
276
|
+
namespaces = {}
|
|
277
|
+
for path in (lib_path, app_path):
|
|
278
|
+
ns = {}
|
|
279
|
+
with open(path) as f:
|
|
280
|
+
exec(compile(f.read(), path, "exec"), ns)
|
|
281
|
+
namespaces[path] = ns
|
|
282
|
+
|
|
283
|
+
# site-packages sits inside the include dir, so only the
|
|
284
|
+
# site-packages rule can exclude it.
|
|
285
|
+
with override_settings(EXPLAIN_ERRORS_RAG_INCLUDE=[tmp]):
|
|
286
|
+
try:
|
|
287
|
+
namespaces[app_path]["call"](namespaces[lib_path]["boom"])
|
|
288
|
+
except ValueError as exc:
|
|
289
|
+
frames = extract_project_frames(exc)
|
|
290
|
+
|
|
291
|
+
filenames = [frame.filename for frame in frames]
|
|
292
|
+
self.assertEqual(filenames, [app_path])
|
|
293
|
+
|
|
195
294
|
def test_extract_project_frames_empty_without_traceback(self):
|
|
196
295
|
self.assertEqual(extract_project_frames(ValueError("no traceback")), [])
|
|
197
296
|
|
|
@@ -229,6 +328,50 @@ class RetrieverTopKTest(SimpleTestCase):
|
|
|
229
328
|
self.assertEqual(retrieve_chunks(ValueError("boom")), [])
|
|
230
329
|
|
|
231
330
|
|
|
331
|
+
class RetrieverQuerySanitizationTest(SimpleTestCase):
|
|
332
|
+
|
|
333
|
+
@requires_sqlite_vec
|
|
334
|
+
def test_query_text_is_sanitized_before_embedding(self):
|
|
335
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
336
|
+
module_path = os.path.join(tmp, "leaky.py")
|
|
337
|
+
with open(module_path, "w") as f:
|
|
338
|
+
f.write(
|
|
339
|
+
"def fail():\n"
|
|
340
|
+
" password = 'hunter2-source-secret'; raise ValueError(\n"
|
|
341
|
+
" 'token=msg-secret-value')\n"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
index_path = os.path.join(tmp, "index.db")
|
|
345
|
+
with VectorStore(index_path) as store:
|
|
346
|
+
store.create(3)
|
|
347
|
+
store.add([("leaky.py", 1, 3, "chunk", [1.0, 0.0, 0.0])])
|
|
348
|
+
|
|
349
|
+
namespace = {}
|
|
350
|
+
exec(compile(open(module_path).read(), module_path, "exec"), namespace)
|
|
351
|
+
try:
|
|
352
|
+
namespace["fail"]()
|
|
353
|
+
except ValueError as exc:
|
|
354
|
+
captured = exc
|
|
355
|
+
|
|
356
|
+
embed_client = _mock_embed_client([1.0, 0.0, 0.0])
|
|
357
|
+
with override_settings(
|
|
358
|
+
EXPLAIN_ERRORS_RAG_ENABLED=True,
|
|
359
|
+
EXPLAIN_ERRORS_RAG_INDEX_PATH=index_path,
|
|
360
|
+
EXPLAIN_ERRORS_RAG_INCLUDE=[tmp],
|
|
361
|
+
):
|
|
362
|
+
with patch(
|
|
363
|
+
"explain_errors.rag.retriever.get_openai_client",
|
|
364
|
+
return_value=embed_client,
|
|
365
|
+
):
|
|
366
|
+
retrieve_chunks(captured)
|
|
367
|
+
|
|
368
|
+
embed_client.embeddings.create.assert_called_once()
|
|
369
|
+
(query_text,) = embed_client.embeddings.create.call_args.kwargs["input"]
|
|
370
|
+
self.assertNotIn("msg-secret-value", query_text)
|
|
371
|
+
self.assertNotIn("hunter2-source-secret", query_text)
|
|
372
|
+
self.assertIn("[REDACTED]", query_text)
|
|
373
|
+
|
|
374
|
+
|
|
232
375
|
@override_settings(DEBUG=True, OPENAI_API_KEY="test-key")
|
|
233
376
|
class MiddlewareRagIntegrationTest(SimpleTestCase):
|
|
234
377
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_explain_errors-0.8.0 → django_explain_errors-0.9.1}/explain_errors/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|