django-api-profiler 0.1.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 (45) hide show
  1. django_api_profiler-0.1.0/.env +6 -0
  2. django_api_profiler-0.1.0/.gitignore +10 -0
  3. django_api_profiler-0.1.0/.python-version +1 -0
  4. django_api_profiler-0.1.0/Dockerfile +21 -0
  5. django_api_profiler-0.1.0/LICENSE +21 -0
  6. django_api_profiler-0.1.0/PKG-INFO +118 -0
  7. django_api_profiler-0.1.0/README.md +96 -0
  8. django_api_profiler-0.1.0/config/__init__.py +3 -0
  9. django_api_profiler-0.1.0/config/asgi.py +7 -0
  10. django_api_profiler-0.1.0/config/celery.py +10 -0
  11. django_api_profiler-0.1.0/config/settings.py +114 -0
  12. django_api_profiler-0.1.0/config/urls.py +6 -0
  13. django_api_profiler-0.1.0/config/wsgi.py +7 -0
  14. django_api_profiler-0.1.0/docker-compose.yml +71 -0
  15. django_api_profiler-0.1.0/manage.py +22 -0
  16. django_api_profiler-0.1.0/profiler/__init__.py +0 -0
  17. django_api_profiler-0.1.0/profiler/admin.py +80 -0
  18. django_api_profiler-0.1.0/profiler/apps.py +5 -0
  19. django_api_profiler-0.1.0/profiler/conf.py +31 -0
  20. django_api_profiler-0.1.0/profiler/db_wrapper.py +33 -0
  21. django_api_profiler-0.1.0/profiler/middleware.py +58 -0
  22. django_api_profiler-0.1.0/profiler/migrations/0001_initial.py +25 -0
  23. django_api_profiler-0.1.0/profiler/migrations/0002_requestmetric_query_count_and_more.py +23 -0
  24. django_api_profiler-0.1.0/profiler/migrations/0003_requestmetric_exception_message_and_more.py +28 -0
  25. django_api_profiler-0.1.0/profiler/migrations/0004_requestmetric_is_slow.py +19 -0
  26. django_api_profiler-0.1.0/profiler/migrations/0005_requestmetric_ip_address_requestmetric_request_id_and_more.py +58 -0
  27. django_api_profiler-0.1.0/profiler/migrations/0006_requestmetric_has_n_plus_one_and_more.py +23 -0
  28. django_api_profiler-0.1.0/profiler/migrations/0007_endpointsummary.py +35 -0
  29. django_api_profiler-0.1.0/profiler/migrations/__init__.py +0 -0
  30. django_api_profiler-0.1.0/profiler/models/__init__.py +2 -0
  31. django_api_profiler-0.1.0/profiler/models/endpoint_summary.py +28 -0
  32. django_api_profiler-0.1.0/profiler/models/request_metric.py +39 -0
  33. django_api_profiler-0.1.0/profiler/services/__init__.py +10 -0
  34. django_api_profiler-0.1.0/profiler/services/analytics.py +88 -0
  35. django_api_profiler-0.1.0/profiler/services/n_plus_one.py +31 -0
  36. django_api_profiler-0.1.0/profiler/services/regression.py +61 -0
  37. django_api_profiler-0.1.0/profiler/services/request_metric.py +50 -0
  38. django_api_profiler-0.1.0/profiler/tasks.py +16 -0
  39. django_api_profiler-0.1.0/profiler/tests/__init__.py +5 -0
  40. django_api_profiler-0.1.0/profiler/tests/build_metric_payload.py +14 -0
  41. django_api_profiler-0.1.0/profiler/tests/n_plus_one.py +51 -0
  42. django_api_profiler-0.1.0/profiler/utils/__init__.py +1 -0
  43. django_api_profiler-0.1.0/profiler/utils/ingestion.py +33 -0
  44. django_api_profiler-0.1.0/pyproject.toml +52 -0
  45. django_api_profiler-0.1.0/uv.lock +359 -0
