dataman-engine 0.1.1__tar.gz → 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.
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/PKG-INFO +12 -5
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/README.md +5 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/pyproject.toml +27 -6
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/pyproject.toml.orig +27 -8
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/__init__.py +1 -1
- dataman_engine-0.2.0/src/dataman/asgi.py +3 -0
- dataman_engine-0.2.0/src/dataman/core/middleware.py +123 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/models.py +2 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/urls.py +107 -0
- dataman_engine-0.2.0/src/dataman/wsgi.py +3 -0
- dataman_engine-0.1.1/src/dataman/core/middleware.py +0 -46
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/cli.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/__init__.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/apps.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/audit.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/auth.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/crypto.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/fields.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/masking.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/migrations/0001_initial.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/migrations/__init__.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/permissions.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/router.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/signals.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/templates/admin/dashboard.html +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/templates/admin/login.html +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/views.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/django_setup.py +0 -0
- {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataman-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A dynamic, headless data layer framework built on Django and DRF for instant CRUD APIs and custom business logic.
|
|
5
5
|
Keywords: django,rest,api,crud,headless,generator,scaffold
|
|
6
6
|
Author: Vikash G
|
|
@@ -10,11 +10,12 @@ Classifier: Development Status :: 4 - Beta
|
|
|
10
10
|
Classifier: Environment :: Console
|
|
11
11
|
Classifier: Environment :: Web Environment
|
|
12
12
|
Classifier: Framework :: Django
|
|
13
|
-
Classifier: Framework :: Django ::
|
|
13
|
+
Classifier: Framework :: Django :: 6.0
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
20
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
21
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
@@ -31,15 +32,21 @@ Requires-Dist: dj-database-url>=2.3.0
|
|
|
31
32
|
Requires-Dist: inflection>=0.5.1
|
|
32
33
|
Requires-Dist: uvicorn>=0.34.0
|
|
33
34
|
Requires-Dist: cryptography>=42.0.0
|
|
34
|
-
Requires-
|
|
35
|
-
|
|
36
|
-
Project-URL:
|
|
35
|
+
Requires-Dist: psycopg2-binary>=2.9.13
|
|
36
|
+
Requires-Python: >=3.12
|
|
37
|
+
Project-URL: Homepage, https://github.com/vikashgraja/dataman
|
|
38
|
+
Project-URL: Repository, https://github.com/vikashgraja/dataman.git
|
|
37
39
|
Description-Content-Type: text/markdown
|
|
38
40
|
|
|
39
41
|
# DataMan (Data MiddleMan)
|
|
40
42
|
|
|
43
|
+
[](docs/QUICKSTART.md)
|
|
44
|
+
|
|
41
45
|
**DataMan** is a dynamic, CLI-driven backend framework built on top of Django and Django REST Framework. It eliminates the boilerplate of writing standard CRUD APIs, routing, and serializers by allowing you to scaffold endpoints instantly from the command line while preserving your ability to inject custom business logic and strict validation whenever you need it.
|
|
42
46
|
|
|
47
|
+
👉 **Looking to build an API in 5 minutes? Check out the [Developer Quickstart Tutorial](docs/QUICKSTART.md)!**
|
|
48
|
+
|
|
49
|
+
|
|
43
50
|
---
|
|
44
51
|
|
|
45
52
|
## Features
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# DataMan (Data MiddleMan)
|
|
2
2
|
|
|
3
|
+
[](docs/QUICKSTART.md)
|
|
4
|
+
|
|
3
5
|
**DataMan** is a dynamic, CLI-driven backend framework built on top of Django and Django REST Framework. It eliminates the boilerplate of writing standard CRUD APIs, routing, and serializers by allowing you to scaffold endpoints instantly from the command line while preserving your ability to inject custom business logic and strict validation whenever you need it.
|
|
4
6
|
|
|
7
|
+
👉 **Looking to build an API in 5 minutes? Check out the [Developer Quickstart Tutorial](docs/QUICKSTART.md)!**
|
|
8
|
+
|
|
9
|
+
|
|
5
10
|
---
|
|
6
11
|
|
|
7
12
|
## Features
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dataman-engine"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "A dynamic, headless data layer framework built on Django and DRF for instant CRUD APIs and custom business logic."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
8
|
classifiers = [
|
|
9
9
|
"Development Status :: 4 - Beta",
|
|
10
10
|
"Environment :: Console",
|
|
11
11
|
"Environment :: Web Environment",
|
|
12
12
|
"Framework :: Django",
|
|
13
|
-
"Framework :: Django ::
|
|
13
|
+
"Framework :: Django :: 6.0",
|
|
14
14
|
"Intended Audience :: Developers",
|
|
15
15
|
"Operating System :: OS Independent",
|
|
16
16
|
"Programming Language :: Python",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
18
19
|
"Programming Language :: Python :: 3.13",
|
|
19
20
|
"Topic :: Internet :: WWW/HTTP",
|
|
20
21
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
@@ -42,6 +43,7 @@ dependencies = [
|
|
|
42
43
|
"inflection>=0.5.1",
|
|
43
44
|
"uvicorn>=0.34.0",
|
|
44
45
|
"cryptography>=42.0.0",
|
|
46
|
+
"psycopg2-binary>=2.9.13",
|
|
45
47
|
]
|
|
46
48
|
|
|
47
49
|
[[project.authors]]
|
|
@@ -52,8 +54,8 @@ email = "vikashgraja@gmail.com"
|
|
|
52
54
|
dataman = "dataman.cli:cli"
|
|
53
55
|
|
|
54
56
|
[project.urls]
|
|
55
|
-
Homepage = "https://github.com/
|
|
56
|
-
Repository = "https://github.com/
|
|
57
|
+
Homepage = "https://github.com/vikashgraja/dataman"
|
|
58
|
+
Repository = "https://github.com/vikashgraja/dataman.git"
|
|
57
59
|
|
|
58
60
|
[tool.coverage.run]
|
|
59
61
|
branch = true
|
|
@@ -65,13 +67,14 @@ module-name = "dataman"
|
|
|
65
67
|
|
|
66
68
|
[tool.ruff]
|
|
67
69
|
line-length = 88
|
|
68
|
-
target-version = "
|
|
70
|
+
target-version = "py312"
|
|
69
71
|
exclude = [
|
|
70
72
|
".venv",
|
|
71
73
|
"venv",
|
|
72
74
|
"__pycache__",
|
|
73
75
|
"build",
|
|
74
76
|
"dist",
|
|
77
|
+
"benchmarks",
|
|
75
78
|
]
|
|
76
79
|
|
|
77
80
|
[tool.ruff.lint]
|
|
@@ -97,6 +100,7 @@ line-ending = "auto"
|
|
|
97
100
|
exclude_dirs = [
|
|
98
101
|
"tests",
|
|
99
102
|
".venv",
|
|
103
|
+
"benchmarks",
|
|
100
104
|
]
|
|
101
105
|
skips = ["B101"]
|
|
102
106
|
|
|
@@ -104,6 +108,20 @@ skips = ["B101"]
|
|
|
104
108
|
testpaths = ["tests"]
|
|
105
109
|
addopts = "-v --tb=short"
|
|
106
110
|
|
|
111
|
+
[tool.tox]
|
|
112
|
+
legacy_tox_ini = """
|
|
113
|
+
[tox]
|
|
114
|
+
env_list = py312, py313, py314
|
|
115
|
+
min_version = 4.0
|
|
116
|
+
|
|
117
|
+
[testenv]
|
|
118
|
+
runner = uv-venv-runner
|
|
119
|
+
dependency_groups =
|
|
120
|
+
dev
|
|
121
|
+
commands =
|
|
122
|
+
pytest {posargs}
|
|
123
|
+
"""
|
|
124
|
+
|
|
107
125
|
[build-system]
|
|
108
126
|
requires = ["uv_build>=0.10.6,<0.13.0"]
|
|
109
127
|
build-backend = "uv_build"
|
|
@@ -111,9 +129,12 @@ build-backend = "uv_build"
|
|
|
111
129
|
[dependency-groups]
|
|
112
130
|
dev = [
|
|
113
131
|
"bandit>=1.9.4",
|
|
132
|
+
"locust>=2.32.0",
|
|
114
133
|
"playwright>=1.62.0",
|
|
115
134
|
"pre-commit>=4.6.1",
|
|
116
135
|
"pytest>=9.1.1",
|
|
117
136
|
"pytest-cov>=7.1.0",
|
|
118
137
|
"ruff>=0.16.1",
|
|
138
|
+
"tox>=4.24.1",
|
|
139
|
+
"tox-uv>=1.25.0",
|
|
119
140
|
]
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dataman-engine"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "A dynamic, headless data layer framework built on Django and DRF for instant CRUD APIs and custom business logic."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Vikash G", email = "vikashgraja@gmail.com" }
|
|
9
9
|
]
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Development Status :: 4 - Beta",
|
|
13
13
|
"Environment :: Console",
|
|
14
14
|
"Environment :: Web Environment",
|
|
15
15
|
"Framework :: Django",
|
|
16
|
-
"Framework :: Django ::
|
|
16
|
+
"Framework :: Django :: 6.0",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
21
22
|
"Programming Language :: Python :: 3.13",
|
|
22
23
|
"Topic :: Internet :: WWW/HTTP",
|
|
23
24
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
@@ -39,14 +40,15 @@ dependencies = [
|
|
|
39
40
|
"inflection>=0.5.1",
|
|
40
41
|
"uvicorn>=0.34.0",
|
|
41
42
|
"cryptography>=42.0.0",
|
|
43
|
+
"psycopg2-binary>=2.9.13",
|
|
42
44
|
]
|
|
43
45
|
|
|
44
46
|
[project.scripts]
|
|
45
47
|
dataman = "dataman.cli:cli"
|
|
46
48
|
|
|
47
49
|
[project.urls]
|
|
48
|
-
Homepage = "https://github.com/
|
|
49
|
-
Repository = "https://github.com/
|
|
50
|
+
Homepage = "https://github.com/vikashgraja/dataman"
|
|
51
|
+
Repository = "https://github.com/vikashgraja/dataman.git"
|
|
50
52
|
|
|
51
53
|
[tool.coverage.run]
|
|
52
54
|
branch = true
|
|
@@ -62,17 +64,20 @@ module-name = "dataman"
|
|
|
62
64
|
[dependency-groups]
|
|
63
65
|
dev = [
|
|
64
66
|
"bandit>=1.9.4",
|
|
67
|
+
"locust>=2.32.0",
|
|
65
68
|
"playwright>=1.62.0",
|
|
66
69
|
"pre-commit>=4.6.1",
|
|
67
70
|
"pytest>=9.1.1",
|
|
68
71
|
"pytest-cov>=7.1.0",
|
|
69
72
|
"ruff>=0.16.1",
|
|
73
|
+
"tox>=4.24.1",
|
|
74
|
+
"tox-uv>=1.25.0",
|
|
70
75
|
]
|
|
71
76
|
|
|
72
77
|
[tool.ruff]
|
|
73
78
|
line-length = 88
|
|
74
|
-
target-version = "
|
|
75
|
-
exclude = [".venv", "venv", "__pycache__", "build", "dist"]
|
|
79
|
+
target-version = "py312"
|
|
80
|
+
exclude = [".venv", "venv", "__pycache__", "build", "dist", "benchmarks"]
|
|
76
81
|
|
|
77
82
|
[tool.ruff.lint]
|
|
78
83
|
select = [
|
|
@@ -94,9 +99,23 @@ skip-magic-trailing-comma = false
|
|
|
94
99
|
line-ending = "auto"
|
|
95
100
|
|
|
96
101
|
[tool.bandit]
|
|
97
|
-
exclude_dirs = ["tests", ".venv"]
|
|
102
|
+
exclude_dirs = ["tests", ".venv", "benchmarks"]
|
|
98
103
|
skips = ["B101"]
|
|
99
104
|
|
|
100
105
|
[tool.pytest.ini_options]
|
|
101
106
|
testpaths = ["tests"]
|
|
102
107
|
addopts = "-v --tb=short"
|
|
108
|
+
|
|
109
|
+
[tool.tox]
|
|
110
|
+
legacy_tox_ini = """
|
|
111
|
+
[tox]
|
|
112
|
+
env_list = py312, py313, py314
|
|
113
|
+
min_version = 4.0
|
|
114
|
+
|
|
115
|
+
[testenv]
|
|
116
|
+
runner = uv-venv-runner
|
|
117
|
+
dependency_groups =
|
|
118
|
+
dev
|
|
119
|
+
commands =
|
|
120
|
+
pytest {posargs}
|
|
121
|
+
"""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import logging
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from django.db import close_old_connections
|
|
8
|
+
|
|
9
|
+
from .models import APILog
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("dataman.telemetry")
|
|
12
|
+
|
|
13
|
+
_log_queue = queue.Queue(maxsize=50000)
|
|
14
|
+
_shutdown_event = threading.Event()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _telemetry_batch_worker():
|
|
18
|
+
"""
|
|
19
|
+
High-performance background batch worker. Drains telemetry log queue
|
|
20
|
+
and persists records using bulk_create to eliminate DB lock contention.
|
|
21
|
+
"""
|
|
22
|
+
while not _shutdown_event.is_set() or not _log_queue.empty():
|
|
23
|
+
batch = []
|
|
24
|
+
try:
|
|
25
|
+
item = _log_queue.get(timeout=0.05)
|
|
26
|
+
batch.append(item)
|
|
27
|
+
while len(batch) < 100:
|
|
28
|
+
try:
|
|
29
|
+
batch.append(_log_queue.get_nowait())
|
|
30
|
+
except queue.Empty:
|
|
31
|
+
break
|
|
32
|
+
except queue.Empty:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
if not batch:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
close_old_connections()
|
|
39
|
+
try:
|
|
40
|
+
objs = [
|
|
41
|
+
APILog(
|
|
42
|
+
method=m,
|
|
43
|
+
path=p,
|
|
44
|
+
status_code=s,
|
|
45
|
+
duration_ms=d,
|
|
46
|
+
ip_address=ip,
|
|
47
|
+
)
|
|
48
|
+
for (m, p, s, d, ip) in batch
|
|
49
|
+
]
|
|
50
|
+
APILog.objects.bulk_create(objs, batch_size=100)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"Failed to batch write APILogs: {e}")
|
|
53
|
+
finally:
|
|
54
|
+
close_old_connections()
|
|
55
|
+
for _ in batch:
|
|
56
|
+
_log_queue.task_done()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_worker_thread = threading.Thread(
|
|
60
|
+
target=_telemetry_batch_worker, name="dataman-telemetry-batch", daemon=True
|
|
61
|
+
)
|
|
62
|
+
_worker_thread.start()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def flush_telemetry_logs(timeout=5):
|
|
66
|
+
"""Barrier helper to ensure all queued background logs are flushed to database."""
|
|
67
|
+
try:
|
|
68
|
+
_log_queue.join()
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.warning(f"Timeout flushing telemetry queue: {e}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _shutdown_telemetry():
|
|
74
|
+
_shutdown_event.set()
|
|
75
|
+
flush_telemetry_logs()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
atexit.register(_shutdown_telemetry)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class APILoggingMiddleware:
|
|
82
|
+
"""Middleware to log API request metrics asynchronously for high concurrency."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, get_response):
|
|
85
|
+
self.get_response = get_response
|
|
86
|
+
|
|
87
|
+
def __call__(self, request):
|
|
88
|
+
path = request.path
|
|
89
|
+
# Only log requests to actual data endpoints,
|
|
90
|
+
# exclude internal/dashboard/docs/health routes.
|
|
91
|
+
if (
|
|
92
|
+
not path.startswith("/api/")
|
|
93
|
+
or path.startswith("/api/schema")
|
|
94
|
+
or path.startswith("/api/docs")
|
|
95
|
+
or path.startswith("/api/redoc")
|
|
96
|
+
or path.startswith("/api/_internal/")
|
|
97
|
+
or path.startswith("/api/health")
|
|
98
|
+
or path.startswith("/health")
|
|
99
|
+
):
|
|
100
|
+
return self.get_response(request)
|
|
101
|
+
|
|
102
|
+
start_time = time.perf_counter()
|
|
103
|
+
response = self.get_response(request)
|
|
104
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
105
|
+
|
|
106
|
+
# Handle IP Address correctly behind proxies
|
|
107
|
+
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
108
|
+
ip = (
|
|
109
|
+
x_forwarded_for.split(",")[0]
|
|
110
|
+
if x_forwarded_for
|
|
111
|
+
else request.META.get("REMOTE_ADDR")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
_log_queue.put_nowait(
|
|
116
|
+
(request.method, path, response.status_code, duration_ms, ip)
|
|
117
|
+
)
|
|
118
|
+
except queue.Full:
|
|
119
|
+
logger.warning(
|
|
120
|
+
"Telemetry log queue full, dropping record under extreme load."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return response
|
|
@@ -360,6 +360,27 @@ try:
|
|
|
360
360
|
"perform_destroy": custom_perform_destroy,
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
def custom_get_queryset(self, m=model, d=depth):
|
|
364
|
+
qs = m.objects.all()
|
|
365
|
+
if d is not None and d > 0:
|
|
366
|
+
fk_fields = [
|
|
367
|
+
f.name
|
|
368
|
+
for f in m._meta.fields
|
|
369
|
+
if f.is_relation and (f.many_to_one or f.one_to_one)
|
|
370
|
+
]
|
|
371
|
+
if fk_fields:
|
|
372
|
+
qs = qs.select_related(*fk_fields)
|
|
373
|
+
reverse_rel_fields = [
|
|
374
|
+
f.get_accessor_name()
|
|
375
|
+
for f in m._meta.related_objects
|
|
376
|
+
if f.get_accessor_name()
|
|
377
|
+
]
|
|
378
|
+
if reverse_rel_fields:
|
|
379
|
+
qs = qs.prefetch_related(*reverse_rel_fields)
|
|
380
|
+
return qs
|
|
381
|
+
|
|
382
|
+
viewset_attrs["get_queryset"] = custom_get_queryset
|
|
383
|
+
|
|
363
384
|
if depth is not None and depth > 0:
|
|
364
385
|
|
|
365
386
|
def custom_get_serializer_class(
|
|
@@ -386,10 +407,94 @@ try:
|
|
|
386
407
|
# Pagination
|
|
387
408
|
if page_size:
|
|
388
409
|
|
|
410
|
+
class FastPaginator:
|
|
411
|
+
def __init__(self, object_list, per_page):
|
|
412
|
+
self.object_list = object_list
|
|
413
|
+
self.per_page = per_page
|
|
414
|
+
self._count = None
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def count(self):
|
|
418
|
+
if self._count is None:
|
|
419
|
+
try:
|
|
420
|
+
self._count = self.object_list.count()
|
|
421
|
+
except Exception:
|
|
422
|
+
self._count = 0
|
|
423
|
+
return self._count
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def num_pages(self):
|
|
427
|
+
if self.count == 0:
|
|
428
|
+
return 1
|
|
429
|
+
return (self.count + self.per_page - 1) // self.per_page
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def page_range(self):
|
|
433
|
+
return range(1, self.num_pages + 1)
|
|
434
|
+
|
|
435
|
+
def page(self, number):
|
|
436
|
+
number = int(number)
|
|
437
|
+
bottom = (number - 1) * self.per_page
|
|
438
|
+
top = bottom + self.per_page
|
|
439
|
+
items = list(self.object_list[bottom : top + 1])
|
|
440
|
+
has_next = len(items) > self.per_page
|
|
441
|
+
if has_next:
|
|
442
|
+
items = items[: self.per_page]
|
|
443
|
+
|
|
444
|
+
class FastPage:
|
|
445
|
+
def __init__(
|
|
446
|
+
self, object_list, number, paginator, has_next, bottom
|
|
447
|
+
):
|
|
448
|
+
self.object_list = object_list
|
|
449
|
+
self.number = number
|
|
450
|
+
self.paginator = paginator
|
|
451
|
+
self._has_next = has_next
|
|
452
|
+
self._bottom = bottom
|
|
453
|
+
|
|
454
|
+
def __len__(self):
|
|
455
|
+
return len(self.object_list)
|
|
456
|
+
|
|
457
|
+
def __iter__(self):
|
|
458
|
+
return iter(self.object_list)
|
|
459
|
+
|
|
460
|
+
def __getitem__(self, index):
|
|
461
|
+
return self.object_list[index]
|
|
462
|
+
|
|
463
|
+
def has_next(self):
|
|
464
|
+
return self._has_next
|
|
465
|
+
|
|
466
|
+
def has_previous(self):
|
|
467
|
+
return self.number > 1
|
|
468
|
+
|
|
469
|
+
def next_page_number(self):
|
|
470
|
+
return self.number + 1
|
|
471
|
+
|
|
472
|
+
def previous_page_number(self):
|
|
473
|
+
return self.number - 1
|
|
474
|
+
|
|
475
|
+
def start_index(self):
|
|
476
|
+
return self._bottom + 1
|
|
477
|
+
|
|
478
|
+
def end_index(self):
|
|
479
|
+
return self._bottom + len(self.object_list)
|
|
480
|
+
|
|
481
|
+
return FastPage(items, number, self, has_next, bottom)
|
|
482
|
+
|
|
389
483
|
class CustomPagination(PageNumberPagination):
|
|
390
484
|
page_size_val = page_size
|
|
485
|
+
django_paginator_class = FastPaginator
|
|
486
|
+
page_size_query_param = "page_size"
|
|
487
|
+
max_page_size = 500
|
|
391
488
|
|
|
392
489
|
def get_page_size(self, request):
|
|
490
|
+
if self.page_size_query_param:
|
|
491
|
+
with contextlib.suppress(Exception):
|
|
492
|
+
val = int(
|
|
493
|
+
request.query_params.get(
|
|
494
|
+
self.page_size_query_param, self.page_size_val
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
return min(max(val, 1), self.max_page_size)
|
|
393
498
|
return self.page_size_val
|
|
394
499
|
|
|
395
500
|
viewset_attrs["pagination_class"] = CustomPagination
|
|
@@ -406,6 +511,8 @@ try:
|
|
|
406
511
|
filter_backends.append(OrderingFilter)
|
|
407
512
|
viewset_attrs["ordering_fields"] = ordering_fields
|
|
408
513
|
|
|
514
|
+
viewset_attrs["ordering"] = ["-id"]
|
|
515
|
+
|
|
409
516
|
if filter_backends:
|
|
410
517
|
viewset_attrs["filter_backends"] = filter_backends
|
|
411
518
|
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
|
|
3
|
-
from .models import APILog
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class APILoggingMiddleware:
|
|
7
|
-
"""Middleware to log API request metrics for the dashboard."""
|
|
8
|
-
|
|
9
|
-
def __init__(self, get_response):
|
|
10
|
-
self.get_response = get_response
|
|
11
|
-
|
|
12
|
-
def __call__(self, request):
|
|
13
|
-
path = request.path
|
|
14
|
-
# Only log requests to actual data endpoints,
|
|
15
|
-
# exclude internal/dashboard/docs/health routes.
|
|
16
|
-
if (
|
|
17
|
-
not path.startswith("/api/")
|
|
18
|
-
or path.startswith("/api/schema")
|
|
19
|
-
or path.startswith("/api/docs")
|
|
20
|
-
or path.startswith("/api/redoc")
|
|
21
|
-
or path.startswith("/api/_internal/")
|
|
22
|
-
or path.startswith("/api/health")
|
|
23
|
-
or path.startswith("/health")
|
|
24
|
-
):
|
|
25
|
-
return self.get_response(request)
|
|
26
|
-
|
|
27
|
-
start_time = time.time()
|
|
28
|
-
response = self.get_response(request)
|
|
29
|
-
duration_ms = int((time.time() - start_time) * 1000)
|
|
30
|
-
|
|
31
|
-
# Handle IP Address correctly behind proxies
|
|
32
|
-
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
33
|
-
if x_forwarded_for:
|
|
34
|
-
ip = x_forwarded_for.split(",")[0]
|
|
35
|
-
else:
|
|
36
|
-
ip = request.META.get("REMOTE_ADDR")
|
|
37
|
-
|
|
38
|
-
APILog.objects.create(
|
|
39
|
-
method=request.method,
|
|
40
|
-
path=path,
|
|
41
|
-
status_code=response.status_code,
|
|
42
|
-
duration_ms=duration_ms,
|
|
43
|
-
ip_address=ip,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
return response
|
|
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
|
{dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/templates/admin/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|