django-explain-errors 0.2.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.
- django_explain_errors-0.2.0/LICENSE +21 -0
- django_explain_errors-0.2.0/MANIFEST.in +3 -0
- django_explain_errors-0.2.0/PKG-INFO +151 -0
- django_explain_errors-0.2.0/README.md +107 -0
- django_explain_errors-0.2.0/README.rst +107 -0
- django_explain_errors-0.2.0/django_explain_errors.egg-info/PKG-INFO +151 -0
- django_explain_errors-0.2.0/django_explain_errors.egg-info/SOURCES.txt +13 -0
- django_explain_errors-0.2.0/django_explain_errors.egg-info/dependency_links.txt +1 -0
- django_explain_errors-0.2.0/django_explain_errors.egg-info/requires.txt +4 -0
- django_explain_errors-0.2.0/django_explain_errors.egg-info/top_level.txt +1 -0
- django_explain_errors-0.2.0/explain_errors/__init__.py +0 -0
- django_explain_errors-0.2.0/explain_errors/middleware.py +109 -0
- django_explain_errors-0.2.0/setup.cfg +4 -0
- django_explain_errors-0.2.0/setup.py +48 -0
- django_explain_errors-0.2.0/tests/test_middleware.py +160 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mike
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-explain-errors
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Django middleware that captures errors and exceptions, sends them to OpenAI for a detailed explanation, and prints the explanation to stdout when debug mode is enabled. Supports both sync and async views.
|
|
5
|
+
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
|
+
Author: topunix
|
|
7
|
+
Author-email: topunixguy@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 4.2
|
|
13
|
+
Classifier: Framework :: Django :: 5.0
|
|
14
|
+
Classifier: Framework :: Django :: 5.1
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: Django>=4.2
|
|
30
|
+
Requires-Dist: openai>=1.0
|
|
31
|
+
Requires-Dist: python-dotenv>=1.0
|
|
32
|
+
Requires-Dist: asgiref>=3.6
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
# Django Explain Errors Middleware
|
|
46
|
+
|
|
47
|
+
This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It uses an environment variable to securely manage the OpenAI API key.
|
|
48
|
+
|
|
49
|
+
The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- Captures Django errors and exceptions
|
|
54
|
+
- Uses OpenAI to explain the error
|
|
55
|
+
- Securely manages the OpenAI API key using environment variables
|
|
56
|
+
- Works with both sync (WSGI) and async (ASGI) views
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
1. Install django-explain-errors by running:
|
|
61
|
+
```bash
|
|
62
|
+
pip install django-explain-errors
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. **Add the middleware to your Django project**:
|
|
66
|
+
|
|
67
|
+
- Open your `settings.py` file and add the middleware to the `MIDDLEWARE` list. Ensure that the middleware is added last in the list:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
MIDDLEWARE = [
|
|
71
|
+
...
|
|
72
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
73
|
+
]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
3. **Set up environment variables**:
|
|
77
|
+
|
|
78
|
+
- Create a `.env` file in your project's root directory and add your OpenAI API key. Alternatively, you can set the API key in `settings.py`:
|
|
79
|
+
|
|
80
|
+
```plaintext
|
|
81
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
1. **Ensure DEBUG is set to True**:
|
|
87
|
+
|
|
88
|
+
Open your `settings.py` file and set:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
DEBUG = True
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
2. **Trigger an error in your Django application**:
|
|
95
|
+
|
|
96
|
+
The middleware will capture the error, send it to OpenAI for explanation, and print the explanation to stdout. When an exception is caught, it returns a JSON `500` response containing the error message and the explanation.
|
|
97
|
+
|
|
98
|
+
## Async Support
|
|
99
|
+
|
|
100
|
+
The middleware exposes both `sync_capable = True` and `async_capable = True`. At initialization it inspects `get_response` to decide whether it is part of a sync or async chain:
|
|
101
|
+
|
|
102
|
+
- Under WSGI (for example `runserver` with sync views), requests flow through the synchronous handler.
|
|
103
|
+
- Under ASGI (for example with async views), requests are awaited through the async handler. The blocking OpenAI call is offloaded with `asgiref.sync.sync_to_async` so the event loop is not blocked.
|
|
104
|
+
|
|
105
|
+
No additional settings are needed. Place the middleware last in `MIDDLEWARE` for both modes.
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
| Setting / variable | Required | Description |
|
|
110
|
+
| ------------------ | -------- | ----------- |
|
|
111
|
+
| `OPENAI_API_KEY` (env or settings) | Yes, when `DEBUG=True` | API key used to authenticate with OpenAI. Read first from the environment, then from `settings`. |
|
|
112
|
+
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
113
|
+
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
114
|
+
| `OPENAI_MAX_TOKENS` | No | Maximum tokens in the explanation. Defaults to `150`. |
|
|
115
|
+
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
|
|
116
|
+
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
|
|
117
|
+
|
|
118
|
+
## Example
|
|
119
|
+
|
|
120
|
+
Here is an example of how to use the middleware in a Django project:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# settings.py
|
|
124
|
+
|
|
125
|
+
DEBUG = True
|
|
126
|
+
|
|
127
|
+
MIDDLEWARE = [
|
|
128
|
+
...
|
|
129
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# .env
|
|
133
|
+
|
|
134
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
When an error occurs, you will see an explanation printed to stdout.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
142
|
+
|
|
143
|
+
## Contributing
|
|
144
|
+
|
|
145
|
+
Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
|
|
146
|
+
|
|
147
|
+
## Acknowledgements
|
|
148
|
+
|
|
149
|
+
- [Django](https://www.djangoproject.com/)
|
|
150
|
+
- [OpenAI](https://www.openai.com/)
|
|
151
|
+
- [python-dotenv](https://github.com/theskumar/python-dotenv)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Django Explain Errors Middleware
|
|
2
|
+
|
|
3
|
+
This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It uses an environment variable to securely manage the OpenAI API key.
|
|
4
|
+
|
|
5
|
+
The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Captures Django errors and exceptions
|
|
10
|
+
- Uses OpenAI to explain the error
|
|
11
|
+
- Securely manages the OpenAI API key using environment variables
|
|
12
|
+
- Works with both sync (WSGI) and async (ASGI) views
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
1. Install django-explain-errors by running:
|
|
17
|
+
```bash
|
|
18
|
+
pip install django-explain-errors
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. **Add the middleware to your Django project**:
|
|
22
|
+
|
|
23
|
+
- Open your `settings.py` file and add the middleware to the `MIDDLEWARE` list. Ensure that the middleware is added last in the list:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
MIDDLEWARE = [
|
|
27
|
+
...
|
|
28
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. **Set up environment variables**:
|
|
33
|
+
|
|
34
|
+
- Create a `.env` file in your project's root directory and add your OpenAI API key. Alternatively, you can set the API key in `settings.py`:
|
|
35
|
+
|
|
36
|
+
```plaintext
|
|
37
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
1. **Ensure DEBUG is set to True**:
|
|
43
|
+
|
|
44
|
+
Open your `settings.py` file and set:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
DEBUG = True
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. **Trigger an error in your Django application**:
|
|
51
|
+
|
|
52
|
+
The middleware will capture the error, send it to OpenAI for explanation, and print the explanation to stdout. When an exception is caught, it returns a JSON `500` response containing the error message and the explanation.
|
|
53
|
+
|
|
54
|
+
## Async Support
|
|
55
|
+
|
|
56
|
+
The middleware exposes both `sync_capable = True` and `async_capable = True`. At initialization it inspects `get_response` to decide whether it is part of a sync or async chain:
|
|
57
|
+
|
|
58
|
+
- Under WSGI (for example `runserver` with sync views), requests flow through the synchronous handler.
|
|
59
|
+
- Under ASGI (for example with async views), requests are awaited through the async handler. The blocking OpenAI call is offloaded with `asgiref.sync.sync_to_async` so the event loop is not blocked.
|
|
60
|
+
|
|
61
|
+
No additional settings are needed. Place the middleware last in `MIDDLEWARE` for both modes.
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
| Setting / variable | Required | Description |
|
|
66
|
+
| ------------------ | -------- | ----------- |
|
|
67
|
+
| `OPENAI_API_KEY` (env or settings) | Yes, when `DEBUG=True` | API key used to authenticate with OpenAI. Read first from the environment, then from `settings`. |
|
|
68
|
+
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
69
|
+
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
70
|
+
| `OPENAI_MAX_TOKENS` | No | Maximum tokens in the explanation. Defaults to `150`. |
|
|
71
|
+
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
|
|
72
|
+
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
|
|
73
|
+
|
|
74
|
+
## Example
|
|
75
|
+
|
|
76
|
+
Here is an example of how to use the middleware in a Django project:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# settings.py
|
|
80
|
+
|
|
81
|
+
DEBUG = True
|
|
82
|
+
|
|
83
|
+
MIDDLEWARE = [
|
|
84
|
+
...
|
|
85
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# .env
|
|
89
|
+
|
|
90
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
When an error occurs, you will see an explanation printed to stdout.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
98
|
+
|
|
99
|
+
## Contributing
|
|
100
|
+
|
|
101
|
+
Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
|
|
102
|
+
|
|
103
|
+
## Acknowledgements
|
|
104
|
+
|
|
105
|
+
- [Django](https://www.djangoproject.com/)
|
|
106
|
+
- [OpenAI](https://www.openai.com/)
|
|
107
|
+
- [python-dotenv](https://github.com/theskumar/python-dotenv)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Django Explain Errors Middleware
|
|
2
|
+
|
|
3
|
+
This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It uses an environment variable to securely manage the OpenAI API key.
|
|
4
|
+
|
|
5
|
+
The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Captures Django errors and exceptions
|
|
10
|
+
- Uses OpenAI to explain the error
|
|
11
|
+
- Securely manages the OpenAI API key using environment variables
|
|
12
|
+
- Works with both sync (WSGI) and async (ASGI) views
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
1. Install django-explain-errors by running:
|
|
17
|
+
```bash
|
|
18
|
+
pip install django-explain-errors
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. **Add the middleware to your Django project**:
|
|
22
|
+
|
|
23
|
+
- Open your `settings.py` file and add the middleware to the `MIDDLEWARE` list. Ensure that the middleware is added last in the list:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
MIDDLEWARE = [
|
|
27
|
+
...
|
|
28
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. **Set up environment variables**:
|
|
33
|
+
|
|
34
|
+
- Create a `.env` file in your project's root directory and add your OpenAI API key. Alternatively, you can set the API key in `settings.py`:
|
|
35
|
+
|
|
36
|
+
```plaintext
|
|
37
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
1. **Ensure DEBUG is set to True**:
|
|
43
|
+
|
|
44
|
+
Open your `settings.py` file and set:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
DEBUG = True
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. **Trigger an error in your Django application**:
|
|
51
|
+
|
|
52
|
+
The middleware will capture the error, send it to OpenAI for explanation, and print the explanation to stdout. When an exception is caught, it returns a JSON `500` response containing the error message and the explanation.
|
|
53
|
+
|
|
54
|
+
## Async Support
|
|
55
|
+
|
|
56
|
+
The middleware exposes both `sync_capable = True` and `async_capable = True`. At initialization it inspects `get_response` to decide whether it is part of a sync or async chain:
|
|
57
|
+
|
|
58
|
+
- Under WSGI (for example `runserver` with sync views), requests flow through the synchronous handler.
|
|
59
|
+
- Under ASGI (for example with async views), requests are awaited through the async handler. The blocking OpenAI call is offloaded with `asgiref.sync.sync_to_async` so the event loop is not blocked.
|
|
60
|
+
|
|
61
|
+
No additional settings are needed. Place the middleware last in `MIDDLEWARE` for both modes.
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
| Setting / variable | Required | Description |
|
|
66
|
+
| ------------------ | -------- | ----------- |
|
|
67
|
+
| `OPENAI_API_KEY` (env or settings) | Yes, when `DEBUG=True` | API key used to authenticate with OpenAI. Read first from the environment, then from `settings`. |
|
|
68
|
+
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
69
|
+
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
70
|
+
| `OPENAI_MAX_TOKENS` | No | Maximum tokens in the explanation. Defaults to `150`. |
|
|
71
|
+
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
|
|
72
|
+
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
|
|
73
|
+
|
|
74
|
+
## Example
|
|
75
|
+
|
|
76
|
+
Here is an example of how to use the middleware in a Django project:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# settings.py
|
|
80
|
+
|
|
81
|
+
DEBUG = True
|
|
82
|
+
|
|
83
|
+
MIDDLEWARE = [
|
|
84
|
+
...
|
|
85
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# .env
|
|
89
|
+
|
|
90
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
When an error occurs, you will see an explanation printed to stdout.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
98
|
+
|
|
99
|
+
## Contributing
|
|
100
|
+
|
|
101
|
+
Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
|
|
102
|
+
|
|
103
|
+
## Acknowledgements
|
|
104
|
+
|
|
105
|
+
- [Django](https://www.djangoproject.com/)
|
|
106
|
+
- [OpenAI](https://www.openai.com/)
|
|
107
|
+
- [python-dotenv](https://github.com/theskumar/python-dotenv)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-explain-errors
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Django middleware that captures errors and exceptions, sends them to OpenAI for a detailed explanation, and prints the explanation to stdout when debug mode is enabled. Supports both sync and async views.
|
|
5
|
+
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
|
+
Author: topunix
|
|
7
|
+
Author-email: topunixguy@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 4.2
|
|
13
|
+
Classifier: Framework :: Django :: 5.0
|
|
14
|
+
Classifier: Framework :: Django :: 5.1
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: Django>=4.2
|
|
30
|
+
Requires-Dist: openai>=1.0
|
|
31
|
+
Requires-Dist: python-dotenv>=1.0
|
|
32
|
+
Requires-Dist: asgiref>=3.6
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
# Django Explain Errors Middleware
|
|
46
|
+
|
|
47
|
+
This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It uses an environment variable to securely manage the OpenAI API key.
|
|
48
|
+
|
|
49
|
+
The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- Captures Django errors and exceptions
|
|
54
|
+
- Uses OpenAI to explain the error
|
|
55
|
+
- Securely manages the OpenAI API key using environment variables
|
|
56
|
+
- Works with both sync (WSGI) and async (ASGI) views
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
1. Install django-explain-errors by running:
|
|
61
|
+
```bash
|
|
62
|
+
pip install django-explain-errors
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. **Add the middleware to your Django project**:
|
|
66
|
+
|
|
67
|
+
- Open your `settings.py` file and add the middleware to the `MIDDLEWARE` list. Ensure that the middleware is added last in the list:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
MIDDLEWARE = [
|
|
71
|
+
...
|
|
72
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
73
|
+
]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
3. **Set up environment variables**:
|
|
77
|
+
|
|
78
|
+
- Create a `.env` file in your project's root directory and add your OpenAI API key. Alternatively, you can set the API key in `settings.py`:
|
|
79
|
+
|
|
80
|
+
```plaintext
|
|
81
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
1. **Ensure DEBUG is set to True**:
|
|
87
|
+
|
|
88
|
+
Open your `settings.py` file and set:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
DEBUG = True
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
2. **Trigger an error in your Django application**:
|
|
95
|
+
|
|
96
|
+
The middleware will capture the error, send it to OpenAI for explanation, and print the explanation to stdout. When an exception is caught, it returns a JSON `500` response containing the error message and the explanation.
|
|
97
|
+
|
|
98
|
+
## Async Support
|
|
99
|
+
|
|
100
|
+
The middleware exposes both `sync_capable = True` and `async_capable = True`. At initialization it inspects `get_response` to decide whether it is part of a sync or async chain:
|
|
101
|
+
|
|
102
|
+
- Under WSGI (for example `runserver` with sync views), requests flow through the synchronous handler.
|
|
103
|
+
- Under ASGI (for example with async views), requests are awaited through the async handler. The blocking OpenAI call is offloaded with `asgiref.sync.sync_to_async` so the event loop is not blocked.
|
|
104
|
+
|
|
105
|
+
No additional settings are needed. Place the middleware last in `MIDDLEWARE` for both modes.
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
| Setting / variable | Required | Description |
|
|
110
|
+
| ------------------ | -------- | ----------- |
|
|
111
|
+
| `OPENAI_API_KEY` (env or settings) | Yes, when `DEBUG=True` | API key used to authenticate with OpenAI. Read first from the environment, then from `settings`. |
|
|
112
|
+
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
113
|
+
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
114
|
+
| `OPENAI_MAX_TOKENS` | No | Maximum tokens in the explanation. Defaults to `150`. |
|
|
115
|
+
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
|
|
116
|
+
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
|
|
117
|
+
|
|
118
|
+
## Example
|
|
119
|
+
|
|
120
|
+
Here is an example of how to use the middleware in a Django project:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# settings.py
|
|
124
|
+
|
|
125
|
+
DEBUG = True
|
|
126
|
+
|
|
127
|
+
MIDDLEWARE = [
|
|
128
|
+
...
|
|
129
|
+
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# .env
|
|
133
|
+
|
|
134
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
When an error occurs, you will see an explanation printed to stdout.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
142
|
+
|
|
143
|
+
## Contributing
|
|
144
|
+
|
|
145
|
+
Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
|
|
146
|
+
|
|
147
|
+
## Acknowledgements
|
|
148
|
+
|
|
149
|
+
- [Django](https://www.djangoproject.com/)
|
|
150
|
+
- [OpenAI](https://www.openai.com/)
|
|
151
|
+
- [python-dotenv](https://github.com/theskumar/python-dotenv)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
README.rst
|
|
5
|
+
setup.py
|
|
6
|
+
django_explain_errors.egg-info/PKG-INFO
|
|
7
|
+
django_explain_errors.egg-info/SOURCES.txt
|
|
8
|
+
django_explain_errors.egg-info/dependency_links.txt
|
|
9
|
+
django_explain_errors.egg-info/requires.txt
|
|
10
|
+
django_explain_errors.egg-info/top_level.txt
|
|
11
|
+
explain_errors/__init__.py
|
|
12
|
+
explain_errors/middleware.py
|
|
13
|
+
tests/test_middleware.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
explain_errors
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.http import JsonResponse
|
|
9
|
+
from asgiref.sync import sync_to_async
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExplainErrorsMiddleware:
|
|
13
|
+
"""
|
|
14
|
+
Captures unhandled exceptions, asks OpenAI to explain them, and prints the
|
|
15
|
+
explanation to stdout when DEBUG is True. Supports both sync (WSGI) and
|
|
16
|
+
async (ASGI) views.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
async_capable = True
|
|
20
|
+
sync_capable = True
|
|
21
|
+
|
|
22
|
+
def __init__(self, get_response):
|
|
23
|
+
self.get_response = get_response
|
|
24
|
+
self._is_async = asyncio.iscoroutinefunction(get_response)
|
|
25
|
+
self.api_called = False
|
|
26
|
+
self.openai_client = None
|
|
27
|
+
|
|
28
|
+
if settings.DEBUG:
|
|
29
|
+
# Load environment variables from .env file
|
|
30
|
+
load_dotenv()
|
|
31
|
+
# Get the OpenAI API key from environment variable (or settings)
|
|
32
|
+
openai_api_key = os.getenv(
|
|
33
|
+
"OPENAI_API_KEY", getattr(settings, "OPENAI_API_KEY", None)
|
|
34
|
+
)
|
|
35
|
+
if not openai_api_key:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"OpenAI API key not found. Please set the OPENAI_API_KEY "
|
|
38
|
+
"environment variable."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Configurable via settings, with sensible defaults.
|
|
42
|
+
self.model = getattr(settings, "OPENAI_MODEL", "gpt-4o-mini")
|
|
43
|
+
self.max_tokens = getattr(settings, "OPENAI_MAX_TOKENS", 150)
|
|
44
|
+
timeout = getattr(settings, "OPENAI_TIMEOUT", 10)
|
|
45
|
+
|
|
46
|
+
self.openai_client = OpenAI(api_key=openai_api_key, timeout=timeout)
|
|
47
|
+
|
|
48
|
+
def __call__(self, request):
|
|
49
|
+
# Delegate to the async path when wrapped around an async view chain.
|
|
50
|
+
if self._is_async:
|
|
51
|
+
return self.__acall__(request)
|
|
52
|
+
return self._sync_handler(request)
|
|
53
|
+
|
|
54
|
+
# --------- Sync path ----------
|
|
55
|
+
def _sync_handler(self, request):
|
|
56
|
+
try:
|
|
57
|
+
response = self.get_response(request)
|
|
58
|
+
except Exception as exception:
|
|
59
|
+
return self.process_exception(request, exception)
|
|
60
|
+
return response
|
|
61
|
+
|
|
62
|
+
# --------- Async path ----------
|
|
63
|
+
async def __acall__(self, request):
|
|
64
|
+
try:
|
|
65
|
+
response = await self.get_response(request)
|
|
66
|
+
except Exception as exception:
|
|
67
|
+
# process_exception performs blocking OpenAI I/O, so run it in a
|
|
68
|
+
# thread to keep the event loop free.
|
|
69
|
+
return await sync_to_async(self.process_exception)(request, exception)
|
|
70
|
+
return response
|
|
71
|
+
|
|
72
|
+
def process_exception(self, request, exception):
|
|
73
|
+
if not settings.DEBUG:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
explanation = None
|
|
77
|
+
if not self.api_called:
|
|
78
|
+
# Get the exception traceback, trimmed to the most recent frames to
|
|
79
|
+
# cap token usage and stay within the model's context window.
|
|
80
|
+
tb = traceback.format_exc()
|
|
81
|
+
max_tb_chars = getattr(settings, "OPENAI_MAX_TRACEBACK_CHARS", 3000)
|
|
82
|
+
if len(tb) > max_tb_chars:
|
|
83
|
+
tb = "...(truncated)...\n" + tb[-max_tb_chars:]
|
|
84
|
+
# Construct the prompt
|
|
85
|
+
prompt = f"Explain the following Django error in simple terms:\n\n{tb}"
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Call OpenAI API
|
|
89
|
+
response = self.openai_client.chat.completions.create(
|
|
90
|
+
model=self.model,
|
|
91
|
+
messages=[
|
|
92
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
93
|
+
{"role": "user", "content": prompt},
|
|
94
|
+
],
|
|
95
|
+
max_tokens=self.max_tokens,
|
|
96
|
+
)
|
|
97
|
+
explanation = response.choices[0].message.content
|
|
98
|
+
|
|
99
|
+
# Print the explanation to stdout
|
|
100
|
+
print("Error Explanation by OpenAI:\n", explanation)
|
|
101
|
+
self.api_called = True # Set flag after the call
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# If the OpenAI call fails, surface the failure but still return
|
|
104
|
+
# a 500 so the request lifecycle completes cleanly.
|
|
105
|
+
print("Failed to get an explanation from OpenAI:", e)
|
|
106
|
+
|
|
107
|
+
return JsonResponse(
|
|
108
|
+
{"error": "An error occurred.", "message": explanation}, status=500
|
|
109
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from setuptools import find_packages, setup
|
|
3
|
+
|
|
4
|
+
# allow setup.py to be run from any path
|
|
5
|
+
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
|
6
|
+
|
|
7
|
+
# get README
|
|
8
|
+
with open('README.md', encoding='utf-8') as f:
|
|
9
|
+
long_description = f.read()
|
|
10
|
+
|
|
11
|
+
setup(
|
|
12
|
+
name='django-explain-errors',
|
|
13
|
+
version='0.2.0',
|
|
14
|
+
packages=find_packages(exclude=['tests', 'tests.*']),
|
|
15
|
+
description='Django middleware that captures errors and exceptions, sends them to OpenAI for a detailed explanation, and prints the explanation to stdout when debug mode is enabled. Supports both sync and async views.',
|
|
16
|
+
long_description_content_type='text/markdown',
|
|
17
|
+
long_description=long_description,
|
|
18
|
+
python_requires='>=3.9',
|
|
19
|
+
install_requires=[
|
|
20
|
+
'Django>=4.2',
|
|
21
|
+
'openai>=1.0',
|
|
22
|
+
'python-dotenv>=1.0',
|
|
23
|
+
'asgiref>=3.6',
|
|
24
|
+
],
|
|
25
|
+
url='https://github.com/topunix/django-explain-errors',
|
|
26
|
+
author='topunix',
|
|
27
|
+
author_email='topunixguy@gmail.com',
|
|
28
|
+
license='MIT',
|
|
29
|
+
classifiers=[
|
|
30
|
+
'Development Status :: 4 - Beta',
|
|
31
|
+
'Environment :: Web Environment',
|
|
32
|
+
'Framework :: Django',
|
|
33
|
+
'Framework :: Django :: 4.2',
|
|
34
|
+
'Framework :: Django :: 5.0',
|
|
35
|
+
'Framework :: Django :: 5.1',
|
|
36
|
+
'Intended Audience :: Developers',
|
|
37
|
+
'License :: OSI Approved :: MIT License',
|
|
38
|
+
'Operating System :: OS Independent',
|
|
39
|
+
'Programming Language :: Python',
|
|
40
|
+
'Programming Language :: Python :: 3',
|
|
41
|
+
'Programming Language :: Python :: 3.9',
|
|
42
|
+
'Programming Language :: Python :: 3.10',
|
|
43
|
+
'Programming Language :: Python :: 3.11',
|
|
44
|
+
'Programming Language :: Python :: 3.12',
|
|
45
|
+
'Programming Language :: Python :: 3.13',
|
|
46
|
+
'Topic :: Internet :: WWW/HTTP',
|
|
47
|
+
],
|
|
48
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from django.http import JsonResponse, HttpResponse
|
|
5
|
+
from django.test import SimpleTestCase, RequestFactory, AsyncRequestFactory, override_settings
|
|
6
|
+
|
|
7
|
+
from explain_errors.middleware import ExplainErrorsMiddleware
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _mock_openai():
|
|
11
|
+
"""Return a patch context for OpenAI plus a configured fake client."""
|
|
12
|
+
patcher = patch("explain_errors.middleware.OpenAI")
|
|
13
|
+
mock_cls = patcher.start()
|
|
14
|
+
client = MagicMock()
|
|
15
|
+
client.chat.completions.create.return_value = MagicMock(
|
|
16
|
+
choices=[MagicMock(message=MagicMock(content="Mocked explanation."))]
|
|
17
|
+
)
|
|
18
|
+
mock_cls.return_value = client
|
|
19
|
+
return patcher, client
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@override_settings(DEBUG=True, OPENAI_API_KEY="test-key")
|
|
23
|
+
class ExplainErrorsMiddlewareTest(SimpleTestCase):
|
|
24
|
+
|
|
25
|
+
def setUp(self):
|
|
26
|
+
self.factory = RequestFactory()
|
|
27
|
+
self.patcher, self.client = _mock_openai()
|
|
28
|
+
self.addCleanup(self.patcher.stop)
|
|
29
|
+
|
|
30
|
+
def _build(self, get_response):
|
|
31
|
+
return ExplainErrorsMiddleware(get_response)
|
|
32
|
+
|
|
33
|
+
# ---- capability markers ----
|
|
34
|
+
def test_capability_markers(self):
|
|
35
|
+
self.assertTrue(ExplainErrorsMiddleware.async_capable)
|
|
36
|
+
self.assertTrue(ExplainErrorsMiddleware.sync_capable)
|
|
37
|
+
|
|
38
|
+
def test_is_async_detection(self):
|
|
39
|
+
sync_mw = self._build(lambda r: HttpResponse("ok"))
|
|
40
|
+
self.assertFalse(sync_mw._is_async)
|
|
41
|
+
|
|
42
|
+
async def aget(r):
|
|
43
|
+
return HttpResponse("ok")
|
|
44
|
+
|
|
45
|
+
self.assertTrue(self._build(aget)._is_async)
|
|
46
|
+
|
|
47
|
+
# ---- process_exception (original test, adapted) ----
|
|
48
|
+
def test_process_exception_with_error(self):
|
|
49
|
+
request = self.factory.get("/")
|
|
50
|
+
middleware = self._build(lambda request: None)
|
|
51
|
+
|
|
52
|
+
response = middleware.process_exception(request, Exception("Test Exception"))
|
|
53
|
+
|
|
54
|
+
self.assertIsInstance(response, JsonResponse)
|
|
55
|
+
self.assertEqual(response.status_code, 500)
|
|
56
|
+
self.assertIn("error", json.loads(response.content))
|
|
57
|
+
|
|
58
|
+
def test_api_called_flag_prevents_second_call(self):
|
|
59
|
+
request = self.factory.get("/")
|
|
60
|
+
middleware = self._build(lambda request: None)
|
|
61
|
+
|
|
62
|
+
middleware.process_exception(request, Exception("Test"))
|
|
63
|
+
self.assertTrue(middleware.api_called)
|
|
64
|
+
|
|
65
|
+
resp = middleware.process_exception(request, Exception("Test"))
|
|
66
|
+
self.assertEqual(self.client.chat.completions.create.call_count, 1)
|
|
67
|
+
self.assertEqual(resp.status_code, 500)
|
|
68
|
+
|
|
69
|
+
# ---- sync request path ----
|
|
70
|
+
def test_sync_passthrough(self):
|
|
71
|
+
sentinel = HttpResponse("ok")
|
|
72
|
+
mw = self._build(lambda r: sentinel)
|
|
73
|
+
self.assertIs(mw(self.factory.get("/")), sentinel)
|
|
74
|
+
|
|
75
|
+
def test_sync_exception_returns_500(self):
|
|
76
|
+
def boom(r):
|
|
77
|
+
raise ValueError("boom")
|
|
78
|
+
|
|
79
|
+
resp = self._build(boom)(self.factory.get("/"))
|
|
80
|
+
self.assertIsInstance(resp, JsonResponse)
|
|
81
|
+
self.assertEqual(resp.status_code, 500)
|
|
82
|
+
self.assertIn("error", json.loads(resp.content))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@override_settings(DEBUG=True, OPENAI_API_KEY="test-key")
|
|
86
|
+
class ExplainErrorsMiddlewareAsyncTest(SimpleTestCase):
|
|
87
|
+
|
|
88
|
+
def setUp(self):
|
|
89
|
+
self.factory = AsyncRequestFactory()
|
|
90
|
+
self.patcher, self.client = _mock_openai()
|
|
91
|
+
self.addCleanup(self.patcher.stop)
|
|
92
|
+
|
|
93
|
+
async def test_async_passthrough(self):
|
|
94
|
+
sentinel = HttpResponse("ok")
|
|
95
|
+
|
|
96
|
+
async def aget(r):
|
|
97
|
+
return sentinel
|
|
98
|
+
|
|
99
|
+
mw = ExplainErrorsMiddleware(aget)
|
|
100
|
+
self.assertIs(await mw(self.factory.get("/")), sentinel)
|
|
101
|
+
|
|
102
|
+
async def test_async_exception_returns_500(self):
|
|
103
|
+
async def boom(r):
|
|
104
|
+
raise ValueError("async boom")
|
|
105
|
+
|
|
106
|
+
mw = ExplainErrorsMiddleware(boom)
|
|
107
|
+
resp = await mw(self.factory.get("/"))
|
|
108
|
+
self.assertIsInstance(resp, JsonResponse)
|
|
109
|
+
self.assertEqual(resp.status_code, 500)
|
|
110
|
+
self.assertIn("error", json.loads(resp.content))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@override_settings(DEBUG=False)
|
|
114
|
+
class ExplainErrorsMiddlewareDebugOffTest(SimpleTestCase):
|
|
115
|
+
|
|
116
|
+
def test_process_exception_returns_none_when_debug_off(self):
|
|
117
|
+
factory = RequestFactory()
|
|
118
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
119
|
+
self.assertIsNone(mw.process_exception(factory.get("/"), Exception("x")))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@override_settings(DEBUG=True, OPENAI_API_KEY="test-key")
|
|
123
|
+
class OpenAICallConfigTest(SimpleTestCase):
|
|
124
|
+
|
|
125
|
+
def setUp(self):
|
|
126
|
+
self.factory = RequestFactory()
|
|
127
|
+
self.patcher, self.client = _mock_openai()
|
|
128
|
+
self.addCleanup(self.patcher.stop)
|
|
129
|
+
|
|
130
|
+
def test_default_model(self):
|
|
131
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
132
|
+
mw.process_exception(self.factory.get("/"), Exception("x"))
|
|
133
|
+
kwargs = self.client.chat.completions.create.call_args.kwargs
|
|
134
|
+
self.assertEqual(kwargs["model"], "gpt-4o-mini")
|
|
135
|
+
self.assertEqual(kwargs["max_tokens"], 150)
|
|
136
|
+
|
|
137
|
+
@override_settings(OPENAI_MODEL="gpt-5-mini", OPENAI_MAX_TOKENS=50)
|
|
138
|
+
def test_configurable_model_and_tokens(self):
|
|
139
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
140
|
+
mw.process_exception(self.factory.get("/"), Exception("x"))
|
|
141
|
+
kwargs = self.client.chat.completions.create.call_args.kwargs
|
|
142
|
+
self.assertEqual(kwargs["model"], "gpt-5-mini")
|
|
143
|
+
self.assertEqual(kwargs["max_tokens"], 50)
|
|
144
|
+
|
|
145
|
+
@override_settings(OPENAI_MAX_TRACEBACK_CHARS=200)
|
|
146
|
+
def test_traceback_is_truncated(self):
|
|
147
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
148
|
+
try:
|
|
149
|
+
raise ValueError("x" * 5000)
|
|
150
|
+
except ValueError as exc:
|
|
151
|
+
mw.process_exception(self.factory.get("/"), exc)
|
|
152
|
+
prompt = self.client.chat.completions.create.call_args.kwargs["messages"][1]["content"]
|
|
153
|
+
self.assertIn("(truncated)", prompt)
|
|
154
|
+
self.assertLess(len(prompt), 400)
|
|
155
|
+
|
|
156
|
+
def test_timeout_passed_to_client(self):
|
|
157
|
+
with override_settings(OPENAI_TIMEOUT=7):
|
|
158
|
+
with patch("explain_errors.middleware.OpenAI") as mock_cls:
|
|
159
|
+
ExplainErrorsMiddleware(lambda r: None)
|
|
160
|
+
self.assertEqual(mock_cls.call_args.kwargs["timeout"], 7)
|