@@ -0,0 +1,6 @@
1
+ SECRET_KEY='django-insecure-8*)!%iue3qqv2q@90o1-qy^##r=zh%1ps63&01aogcpkw%vz2#'
2
+ DB_NAME=profiler_db
3
+ DB_USER=ahmed
4
+ DB_PASSWORD=programming
5
+ DB_HOST=localhost
6
+ DB_PORT=5432
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ WORKDIR /src
7
+
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ libpq-dev \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ RUN pip install uv
13
+
14
+ COPY pyproject.toml uv.lock .python-version ./
15
+ RUN uv sync --frozen
16
+
17
+ COPY . .
18
+
19
+ EXPOSE 8000
20
+
21
+ CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmed
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,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-api-profiler
3
+ Version: 0.1.0
4
+ Summary: Lightweight API profiling and endpoint analytics for Django
5
+ Author: Ahmed
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: analytics,django,monitoring,performance,profiling
9
+ Classifier: Framework :: Django
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: django>=4.2
14
+ Provides-Extra: async
15
+ Requires-Dist: celery>=5.6.3; extra == 'async'
16
+ Requires-Dist: redis>=7.4.0; extra == 'async'
17
+ Provides-Extra: beat
18
+ Requires-Dist: django-celery-beat>=2.9.0; extra == 'beat'
19
+ Provides-Extra: postgres
20
+ Requires-Dist: psycopg2-binary>=2.9.12; extra == 'postgres'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Django API Profiler
24
+
25
+ Lightweight API profiling and endpoint analytics for Django.
26
+
27
+ ## Features
28
+
29
+ - Request latency tracking
30
+ - Endpoint summaries
31
+ - p95 response metrics
32
+ - Error rate monitoring
33
+ - Slow request detection
34
+ - N+1 query detection
35
+ - Django admin integration
36
+ - Optional async ingestion with Celery
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install django-api-profiler
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Quickstart
49
+
50
+ Add the app:
51
+
52
+ ```python
53
+ INSTALLED_APPS = [
54
+ ...
55
+ "django_api_profiler",
56
+ ]
57
+ ```
58
+
59
+ Add the middleware:
60
+
61
+ ```python
62
+ MIDDLEWARE = [
63
+ ...
64
+ "django_api_profiler.middleware.ApiProfilerMiddleware",
65
+ ]
66
+ ```
67
+
68
+ Run migrations:
69
+
70
+ ```bash
71
+ python manage.py migrate
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Configuration
77
+
78
+ ```python
79
+ PROFILER = {
80
+ "ASYNC": False,
81
+ "SLOW_REQUEST_THRESHOLD_MS": 1000,
82
+ "AGGREGATION_WINDOW_MINUTES": 60,
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Admin Dashboard
89
+
90
+ The package provides Django admin integration for:
91
+ - endpoint summaries
92
+ - latency metrics
93
+ - error tracking
94
+ - regression detection
95
+
96
+ ---
97
+
98
+ ## Optional Async Support
99
+
100
+ Install async dependencies:
101
+
102
+ ```bash
103
+ pip install django-api-profiler[async]
104
+ ```
105
+
106
+ Then enable:
107
+
108
+ ```python
109
+ PROFILER = {
110
+ "ASYNC": True,
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,96 @@
1
+ # Django API Profiler
2
+
3
+ Lightweight API profiling and endpoint analytics for Django.
4
+
5
+ ## Features
6
+
7
+ - Request latency tracking
8
+ - Endpoint summaries
9
+ - p95 response metrics
10
+ - Error rate monitoring
11
+ - Slow request detection
12
+ - N+1 query detection
13
+ - Django admin integration
14
+ - Optional async ingestion with Celery
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install django-api-profiler
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Quickstart
27
+
28
+ Add the app:
29
+
30
+ ```python
31
+ INSTALLED_APPS = [
32
+ ...
33
+ "django_api_profiler",
34
+ ]
35
+ ```
36
+
37
+ Add the middleware:
38
+
39
+ ```python
40
+ MIDDLEWARE = [
41
+ ...
42
+ "django_api_profiler.middleware.ApiProfilerMiddleware",
43
+ ]
44
+ ```
45
+
46
+ Run migrations:
47
+
48
+ ```bash
49
+ python manage.py migrate
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Configuration
55
+
56
+ ```python
57
+ PROFILER = {
58
+ "ASYNC": False,
59
+ "SLOW_REQUEST_THRESHOLD_MS": 1000,
60
+ "AGGREGATION_WINDOW_MINUTES": 60,
61
+ }
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Admin Dashboard
67
+
68
+ The package provides Django admin integration for:
69
+ - endpoint summaries
70
+ - latency metrics
71
+ - error tracking
72
+ - regression detection
73
+
74
+ ---
75
+
76
+ ## Optional Async Support
77
+
78
+ Install async dependencies:
79
+
80
+ ```bash
81
+ pip install django-api-profiler[async]
82
+ ```
83
+
84
+ Then enable:
85
+
86
+ ```python
87
+ PROFILER = {
88
+ "ASYNC": True,
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,3 @@
1
+ from .celery import app as celery_app
2
+
3
+ __all__ = ('celery_app',)
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.asgi import get_asgi_application
4
+
5
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
6
+
7
+ application = get_asgi_application()
@@ -0,0 +1,10 @@
1
+ import os
2
+ from celery import Celery
3
+
4
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
5
+
6
+ app = Celery('config')
7
+
8
+ app.config_from_object('django.conf:settings', namespace='CELERY')
9
+
10
+ app.autodiscover_tasks()
@@ -0,0 +1,114 @@
1
+ from pathlib import Path
2
+ from dotenv import load_dotenv
3
+ from celery.schedules import crontab
4
+ import os
5
+
6
+ load_dotenv()
7
+
8
+ BASE_DIR = Path(__file__).resolve().parent.parent
9
+
10
+
11
+ SECRET_KEY = os.getenv("SECRET_KEY")
12
+
13
+
14
+ DEBUG = True
15
+
16
+ ALLOWED_HOSTS = ['*']
17
+
18
+
19
+
20
+ INSTALLED_APPS = [
21
+ 'django.contrib.admin',
22
+ 'django.contrib.auth',
23
+ 'django.contrib.contenttypes',
24
+ 'django.contrib.sessions',
25
+ 'django.contrib.messages',
26
+ 'django.contrib.staticfiles',
27
+ 'django_celery_beat',
28
+ 'profiler',
29
+ ]
30
+
31
+ MIDDLEWARE = [
32
+ 'django.middleware.security.SecurityMiddleware',
33
+ 'django.contrib.sessions.middleware.SessionMiddleware',
34
+ 'django.middleware.common.CommonMiddleware',
35
+ 'django.middleware.csrf.CsrfViewMiddleware',
36
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
37
+ 'django.contrib.messages.middleware.MessageMiddleware',
38
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
39
+ 'profiler.middleware.ApiProfilerMiddleware',
40
+ ]
41
+
42
+ ROOT_URLCONF = 'config.urls'
43
+
44
+ TEMPLATES = [
45
+ {
46
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
47
+ 'DIRS': [],
48
+ 'APP_DIRS': True,
49
+ 'OPTIONS': {
50
+ 'context_processors': [
51
+ 'django.template.context_processors.request',
52
+ 'django.contrib.auth.context_processors.auth',
53
+ 'django.contrib.messages.context_processors.messages',
54
+ ],
55
+ },
56
+ },
57
+ ]
58
+
59
+ WSGI_APPLICATION = 'config.wsgi.application'
60
+
61
+
62
+ DATABASES = {
63
+ 'default': {
64
+ 'ENGINE': 'django.db.backends.postgresql',
65
+ 'NAME': os.environ.get('DB_NAME'),
66
+ 'USER': os.environ.get('DB_USER'),
67
+ 'PASSWORD': os.environ.get('DB_PASSWORD'),
68
+ 'HOST': os.environ.get('DB_HOST', 'localhost'),
69
+ 'PORT': os.environ.get('DB_PORT', '5432'),
70
+ }
71
+ }
72
+
73
+
74
+ AUTH_PASSWORD_VALIDATORS = [
75
+ {
76
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
77
+ },
78
+ {
79
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
80
+ },
81
+ {
82
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
83
+ },
84
+ {
85
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
86
+ },
87
+ ]
88
+
89
+
90
+ LANGUAGE_CODE = 'en-us'
91
+
92
+ TIME_ZONE = 'UTC'
93
+
94
+ USE_I18N = True
95
+
96
+ USE_TZ = True
97
+
98
+
99
+ STATIC_URL = 'static/'
100
+
101
+ CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
102
+ CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
103
+ CELERY_TIMEZONE = 'UTC'
104
+
105
+ CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
106
+
107
+
108
+
109
+ CELERY_BEAT_SCHEDULE = {
110
+ 'compute-endpoint-summaries': {
111
+ 'task': 'profiler.tasks.run_aggregation',
112
+ 'schedule': crontab(minute=0),
113
+ },
114
+ }
@@ -0,0 +1,6 @@
1
+ from django.contrib import admin
2
+ from django.urls import path
3
+
4
+ urlpatterns = [
5
+ path('admin/', admin.site.urls),
6
+ ]
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.wsgi import get_wsgi_application
4
+
5
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
6
+
7
+ application = get_wsgi_application()
@@ -0,0 +1,71 @@
1
+ services:
2
+
3
+ db:
4
+ image: postgres:16-alpine
5
+ environment:
6
+ POSTGRES_DB: ${DB_NAME}
7
+ POSTGRES_USER: ${DB_USER}
8
+ POSTGRES_PASSWORD: ${DB_PASSWORD}
9
+ volumes:
10
+ - postgres_data:/var/lib/postgresql/data
11
+ healthcheck:
12
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
13
+ interval: 10s
14
+ timeout: 5s
15
+ retries: 5
16
+
17
+ redis:
18
+ image: redis:7-alpine
19
+ healthcheck:
20
+ test: ["CMD", "redis-cli", "ping"]
21
+ interval: 10s
22
+ timeout: 5s
23
+ retries: 5
24
+
25
+ web:
26
+ build: .
27
+ image: django-api-profiler:latest
28
+ restart: unless-stopped
29
+ command: >
30
+ sh -c "/src/.venv/bin/python manage.py migrate &&
31
+ /src/.venv/bin/gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3"
32
+ volumes:
33
+ - .:/src
34
+ ports:
35
+ - "8000:8000"
36
+ env_file:
37
+ - .env
38
+ depends_on:
39
+ db:
40
+ condition: service_healthy
41
+ redis:
42
+ condition: service_healthy
43
+
44
+ celery:
45
+ image: django-api-profiler:latest
46
+ command: uv run celery -A config worker --loglevel=info
47
+ volumes:
48
+ - .:/src
49
+ env_file:
50
+ - .env
51
+ depends_on:
52
+ db:
53
+ condition: service_healthy
54
+ redis:
55
+ condition: service_healthy
56
+
57
+ celery_beat:
58
+ image: django-api-profiler:latest
59
+ command: uv run celery -A config beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler
60
+ volumes:
61
+ - .:/src
62
+ env_file:
63
+ - .env
64
+ depends_on:
65
+ db:
66
+ condition: service_healthy
67
+ redis:
68
+ condition: service_healthy
69
+
70
+ volumes:
71
+ postgres_data:
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python
2
+ """Django's command-line utility for administrative tasks."""
3
+ import os
4
+ import sys
5
+
6
+
7
+ def main():
8
+ """Run administrative tasks."""
9
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
10
+ try:
11
+ from django.core.management import execute_from_command_line
12
+ except ImportError as exc:
13
+ raise ImportError(
14
+ "Couldn't import Django. Are you sure it's installed and "
15
+ "available on your PYTHONPATH environment variable? Did you "
16
+ "forget to activate a virtual environment?"
17
+ ) from exc
18
+ execute_from_command_line(sys.argv)
19
+
20
+
21
+ if __name__ == '__main__':
22
+ main()
File without changes
@@ -0,0 +1,80 @@
1
+ from django.contrib import admin
2
+ from django.utils.html import format_html
3
+ from .models import RequestMetric , EndpointSummary
4
+
5
+
6
+ @admin.register(RequestMetric)
7
+ class RequestMetricAdmin(admin.ModelAdmin):
8
+ list_display = (
9
+ "path",
10
+ "method",
11
+ "status_code",
12
+ "colored_response_time",
13
+ "query_count",
14
+ "total_query_time_ms",
15
+ "exception_type",
16
+ "exception_message",
17
+ "created_at",
18
+ )
19
+
20
+ list_filter = (
21
+ "method",
22
+ "status_code",
23
+ "created_at"
24
+ )
25
+
26
+ readonly_fields = list_display
27
+
28
+ ordering = ("-created_at",)
29
+
30
+ def colored_response_time(self, obj):
31
+
32
+ if obj.response_time_ms > 1000:
33
+ color = "red"
34
+
35
+ elif obj.response_time_ms > 500:
36
+ color = "orange"
37
+
38
+ else:
39
+ color = "lightgreen"
40
+
41
+ formatted_time = f"{obj.response_time_ms:.2f} ms"
42
+
43
+ return format_html(
44
+ '<span style="color: {}; font-weight: bold;">{}</span>',
45
+ color,
46
+ formatted_time
47
+ )
48
+
49
+
50
+ @admin.register(EndpointSummary)
51
+ class EndpointSummaryAdmin(admin.ModelAdmin):
52
+
53
+ list_display = (
54
+ "route",
55
+ "window_start",
56
+ "total_requests",
57
+ "avg_response_ms",
58
+ "p95_response_ms",
59
+ "error_count",
60
+ "slow_count",
61
+ )
62
+
63
+ list_filter = (
64
+ "window_start",
65
+ "computed_at",
66
+ )
67
+
68
+ search_fields = ("route",)
69
+
70
+ ordering = ("-window_start",)
71
+
72
+ readonly_fields = [field.name for field in EndpointSummary._meta.fields]
73
+
74
+ date_hierarchy = "window_start"
75
+
76
+ def has_add_permission(self, request):
77
+ return False
78
+
79
+ def has_delete_permission(self, request, obj=None):
80
+ return False
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ProfilerConfig(AppConfig):
5
+ name = 'profiler'
@@ -0,0 +1,31 @@
1
+ from django.conf import settings
2
+
3
+ _DEFAULTS = {
4
+ "ASYNC": False,
5
+ "SLOW_REQUEST_THRESHOLD_MS": 1000,
6
+ "AGGREGATION_WINDOW_MINUTES": 60,
7
+ "IGNORED_PATHS": ["/admin", "/static", "/favicon.ico"],
8
+ "N_PLUS_ONE_THRESHOLD": 3,
9
+ "SLOW_QUERY_THRESHOLD_MS": 100,
10
+ "REGRESSION_RESPONSE_TIME_FACTOR": 2.0,
11
+ "REGRESSION_ERROR_RATE_DELTA": 0.1,
12
+ }
13
+
14
+
15
+ class ProfilerSettings:
16
+
17
+ def __init__(self):
18
+ self._user_settings = getattr(settings, "PROFILER", {})
19
+
20
+ def __getattr__(self, name):
21
+ if name not in _DEFAULTS:
22
+ raise AttributeError(f"Invalid profiler setting: '{name}'")
23
+
24
+ default = _DEFAULTS[name]
25
+ user_value = self._user_settings.get(name, default)
26
+
27
+ return user_value
28
+
29
+
30
+
31
+ profiler_settings = ProfilerSettings()
@@ -0,0 +1,33 @@
1
+ from django.db.backends.utils import CursorWrapper
2
+ import threading
3
+
4
+ _local = threading.local()
5
+
6
+ def reset_query_log():
7
+ _local.queries = []
8
+
9
+ def get_query_log():
10
+ return list(getattr(_local, "queries", []))
11
+
12
+ class ProfilingCursorWrapper(CursorWrapper):
13
+ def execute(self, sql, params=None):
14
+ import time
15
+ start = time.perf_counter()
16
+ try:
17
+ return super().execute(sql, params)
18
+ finally:
19
+ duration = (time.perf_counter() - start) * 1000
20
+ if not hasattr(_local, "queries"):
21
+ _local.queries = []
22
+ _local.queries.append({"sql": sql, "time_ms": duration})
23
+
24
+ def executemany(self, sql, param_list):
25
+ import time
26
+ start = time.perf_counter()
27
+ try:
28
+ return super().executemany(sql, param_list)
29
+ finally:
30
+ duration = (time.perf_counter() - start) * 1000
31
+ if not hasattr(_local, "queries"):
32
+ _local.queries = []
33
+ _local.queries.append({"sql": sql, "time_ms": duration})