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.
Files changed (29) hide show
  1. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/PKG-INFO +12 -5
  2. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/README.md +5 -0
  3. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/pyproject.toml +27 -6
  4. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/pyproject.toml.orig +27 -8
  5. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/__init__.py +1 -1
  6. dataman_engine-0.2.0/src/dataman/asgi.py +3 -0
  7. dataman_engine-0.2.0/src/dataman/core/middleware.py +123 -0
  8. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/models.py +2 -0
  9. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/urls.py +107 -0
  10. dataman_engine-0.2.0/src/dataman/wsgi.py +3 -0
  11. dataman_engine-0.1.1/src/dataman/core/middleware.py +0 -46
  12. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/cli.py +0 -0
  13. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/__init__.py +0 -0
  14. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/apps.py +0 -0
  15. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/audit.py +0 -0
  16. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/auth.py +0 -0
  17. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/crypto.py +0 -0
  18. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/fields.py +0 -0
  19. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/masking.py +0 -0
  20. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/migrations/0001_initial.py +0 -0
  21. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/migrations/__init__.py +0 -0
  22. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/permissions.py +0 -0
  23. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/router.py +0 -0
  24. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/signals.py +0 -0
  25. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/templates/admin/dashboard.html +0 -0
  26. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/templates/admin/login.html +0 -0
  27. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/core/views.py +0 -0
  28. {dataman_engine-0.1.1 → dataman_engine-0.2.0}/src/dataman/django_setup.py +0 -0
  29. {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.1.1
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 :: 5.0
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-Python: >=3.13
35
- Project-URL: Homepage, https://github.com/vikashg/dataman
36
- Project-URL: Repository, https://github.com/vikashg/dataman.git
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
+ [![Quickstart Guide](https://img.shields.io/badge/Docs-Quickstart_Guide-blue.svg)](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
+ [![Quickstart Guide](https://img.shields.io/badge/Docs-Quickstart_Guide-blue.svg)](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.1.1"
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.13"
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 :: 5.0",
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/vikashg/dataman"
56
- Repository = "https://github.com/vikashg/dataman.git"
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 = "py310"
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.1.1"
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.13"
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 :: 5.0",
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/vikashg/dataman"
49
- Repository = "https://github.com/vikashg/dataman.git"
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 = "py310"
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
+ """
@@ -9,7 +9,7 @@ from dataman.core.fields import (
9
9
  EncryptedTextField,
10
10
  )
11
11
 
12
- __version__ = "0.1.0"
12
+ __version__ = "0.2.0"
13
13
  __all__ = [
14
14
  "EncryptedCharField",
15
15
  "EncryptedTextField",
@@ -0,0 +1,3 @@
1
+ from dataman.django_setup import get_asgi_application
2
+
3
+ application = get_asgi_application()
@@ -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
@@ -16,6 +16,8 @@ RESERVED_ROOT_DIRS = {
16
16
  ".ruff_cache",
17
17
  ".system_generated",
18
18
  ".venv",
19
+ "benchmark",
20
+ "benchmarks",
19
21
  "build",
20
22
  "coverage_hack",
21
23
  "dist",
@@ -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
 
@@ -0,0 +1,3 @@
1
+ from dataman.django_setup import get_wsgi_application
2
+
3
+ application = get_wsgi_application()
@@ -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