django-qlab 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.
- django_qlab-0.3.0/LICENSE +21 -0
- django_qlab-0.3.0/PKG-INFO +313 -0
- django_qlab-0.3.0/README.md +273 -0
- django_qlab-0.3.0/pyproject.toml +87 -0
- django_qlab-0.3.0/qlab/__init__.py +6 -0
- django_qlab-0.3.0/qlab/admin.py +55 -0
- django_qlab-0.3.0/qlab/api_views.py +188 -0
- django_qlab-0.3.0/qlab/apps.py +7 -0
- django_qlab-0.3.0/qlab/helpers.py +741 -0
- django_qlab-0.3.0/qlab/migrations/0001_initial.py +154 -0
- django_qlab-0.3.0/qlab/migrations/__init__.py +0 -0
- django_qlab-0.3.0/qlab/mixins.py +729 -0
- django_qlab-0.3.0/qlab/model_validation.py +260 -0
- django_qlab-0.3.0/qlab/models.py +96 -0
- django_qlab-0.3.0/qlab/pydantic_filters.py +288 -0
- django_qlab-0.3.0/qlab/serializers.py +248 -0
- django_qlab-0.3.0/qlab/settings.py +57 -0
- django_qlab-0.3.0/qlab/static/qlab/index.html +20 -0
- django_qlab-0.3.0/qlab/static/qlab/qlab.css +1 -0
- django_qlab-0.3.0/qlab/static/qlab/qlab.js +120 -0
- django_qlab-0.3.0/qlab/templates/qlab/index.html +22 -0
- django_qlab-0.3.0/qlab/urls.py +57 -0
- django_qlab-0.3.0/qlab/views.py +37 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Tabea Hoehne
|
|
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,313 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-qlab
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Dynamic query API for Django REST Framework
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: django,rest,api,query,filter,metadata
|
|
8
|
+
Author: Tabea Hoehne
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 4.0
|
|
12
|
+
Classifier: Framework :: Django :: 4.1
|
|
13
|
+
Classifier: Framework :: Django :: 4.2
|
|
14
|
+
Classifier: Framework :: Django :: 5.0
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: Django (>=4.0)
|
|
25
|
+
Requires-Dist: black (>=23.0) ; extra == "dev"
|
|
26
|
+
Requires-Dist: build (>=1.2) ; extra == "dev"
|
|
27
|
+
Requires-Dist: djangorestframework (>=3.14)
|
|
28
|
+
Requires-Dist: drf-spectacular (>=0.26)
|
|
29
|
+
Requires-Dist: pre-commit (>=4.0) ; extra == "dev"
|
|
30
|
+
Requires-Dist: pydantic (>=2.0)
|
|
31
|
+
Requires-Dist: pytest (>=7.0) ; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-django (>=4.5) ; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff (>=0.1) ; extra == "dev"
|
|
34
|
+
Requires-Dist: twine (>=5.1) ; extra == "dev"
|
|
35
|
+
Project-URL: Documentation, https://github.com/tabeahoehne132/django-qlab#readme
|
|
36
|
+
Project-URL: Homepage, https://github.com/tabeahoehne132/django-qlab
|
|
37
|
+
Project-URL: Repository, https://github.com/tabeahoehne132/django-qlab
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# django-qlab
|
|
41
|
+
|
|
42
|
+
Dynamic query API and bundled React UI for Django REST Framework.
|
|
43
|
+
Inspect model data, run filtered queries, save and replay them — no custom views required.
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/django-qlab/)
|
|
46
|
+
[](https://pypi.org/project/django-qlab/)
|
|
47
|
+
[](https://pypi.org/project/django-qlab/)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Screenshots
|
|
53
|
+
|
|
54
|
+
Dashboard:
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
Query builder:
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## What ships
|
|
65
|
+
|
|
66
|
+
- Dynamic model querying with field selection and nested AND / OR / NOT filters
|
|
67
|
+
- Metadata endpoint — fields, types, relations, operators and autocomplete
|
|
68
|
+
- Neighborhood endpoint for relation exploration
|
|
69
|
+
- Bundled React + TypeScript UI served directly from `qlab.urls`
|
|
70
|
+
- Saved queries with create, update, delete and bulk operations
|
|
71
|
+
- Query run history with replay and save-from-history
|
|
72
|
+
- Per-user settings stored in the database
|
|
73
|
+
- Django admin integration for all persistence models
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install django-qlab
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Add the required apps:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# settings.py
|
|
87
|
+
INSTALLED_APPS = [
|
|
88
|
+
...
|
|
89
|
+
"django.contrib.staticfiles",
|
|
90
|
+
"rest_framework",
|
|
91
|
+
"drf_spectacular",
|
|
92
|
+
"qlab",
|
|
93
|
+
]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Mount the URLs:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# urls.py
|
|
100
|
+
from django.urls import include, path
|
|
101
|
+
|
|
102
|
+
urlpatterns = [
|
|
103
|
+
...
|
|
104
|
+
path("qlab/", include("qlab.urls")),
|
|
105
|
+
]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Run migrations and collect static files:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python manage.py migrate
|
|
112
|
+
python manage.py collectstatic
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Open `/qlab/` in your browser — done.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Setup in 5 steps
|
|
120
|
+
|
|
121
|
+
1. `pip install django-qlab`
|
|
122
|
+
2. Add `qlab` (and its dependencies) to `INSTALLED_APPS`
|
|
123
|
+
3. Include `qlab.urls` in your URL config
|
|
124
|
+
4. `python manage.py migrate && python manage.py collectstatic`
|
|
125
|
+
5. Open `/qlab/`
|
|
126
|
+
|
|
127
|
+
No separate frontend server. No npm. The compiled UI ships with the package.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Optional: login protection
|
|
132
|
+
|
|
133
|
+
Subclass `QLabView` to enforce authentication on the UI entrypoint:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# urls.py
|
|
137
|
+
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
138
|
+
from django.urls import include, path
|
|
139
|
+
from qlab.views import QLabView
|
|
140
|
+
|
|
141
|
+
class SecuredQLabView(LoginRequiredMixin, QLabView):
|
|
142
|
+
login_url = "/admin/login/"
|
|
143
|
+
|
|
144
|
+
urlpatterns = [
|
|
145
|
+
path("qlab/", SecuredQLabView.as_view(), name="qlab"),
|
|
146
|
+
path("qlab/", include("qlab.urls")),
|
|
147
|
+
]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Optional: queryset scoping
|
|
153
|
+
|
|
154
|
+
Override `QLabFrontendApiViewSet` to scope queries per user, tenant or business group:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from rest_framework import permissions
|
|
158
|
+
from qlab.api_views import QLabFrontendApiViewSet
|
|
159
|
+
|
|
160
|
+
class ScopedQLabViewSet(QLabFrontendApiViewSet):
|
|
161
|
+
permission_classes = [permissions.IsAuthenticated]
|
|
162
|
+
|
|
163
|
+
def get_queryset(self, model):
|
|
164
|
+
return model.objects.filter(tenant=self.request.user.tenant)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then mount the scoped ViewSet before the default `qlab.urls` include:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
urlpatterns = [
|
|
171
|
+
path("qlab/", SecuredQLabView.as_view(), name="qlab"),
|
|
172
|
+
path("qlab/api/query/", ScopedQLabViewSet.as_view({"post": "post"}), name="qlab-query"),
|
|
173
|
+
path("qlab/", include("qlab.urls")),
|
|
174
|
+
]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Settings
|
|
180
|
+
|
|
181
|
+
Add a `QLAB_SETTINGS` dict to your Django settings to override defaults:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# settings.py
|
|
185
|
+
QLAB_SETTINGS = {
|
|
186
|
+
"DEFAULT_APP_LABEL": "myapp", # pre-select this app in the UI
|
|
187
|
+
"PAGE_SIZE": 100, # default page size
|
|
188
|
+
"MAX_PAGE_SIZE": 500, # hard cap per request
|
|
189
|
+
"MAX_RELATION_DEPTH": 2, # how deep relation graphs expand
|
|
190
|
+
"MAX_FILTER_CONDITIONS": 10, # max filter nodes per query
|
|
191
|
+
"MAX_NODES": 100, # max records returned by neighborhood
|
|
192
|
+
"ALLOWED_APPS": [], # restrict to specific app labels (empty = all)
|
|
193
|
+
"RESTRICTED_MODELS": [], # block specific model names globally
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## API surface
|
|
200
|
+
|
|
201
|
+
All routes are mounted relative to the prefix you chose (e.g. `/qlab/`):
|
|
202
|
+
|
|
203
|
+
| Method | Path | Description |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `GET` | `/` | Bundled React UI |
|
|
206
|
+
| `GET` | `/api/bootstrap/` | Initial data load (models, settings) |
|
|
207
|
+
| `POST` | `/api/query/` | Run a filtered, paginated query |
|
|
208
|
+
| `POST` | `/api/metadata/` | Model field and relation schema |
|
|
209
|
+
| `POST` | `/api/neighborhood/` | Relation graph for a set of records |
|
|
210
|
+
| `GET / PATCH` | `/api/settings/` | Per-user UI settings |
|
|
211
|
+
| `GET / POST` | `/api/saved-queries/` | List and create saved queries |
|
|
212
|
+
| `GET / PATCH / DELETE` | `/api/saved-queries/<id>/` | Manage a single saved query |
|
|
213
|
+
| `POST` | `/api/saved-queries/<id>/run/` | Execute a saved query |
|
|
214
|
+
| `GET` | `/api/history/` | Query run history |
|
|
215
|
+
|
|
216
|
+
### Example query payload
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"model": "Device",
|
|
221
|
+
"app_label": "myapp",
|
|
222
|
+
"select_fields": ["id", "name", "status", "region"],
|
|
223
|
+
"filter_fields": {
|
|
224
|
+
"and_operation": [
|
|
225
|
+
{
|
|
226
|
+
"or_operation": [
|
|
227
|
+
{ "field": "status", "op": "is", "value": "active" },
|
|
228
|
+
{ "field": "status", "op": "is", "value": "maintenance" }
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"or_operation": [
|
|
233
|
+
{ "field": "region", "op": "is", "value": "DE" },
|
|
234
|
+
{ "field": "region", "op": "is", "value": "AT" }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
"page": 1,
|
|
240
|
+
"page_size": 100
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## UI capabilities
|
|
247
|
+
|
|
248
|
+
- **Dashboard** — model counts, saved query count and recent activity
|
|
249
|
+
- **Query builder** — field picker, nested `(a or b) and (x or y)` filter groups, CSV export, JSON copy
|
|
250
|
+
- **Models browser** — field types, nullability, filterable flags, relation inspection
|
|
251
|
+
- **Saved queries** — create, update, delete, bulk delete and run from the UI
|
|
252
|
+
- **History** — replay past runs, save from history, filter by model and time range
|
|
253
|
+
- **Settings** — page size, default app, theme
|
|
254
|
+
- **Light and dark mode**
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Django admin
|
|
259
|
+
|
|
260
|
+
The package registers the following models in Django admin:
|
|
261
|
+
|
|
262
|
+
| Model | Description |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `QLabUserSettings` | Per-user theme, page size and active tab |
|
|
265
|
+
| `SavedQuery` | Stored query payloads with metadata |
|
|
266
|
+
| `QueryRunHistory` | Execution log with status, duration and result snapshot |
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Requirements
|
|
271
|
+
|
|
272
|
+
| Package | Version |
|
|
273
|
+
|---|---|
|
|
274
|
+
| Python | ≥ 3.9 |
|
|
275
|
+
| Django | ≥ 4.0 |
|
|
276
|
+
| djangorestframework | ≥ 3.14 |
|
|
277
|
+
| pydantic | ≥ 2.0 |
|
|
278
|
+
| drf-spectacular | ≥ 0.26 |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Frontend development
|
|
283
|
+
|
|
284
|
+
This section is for maintainers working on the UI itself. Package consumers do not need npm.
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
cd frontend
|
|
288
|
+
npm install
|
|
289
|
+
npm run dev # dev server with HMR
|
|
290
|
+
npm run build # write compiled assets to qlab/static/qlab/
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Local demo
|
|
296
|
+
|
|
297
|
+
A gitignored demo project lives in `.local-demo/`:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
cd .local-demo
|
|
301
|
+
python manage.py migrate
|
|
302
|
+
python manage.py seed_demo_data
|
|
303
|
+
python manage.py runserver 8054
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Then open [http://127.0.0.1:8054/qlab/](http://127.0.0.1:8054/qlab/).
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT
|
|
313
|
+
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# django-qlab
|
|
2
|
+
|
|
3
|
+
Dynamic query API and bundled React UI for Django REST Framework.
|
|
4
|
+
Inspect model data, run filtered queries, save and replay them — no custom views required.
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/django-qlab/)
|
|
7
|
+
[](https://pypi.org/project/django-qlab/)
|
|
8
|
+
[](https://pypi.org/project/django-qlab/)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Screenshots
|
|
14
|
+
|
|
15
|
+
Dashboard:
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
Query builder:
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What ships
|
|
26
|
+
|
|
27
|
+
- Dynamic model querying with field selection and nested AND / OR / NOT filters
|
|
28
|
+
- Metadata endpoint — fields, types, relations, operators and autocomplete
|
|
29
|
+
- Neighborhood endpoint for relation exploration
|
|
30
|
+
- Bundled React + TypeScript UI served directly from `qlab.urls`
|
|
31
|
+
- Saved queries with create, update, delete and bulk operations
|
|
32
|
+
- Query run history with replay and save-from-history
|
|
33
|
+
- Per-user settings stored in the database
|
|
34
|
+
- Django admin integration for all persistence models
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install django-qlab
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add the required apps:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# settings.py
|
|
48
|
+
INSTALLED_APPS = [
|
|
49
|
+
...
|
|
50
|
+
"django.contrib.staticfiles",
|
|
51
|
+
"rest_framework",
|
|
52
|
+
"drf_spectacular",
|
|
53
|
+
"qlab",
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Mount the URLs:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# urls.py
|
|
61
|
+
from django.urls import include, path
|
|
62
|
+
|
|
63
|
+
urlpatterns = [
|
|
64
|
+
...
|
|
65
|
+
path("qlab/", include("qlab.urls")),
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Run migrations and collect static files:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python manage.py migrate
|
|
73
|
+
python manage.py collectstatic
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Open `/qlab/` in your browser — done.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Setup in 5 steps
|
|
81
|
+
|
|
82
|
+
1. `pip install django-qlab`
|
|
83
|
+
2. Add `qlab` (and its dependencies) to `INSTALLED_APPS`
|
|
84
|
+
3. Include `qlab.urls` in your URL config
|
|
85
|
+
4. `python manage.py migrate && python manage.py collectstatic`
|
|
86
|
+
5. Open `/qlab/`
|
|
87
|
+
|
|
88
|
+
No separate frontend server. No npm. The compiled UI ships with the package.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Optional: login protection
|
|
93
|
+
|
|
94
|
+
Subclass `QLabView` to enforce authentication on the UI entrypoint:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# urls.py
|
|
98
|
+
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
99
|
+
from django.urls import include, path
|
|
100
|
+
from qlab.views import QLabView
|
|
101
|
+
|
|
102
|
+
class SecuredQLabView(LoginRequiredMixin, QLabView):
|
|
103
|
+
login_url = "/admin/login/"
|
|
104
|
+
|
|
105
|
+
urlpatterns = [
|
|
106
|
+
path("qlab/", SecuredQLabView.as_view(), name="qlab"),
|
|
107
|
+
path("qlab/", include("qlab.urls")),
|
|
108
|
+
]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Optional: queryset scoping
|
|
114
|
+
|
|
115
|
+
Override `QLabFrontendApiViewSet` to scope queries per user, tenant or business group:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from rest_framework import permissions
|
|
119
|
+
from qlab.api_views import QLabFrontendApiViewSet
|
|
120
|
+
|
|
121
|
+
class ScopedQLabViewSet(QLabFrontendApiViewSet):
|
|
122
|
+
permission_classes = [permissions.IsAuthenticated]
|
|
123
|
+
|
|
124
|
+
def get_queryset(self, model):
|
|
125
|
+
return model.objects.filter(tenant=self.request.user.tenant)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Then mount the scoped ViewSet before the default `qlab.urls` include:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
urlpatterns = [
|
|
132
|
+
path("qlab/", SecuredQLabView.as_view(), name="qlab"),
|
|
133
|
+
path("qlab/api/query/", ScopedQLabViewSet.as_view({"post": "post"}), name="qlab-query"),
|
|
134
|
+
path("qlab/", include("qlab.urls")),
|
|
135
|
+
]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Settings
|
|
141
|
+
|
|
142
|
+
Add a `QLAB_SETTINGS` dict to your Django settings to override defaults:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
# settings.py
|
|
146
|
+
QLAB_SETTINGS = {
|
|
147
|
+
"DEFAULT_APP_LABEL": "myapp", # pre-select this app in the UI
|
|
148
|
+
"PAGE_SIZE": 100, # default page size
|
|
149
|
+
"MAX_PAGE_SIZE": 500, # hard cap per request
|
|
150
|
+
"MAX_RELATION_DEPTH": 2, # how deep relation graphs expand
|
|
151
|
+
"MAX_FILTER_CONDITIONS": 10, # max filter nodes per query
|
|
152
|
+
"MAX_NODES": 100, # max records returned by neighborhood
|
|
153
|
+
"ALLOWED_APPS": [], # restrict to specific app labels (empty = all)
|
|
154
|
+
"RESTRICTED_MODELS": [], # block specific model names globally
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## API surface
|
|
161
|
+
|
|
162
|
+
All routes are mounted relative to the prefix you chose (e.g. `/qlab/`):
|
|
163
|
+
|
|
164
|
+
| Method | Path | Description |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `GET` | `/` | Bundled React UI |
|
|
167
|
+
| `GET` | `/api/bootstrap/` | Initial data load (models, settings) |
|
|
168
|
+
| `POST` | `/api/query/` | Run a filtered, paginated query |
|
|
169
|
+
| `POST` | `/api/metadata/` | Model field and relation schema |
|
|
170
|
+
| `POST` | `/api/neighborhood/` | Relation graph for a set of records |
|
|
171
|
+
| `GET / PATCH` | `/api/settings/` | Per-user UI settings |
|
|
172
|
+
| `GET / POST` | `/api/saved-queries/` | List and create saved queries |
|
|
173
|
+
| `GET / PATCH / DELETE` | `/api/saved-queries/<id>/` | Manage a single saved query |
|
|
174
|
+
| `POST` | `/api/saved-queries/<id>/run/` | Execute a saved query |
|
|
175
|
+
| `GET` | `/api/history/` | Query run history |
|
|
176
|
+
|
|
177
|
+
### Example query payload
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"model": "Device",
|
|
182
|
+
"app_label": "myapp",
|
|
183
|
+
"select_fields": ["id", "name", "status", "region"],
|
|
184
|
+
"filter_fields": {
|
|
185
|
+
"and_operation": [
|
|
186
|
+
{
|
|
187
|
+
"or_operation": [
|
|
188
|
+
{ "field": "status", "op": "is", "value": "active" },
|
|
189
|
+
{ "field": "status", "op": "is", "value": "maintenance" }
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"or_operation": [
|
|
194
|
+
{ "field": "region", "op": "is", "value": "DE" },
|
|
195
|
+
{ "field": "region", "op": "is", "value": "AT" }
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
"page": 1,
|
|
201
|
+
"page_size": 100
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## UI capabilities
|
|
208
|
+
|
|
209
|
+
- **Dashboard** — model counts, saved query count and recent activity
|
|
210
|
+
- **Query builder** — field picker, nested `(a or b) and (x or y)` filter groups, CSV export, JSON copy
|
|
211
|
+
- **Models browser** — field types, nullability, filterable flags, relation inspection
|
|
212
|
+
- **Saved queries** — create, update, delete, bulk delete and run from the UI
|
|
213
|
+
- **History** — replay past runs, save from history, filter by model and time range
|
|
214
|
+
- **Settings** — page size, default app, theme
|
|
215
|
+
- **Light and dark mode**
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Django admin
|
|
220
|
+
|
|
221
|
+
The package registers the following models in Django admin:
|
|
222
|
+
|
|
223
|
+
| Model | Description |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `QLabUserSettings` | Per-user theme, page size and active tab |
|
|
226
|
+
| `SavedQuery` | Stored query payloads with metadata |
|
|
227
|
+
| `QueryRunHistory` | Execution log with status, duration and result snapshot |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Requirements
|
|
232
|
+
|
|
233
|
+
| Package | Version |
|
|
234
|
+
|---|---|
|
|
235
|
+
| Python | ≥ 3.9 |
|
|
236
|
+
| Django | ≥ 4.0 |
|
|
237
|
+
| djangorestframework | ≥ 3.14 |
|
|
238
|
+
| pydantic | ≥ 2.0 |
|
|
239
|
+
| drf-spectacular | ≥ 0.26 |
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Frontend development
|
|
244
|
+
|
|
245
|
+
This section is for maintainers working on the UI itself. Package consumers do not need npm.
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
cd frontend
|
|
249
|
+
npm install
|
|
250
|
+
npm run dev # dev server with HMR
|
|
251
|
+
npm run build # write compiled assets to qlab/static/qlab/
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Local demo
|
|
257
|
+
|
|
258
|
+
A gitignored demo project lives in `.local-demo/`:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
cd .local-demo
|
|
262
|
+
python manage.py migrate
|
|
263
|
+
python manage.py seed_demo_data
|
|
264
|
+
python manage.py runserver 8054
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Then open [http://127.0.0.1:8054/qlab/](http://127.0.0.1:8054/qlab/).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.5.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-qlab"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Dynamic query API for Django REST Framework"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Tabea Hoehne"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["django", "rest", "api", "query", "filter", "metadata"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Framework :: Django :: 4.0",
|
|
19
|
+
"Framework :: Django :: 4.1",
|
|
20
|
+
"Framework :: Django :: 4.2",
|
|
21
|
+
"Framework :: Django :: 5.0",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
dependencies = [
|
|
33
|
+
"Django>=4.0",
|
|
34
|
+
"djangorestframework>=3.14",
|
|
35
|
+
"pydantic>=2.0",
|
|
36
|
+
"drf-spectacular>=0.26",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.0",
|
|
42
|
+
"pytest-django>=4.5",
|
|
43
|
+
"black>=23.0",
|
|
44
|
+
"ruff>=0.1",
|
|
45
|
+
"pre-commit>=4.0",
|
|
46
|
+
"build>=1.2",
|
|
47
|
+
"twine>=5.1",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://github.com/tabeahoehne132/django-qlab"
|
|
52
|
+
Documentation = "https://github.com/tabeahoehne132/django-qlab#readme"
|
|
53
|
+
Repository = "https://github.com/tabeahoehne132/django-qlab"
|
|
54
|
+
|
|
55
|
+
[tool.poetry]
|
|
56
|
+
packages = [{include = "qlab"}]
|
|
57
|
+
include = [
|
|
58
|
+
"qlab/templates/**/*",
|
|
59
|
+
"qlab/static/**/*",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.black]
|
|
63
|
+
line-length = 88
|
|
64
|
+
target-version = ["py39"]
|
|
65
|
+
extend-exclude = """
|
|
66
|
+
(
|
|
67
|
+
qlab/static/qlab/.*|
|
|
68
|
+
build/.*|
|
|
69
|
+
django_qlab\\.egg-info/.*|
|
|
70
|
+
frontend/.*|
|
|
71
|
+
docs/.*\\.svg
|
|
72
|
+
)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
line-length = 88
|
|
77
|
+
target-version = "py39"
|
|
78
|
+
extend-exclude = [
|
|
79
|
+
"build",
|
|
80
|
+
"django_qlab.egg-info",
|
|
81
|
+
"frontend",
|
|
82
|
+
"qlab/static/qlab",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint]
|
|
86
|
+
select = ["E", "F", "I"]
|
|
87
|
+
ignore = ["E501"]
|