django-scope 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 (78) hide show
  1. django_scope-0.1.0/.gitignore +26 -0
  2. django_scope-0.1.0/LICENSE +21 -0
  3. django_scope-0.1.0/PKG-INFO +87 -0
  4. django_scope-0.1.0/README.md +50 -0
  5. django_scope-0.1.0/pyproject.toml +67 -0
  6. django_scope-0.1.0/src/telescope/__init__.py +35 -0
  7. django_scope-0.1.0/src/telescope/apps.py +15 -0
  8. django_scope-0.1.0/src/telescope/backends/__init__.py +0 -0
  9. django_scope-0.1.0/src/telescope/backends/cache_backend.py +97 -0
  10. django_scope-0.1.0/src/telescope/backends/mail_backend.py +13 -0
  11. django_scope-0.1.0/src/telescope/consumers/__init__.py +0 -0
  12. django_scope-0.1.0/src/telescope/consumers/telescope_consumer.py +39 -0
  13. django_scope-0.1.0/src/telescope/context.py +37 -0
  14. django_scope-0.1.0/src/telescope/db_router.py +31 -0
  15. django_scope-0.1.0/src/telescope/entry_type.py +34 -0
  16. django_scope-0.1.0/src/telescope/filtering.py +56 -0
  17. django_scope-0.1.0/src/telescope/management/__init__.py +0 -0
  18. django_scope-0.1.0/src/telescope/management/commands/__init__.py +0 -0
  19. django_scope-0.1.0/src/telescope/management/commands/telescope_clear.py +29 -0
  20. django_scope-0.1.0/src/telescope/management/commands/telescope_prune.py +21 -0
  21. django_scope-0.1.0/src/telescope/middleware/__init__.py +3 -0
  22. django_scope-0.1.0/src/telescope/middleware/telescope_middleware.py +64 -0
  23. django_scope-0.1.0/src/telescope/migrations/0001_initial.py +54 -0
  24. django_scope-0.1.0/src/telescope/migrations/__init__.py +0 -0
  25. django_scope-0.1.0/src/telescope/models.py +49 -0
  26. django_scope-0.1.0/src/telescope/patches/__init__.py +0 -0
  27. django_scope-0.1.0/src/telescope/patches/command_patch.py +2 -0
  28. django_scope-0.1.0/src/telescope/patches/http_client_patch.py +82 -0
  29. django_scope-0.1.0/src/telescope/patches/redis_patch.py +44 -0
  30. django_scope-0.1.0/src/telescope/pruning.py +25 -0
  31. django_scope-0.1.0/src/telescope/recorder.py +135 -0
  32. django_scope-0.1.0/src/telescope/routing.py +7 -0
  33. django_scope-0.1.0/src/telescope/serializers.py +128 -0
  34. django_scope-0.1.0/src/telescope/settings.py +54 -0
  35. django_scope-0.1.0/src/telescope/static/telescope/assets/CodeBlock.vue_vue_type_script_setup_true_lang-CWnwRjkl.js +1 -0
  36. django_scope-0.1.0/src/telescope/static/telescope/assets/DashboardPage-CDM_ThxH.js +1 -0
  37. django_scope-0.1.0/src/telescope/static/telescope/assets/DurationBadge.vue_vue_type_script_setup_true_lang-VTWOmC8g.js +1 -0
  38. django_scope-0.1.0/src/telescope/static/telescope/assets/EntryDetailPage-BBz7GhBO.js +1 -0
  39. django_scope-0.1.0/src/telescope/static/telescope/assets/EntryTable.vue_vue_type_script_setup_true_lang-BXU10mml.js +1 -0
  40. django_scope-0.1.0/src/telescope/static/telescope/assets/ExceptionDetailPage-BkhuNBvQ.js +1 -0
  41. django_scope-0.1.0/src/telescope/static/telescope/assets/ExceptionsPage-bnRg7Glp.js +1 -0
  42. django_scope-0.1.0/src/telescope/static/telescope/assets/KeyValueTable.vue_vue_type_script_setup_true_lang-DrdZmCRu.js +1 -0
  43. django_scope-0.1.0/src/telescope/static/telescope/assets/MailDetailPage-cGPREsmc.js +1 -0
  44. django_scope-0.1.0/src/telescope/static/telescope/assets/QueriesPage-BUwgnN6Y.js +1 -0
  45. django_scope-0.1.0/src/telescope/static/telescope/assets/RequestDetailPage-CNYh-4Uo.js +1 -0
  46. django_scope-0.1.0/src/telescope/static/telescope/assets/RequestsPage-DZ6R_AFj.js +1 -0
  47. django_scope-0.1.0/src/telescope/static/telescope/assets/SearchBar.vue_vue_type_script_setup_true_lang-BGTl9mYY.js +1 -0
  48. django_scope-0.1.0/src/telescope/static/telescope/assets/StatusBadge.vue_vue_type_script_setup_true_lang-DpEbUkiq.js +1 -0
  49. django_scope-0.1.0/src/telescope/static/telescope/assets/TypedListPage-B8mjAFfL.js +1 -0
  50. django_scope-0.1.0/src/telescope/static/telescope/assets/api-DCUZqYAI.js +1 -0
  51. django_scope-0.1.0/src/telescope/static/telescope/assets/index.css +1 -0
  52. django_scope-0.1.0/src/telescope/static/telescope/assets/main.js +30 -0
  53. django_scope-0.1.0/src/telescope/static/telescope/assets/useTimeAgo-BLzdtsQE.js +1 -0
  54. django_scope-0.1.0/src/telescope/templates/telescope/index.html +20 -0
  55. django_scope-0.1.0/src/telescope/truncation.py +38 -0
  56. django_scope-0.1.0/src/telescope/urls.py +32 -0
  57. django_scope-0.1.0/src/telescope/views/__init__.py +0 -0
  58. django_scope-0.1.0/src/telescope/views/api.py +189 -0
  59. django_scope-0.1.0/src/telescope/views/spa.py +27 -0
  60. django_scope-0.1.0/src/telescope/watchers/__init__.py +72 -0
  61. django_scope-0.1.0/src/telescope/watchers/base.py +19 -0
  62. django_scope-0.1.0/src/telescope/watchers/batch_watcher.py +51 -0
  63. django_scope-0.1.0/src/telescope/watchers/cache_watcher.py +29 -0
  64. django_scope-0.1.0/src/telescope/watchers/client_request_watcher.py +10 -0
  65. django_scope-0.1.0/src/telescope/watchers/command_watcher.py +54 -0
  66. django_scope-0.1.0/src/telescope/watchers/dump_watcher.py +9 -0
  67. django_scope-0.1.0/src/telescope/watchers/event_watcher.py +63 -0
  68. django_scope-0.1.0/src/telescope/watchers/exception_watcher.py +86 -0
  69. django_scope-0.1.0/src/telescope/watchers/gate_watcher.py +37 -0
  70. django_scope-0.1.0/src/telescope/watchers/log_watcher.py +55 -0
  71. django_scope-0.1.0/src/telescope/watchers/mail_watcher.py +36 -0
  72. django_scope-0.1.0/src/telescope/watchers/model_watcher.py +84 -0
  73. django_scope-0.1.0/src/telescope/watchers/notification_watcher.py +9 -0
  74. django_scope-0.1.0/src/telescope/watchers/query_watcher.py +102 -0
  75. django_scope-0.1.0/src/telescope/watchers/redis_watcher.py +10 -0
  76. django_scope-0.1.0/src/telescope/watchers/request_watcher.py +126 -0
  77. django_scope-0.1.0/src/telescope/watchers/schedule_watcher.py +44 -0
  78. django_scope-0.1.0/src/telescope/watchers/view_watcher.py +37 -0
@@ -0,0 +1,26 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .env
12
+ *.sqlite3
13
+ db.sqlite3
14
+ node_modules/
15
+ frontend/dist/
16
+ .DS_Store
17
+ *.swp
18
+ *.swo
19
+ .idea/
20
+ .vscode/
21
+ *.log
22
+ .ruff_cache/
23
+ .pytest_cache/
24
+ .mypy_cache/
25
+ htmlcov/
26
+ .coverage
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MythicalCosmic
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,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-scope
3
+ Version: 0.1.0
4
+ Summary: An elegant debug assistant for Django, inspired by Laravel Telescope
5
+ Author: cosmic
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: debug,django,monitoring,profiling,telescope
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: Django :: 4.2
12
+ Classifier: Framework :: Django :: 5.0
13
+ Classifier: Framework :: Django :: 5.1
14
+ Classifier: Framework :: Django :: 6
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Debuggers
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: build>=1.4.2
25
+ Requires-Dist: channels>=4.0
26
+ Requires-Dist: daphne>=4.2.1
27
+ Requires-Dist: django>=4.2
28
+ Requires-Dist: setuptools>=82.0.1
29
+ Requires-Dist: twine>=6.2.0
30
+ Requires-Dist: wheel>=0.46.3
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio; extra == 'dev'
34
+ Requires-Dist: pytest-django; extra == 'dev'
35
+ Requires-Dist: ruff; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # django-telescope
39
+
40
+ An elegant debug assistant for Django, inspired by Laravel Telescope.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install django-telescope
46
+ ```
47
+
48
+ Add to your Django settings:
49
+
50
+ ```python
51
+ INSTALLED_APPS = [
52
+ "daphne",
53
+ "telescope",
54
+ # ... your apps
55
+ ]
56
+
57
+ TELESCOPE = {
58
+ "ENABLED": True,
59
+ }
60
+ ```
61
+
62
+ Add the middleware:
63
+
64
+ ```python
65
+ MIDDLEWARE = [
66
+ "telescope.middleware.TelescopeMiddleware",
67
+ # ... other middleware
68
+ ]
69
+ ```
70
+
71
+ Add URL routing:
72
+
73
+ ```python
74
+ from django.urls import include, path
75
+
76
+ urlpatterns = [
77
+ path("telescope/", include("telescope.urls")),
78
+ ]
79
+ ```
80
+
81
+ ## Features
82
+
83
+ - Real-time monitoring via WebSockets
84
+ - 17 watchers: requests, queries, exceptions, models, logs, cache, mail, Redis, views, events, commands, dumps, HTTP client, gates, notifications, schedules, batches
85
+ - N+1 query detection
86
+ - Beautiful Vue.js dashboard with dark/light themes
87
+ - Request-scoped buffering with bulk writes
@@ -0,0 +1,50 @@
1
+ # django-telescope
2
+
3
+ An elegant debug assistant for Django, inspired by Laravel Telescope.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install django-telescope
9
+ ```
10
+
11
+ Add to your Django settings:
12
+
13
+ ```python
14
+ INSTALLED_APPS = [
15
+ "daphne",
16
+ "telescope",
17
+ # ... your apps
18
+ ]
19
+
20
+ TELESCOPE = {
21
+ "ENABLED": True,
22
+ }
23
+ ```
24
+
25
+ Add the middleware:
26
+
27
+ ```python
28
+ MIDDLEWARE = [
29
+ "telescope.middleware.TelescopeMiddleware",
30
+ # ... other middleware
31
+ ]
32
+ ```
33
+
34
+ Add URL routing:
35
+
36
+ ```python
37
+ from django.urls import include, path
38
+
39
+ urlpatterns = [
40
+ path("telescope/", include("telescope.urls")),
41
+ ]
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - Real-time monitoring via WebSockets
47
+ - 17 watchers: requests, queries, exceptions, models, logs, cache, mail, Redis, views, events, commands, dumps, HTTP client, gates, notifications, schedules, batches
48
+ - N+1 query detection
49
+ - Beautiful Vue.js dashboard with dark/light themes
50
+ - Request-scoped buffering with bulk writes
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "django-scope"
7
+ version = "0.1.0"
8
+ description = "An elegant debug assistant for Django, inspired by Laravel Telescope"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "cosmic" }]
13
+ keywords = ["django", "debug", "telescope", "monitoring", "profiling"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: Django",
17
+ "Framework :: Django :: 4.2",
18
+ "Framework :: Django :: 5.0",
19
+ "Framework :: Django :: 5.1",
20
+ "Framework :: Django :: 6",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Software Development :: Debuggers",
29
+ ]
30
+ dependencies = [
31
+ "django>=4.2",
32
+ "channels>=4.0",
33
+ "daphne>=4.2.1",
34
+ "setuptools>=82.0.1",
35
+ "wheel>=0.46.3",
36
+ "twine>=6.2.0",
37
+ "build>=1.4.2",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "pytest",
43
+ "pytest-django",
44
+ "pytest-asyncio",
45
+ "ruff",
46
+ ]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/telescope"]
50
+
51
+ [tool.hatch.build.targets.sdist]
52
+ include = ["src/telescope/**", "LICENSE", "README.md"]
53
+
54
+ [tool.pytest.ini_options]
55
+ DJANGO_SETTINGS_MODULE = "tests.settings"
56
+ pythonpath = ["src", "."]
57
+
58
+ [tool.ruff]
59
+ src = ["src"]
60
+ line-length = 120
61
+
62
+ [dependency-groups]
63
+ dev = [
64
+ "pytest>=9.0.2",
65
+ "pytest-asyncio>=1.3.0",
66
+ "pytest-django>=4.12.0",
67
+ ]
@@ -0,0 +1,35 @@
1
+ default_app_config = "telescope.apps.TelescopeConfig"
2
+
3
+
4
+ def dump(value, *, label=None):
5
+ """Public API: record a dump entry from user code."""
6
+ import inspect
7
+ from .entry_type import EntryType
8
+ from .recorder import Recorder
9
+
10
+ frame = inspect.currentframe().f_back
11
+ caller = f"{frame.f_code.co_filename}:{frame.f_lineno}"
12
+
13
+ Recorder.record(
14
+ entry_type=EntryType.DUMP,
15
+ content={
16
+ "dump": repr(value),
17
+ "label": label,
18
+ "caller": caller,
19
+ },
20
+ )
21
+
22
+
23
+ def notify(notification_class, recipient, channels=None):
24
+ """Public API: record a notification entry."""
25
+ from .entry_type import EntryType
26
+ from .recorder import Recorder
27
+
28
+ Recorder.record(
29
+ entry_type=EntryType.NOTIFICATION,
30
+ content={
31
+ "notification": notification_class.__name__ if isinstance(notification_class, type) else str(notification_class),
32
+ "recipient": str(recipient),
33
+ "channels": channels or [],
34
+ },
35
+ )
@@ -0,0 +1,15 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class TelescopeConfig(AppConfig):
5
+ name = "telescope"
6
+ default_auto_field = "django.db.models.BigAutoField"
7
+ verbose_name = "Django Telescope"
8
+
9
+ def ready(self):
10
+ from .settings import get_config
11
+
12
+ if get_config("ENABLED"):
13
+ from .watchers import WatcherRegistry
14
+
15
+ WatcherRegistry.register_all()
File without changes
@@ -0,0 +1,97 @@
1
+ import time
2
+
3
+ from django.core.cache.backends.locmem import LocMemCache
4
+
5
+
6
+ class TelescopeCacheBackend(LocMemCache):
7
+ """Wrapping cache backend that records all cache operations."""
8
+
9
+ def __init__(self, server, params):
10
+ # The actual backend class to wrap can be specified in OPTIONS
11
+ self._telescope_alias = params.get("telescope_alias", "default")
12
+ actual_backend = params.pop("TELESCOPE_BACKEND", None)
13
+
14
+ if actual_backend:
15
+ # Dynamically instantiate the real backend
16
+ from django.utils.module_loading import import_string
17
+
18
+ backend_cls = import_string(actual_backend)
19
+ self._real_cache = backend_cls(server, params)
20
+ else:
21
+ super().__init__(server, params)
22
+ self._real_cache = None
23
+
24
+ def _get_backend(self):
25
+ return self._real_cache if self._real_cache else self
26
+
27
+ def get(self, key, default=None, version=None):
28
+ start = time.perf_counter()
29
+ backend = self._get_backend()
30
+ if backend is self:
31
+ value = super().get(key, default, version)
32
+ else:
33
+ value = backend.get(key, default, version)
34
+ duration_ms = (time.perf_counter() - start) * 1000
35
+
36
+ from ..watchers.cache_watcher import CacheWatcher
37
+
38
+ CacheWatcher.record_operation(
39
+ operation="get",
40
+ key=key,
41
+ value=value,
42
+ hit=value is not default,
43
+ duration_ms=duration_ms,
44
+ backend_alias=self._telescope_alias,
45
+ )
46
+ return value
47
+
48
+ def set(self, key, value, timeout=None, version=None):
49
+ start = time.perf_counter()
50
+ backend = self._get_backend()
51
+ if backend is self:
52
+ result = super().set(key, value, timeout, version)
53
+ else:
54
+ result = backend.set(key, value, timeout, version)
55
+ duration_ms = (time.perf_counter() - start) * 1000
56
+
57
+ from ..watchers.cache_watcher import CacheWatcher
58
+
59
+ CacheWatcher.record_operation(
60
+ operation="set",
61
+ key=key,
62
+ value=value,
63
+ duration_ms=duration_ms,
64
+ backend_alias=self._telescope_alias,
65
+ )
66
+ return result
67
+
68
+ def delete(self, key, version=None):
69
+ start = time.perf_counter()
70
+ backend = self._get_backend()
71
+ if backend is self:
72
+ result = super().delete(key, version)
73
+ else:
74
+ result = backend.delete(key, version)
75
+ duration_ms = (time.perf_counter() - start) * 1000
76
+
77
+ from ..watchers.cache_watcher import CacheWatcher
78
+
79
+ CacheWatcher.record_operation(
80
+ operation="delete",
81
+ key=key,
82
+ duration_ms=duration_ms,
83
+ backend_alias=self._telescope_alias,
84
+ )
85
+ return result
86
+
87
+ def clear(self):
88
+ backend = self._get_backend()
89
+ if backend is self:
90
+ result = super().clear()
91
+ else:
92
+ result = backend.clear()
93
+
94
+ from ..watchers.cache_watcher import CacheWatcher
95
+
96
+ CacheWatcher.record_operation(operation="clear", key="*")
97
+ return result
@@ -0,0 +1,13 @@
1
+ from django.core.mail.backends.smtp import EmailBackend
2
+
3
+
4
+ class TelescopeMailBackend(EmailBackend):
5
+ """Wrapping mail backend that records all sent emails."""
6
+
7
+ def send_messages(self, email_messages):
8
+ from ..watchers.mail_watcher import MailWatcher
9
+
10
+ for message in email_messages:
11
+ MailWatcher.record_mail(message)
12
+
13
+ return super().send_messages(email_messages)
File without changes
@@ -0,0 +1,39 @@
1
+ import json
2
+ import logging
3
+
4
+ from channels.generic.websocket import AsyncJsonWebsocketConsumer
5
+
6
+ logger = logging.getLogger("telescope.consumers")
7
+
8
+
9
+ class TelescopeConsumer(AsyncJsonWebsocketConsumer):
10
+ """WebSocket consumer for real-time telescope entry updates."""
11
+
12
+ async def connect(self):
13
+ await self.channel_layer.group_add("telescope", self.channel_name)
14
+ await self.accept()
15
+ logger.debug("Telescope WebSocket connected: %s", self.channel_name)
16
+
17
+ async def disconnect(self, close_code):
18
+ await self.channel_layer.group_discard("telescope", self.channel_name)
19
+ logger.debug("Telescope WebSocket disconnected: %s", self.channel_name)
20
+
21
+ async def receive_json(self, content, **kwargs):
22
+ # Client can send filter preferences
23
+ msg_type = content.get("type")
24
+ if msg_type == "filter":
25
+ # Store filter preferences on the consumer instance
26
+ self._filters = content.get("filters", {})
27
+
28
+ async def telescope_entry(self, event):
29
+ """Handle new telescope entry broadcast."""
30
+ entry = event.get("entry", {})
31
+
32
+ # Apply client-side filters if set
33
+ if hasattr(self, "_filters") and self._filters:
34
+ entry_type = entry.get("type_slug")
35
+ allowed_types = self._filters.get("types")
36
+ if allowed_types and entry_type not in allowed_types:
37
+ return
38
+
39
+ await self.send_json({"type": "entry", "data": entry})
@@ -0,0 +1,37 @@
1
+ import uuid
2
+ from contextvars import ContextVar
3
+
4
+ # Unique ID grouping all entries from the same request/command
5
+ batch_id_var: ContextVar[str | None] = ContextVar("telescope_batch_id", default=None)
6
+
7
+ # Buffer of entries to be flushed at end of request
8
+ buffer_var: ContextVar[list | None] = ContextVar("telescope_buffer", default=None)
9
+
10
+ # Whether recording is active for this scope
11
+ recording_var: ContextVar[bool] = ContextVar("telescope_recording", default=True)
12
+
13
+
14
+ def start_scope():
15
+ """Begin a new telescope scope (called at request start)."""
16
+ batch_id_var.set(str(uuid.uuid4()))
17
+ buffer_var.set([])
18
+ recording_var.set(True)
19
+
20
+
21
+ def end_scope():
22
+ """End the current telescope scope."""
23
+ batch_id_var.set(None)
24
+ buffer_var.set(None)
25
+ recording_var.set(True)
26
+
27
+
28
+ def get_batch_id():
29
+ return batch_id_var.get()
30
+
31
+
32
+ def get_buffer():
33
+ return buffer_var.get()
34
+
35
+
36
+ def is_recording():
37
+ return recording_var.get()
@@ -0,0 +1,31 @@
1
+ from .settings import get_config
2
+
3
+ APP_LABEL = "telescope"
4
+
5
+
6
+ class TelescopeRouter:
7
+ """Routes telescope models to a separate database if configured."""
8
+
9
+ def _db(self):
10
+ return get_config("DB_CONNECTION")
11
+
12
+ def db_for_read(self, model, **hints):
13
+ if model._meta.app_label == APP_LABEL:
14
+ return self._db()
15
+ return None
16
+
17
+ def db_for_write(self, model, **hints):
18
+ if model._meta.app_label == APP_LABEL:
19
+ return self._db()
20
+ return None
21
+
22
+ def allow_relation(self, obj1, obj2, **hints):
23
+ if obj1._meta.app_label == APP_LABEL or obj2._meta.app_label == APP_LABEL:
24
+ return True
25
+ return None
26
+
27
+ def allow_migrate(self, db, app_label, model_name=None, **hints):
28
+ target = self._db() or "default"
29
+ if app_label == APP_LABEL:
30
+ return db == target
31
+ return None
@@ -0,0 +1,34 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class EntryType(IntEnum):
5
+ REQUEST = 1
6
+ QUERY = 2
7
+ EXCEPTION = 3
8
+ MODEL = 4
9
+ LOG = 5
10
+ CACHE = 6
11
+ REDIS = 7
12
+ MAIL = 8
13
+ VIEW = 9
14
+ EVENT = 10
15
+ COMMAND = 11
16
+ DUMP = 12
17
+ CLIENT_REQUEST = 13
18
+ GATE = 14
19
+ NOTIFICATION = 15
20
+ SCHEDULE = 16
21
+ BATCH = 17
22
+
23
+ @property
24
+ def label(self):
25
+ return self.name.replace("_", " ").title()
26
+
27
+ @property
28
+ def slug(self):
29
+ return self.name.lower().replace("_", "-")
30
+
31
+ @classmethod
32
+ def from_slug(cls, slug):
33
+ name = slug.upper().replace("-", "_")
34
+ return cls[name]
@@ -0,0 +1,56 @@
1
+ from django.db.models import Q
2
+
3
+ from .entry_type import EntryType
4
+ from .models import TelescopeEntry
5
+
6
+
7
+ def apply_filters(queryset, params):
8
+ """Apply query filters from request parameters."""
9
+
10
+ # Filter by entry type
11
+ entry_type = params.get("type")
12
+ if entry_type:
13
+ try:
14
+ et = EntryType.from_slug(entry_type)
15
+ queryset = queryset.filter(type=et.value)
16
+ except (KeyError, ValueError):
17
+ pass
18
+
19
+ # Filter by tag
20
+ tag = params.get("tag")
21
+ if tag:
22
+ queryset = queryset.filter(tags__tag=tag)
23
+
24
+ # Filter by batch_id
25
+ batch_id = params.get("batch_id")
26
+ if batch_id:
27
+ queryset = queryset.filter(batch_id=batch_id)
28
+
29
+ # Filter by family_hash
30
+ family_hash = params.get("family_hash")
31
+ if family_hash:
32
+ queryset = queryset.filter(family_hash=family_hash)
33
+
34
+ # Search in content (JSON contains)
35
+ search = params.get("search")
36
+ if search:
37
+ queryset = queryset.filter(
38
+ Q(content__icontains=search) | Q(tags__tag__icontains=search)
39
+ ).distinct()
40
+
41
+ # Cursor-based pagination (before=<id>)
42
+ before = params.get("before")
43
+ if before:
44
+ try:
45
+ queryset = queryset.filter(id__lt=int(before))
46
+ except (ValueError, TypeError):
47
+ pass
48
+
49
+ # Limit
50
+ limit = params.get("limit", 50)
51
+ try:
52
+ limit = min(int(limit), 100)
53
+ except (ValueError, TypeError):
54
+ limit = 50
55
+
56
+ return queryset[:limit]
@@ -0,0 +1,29 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from telescope.entry_type import EntryType
4
+ from telescope.pruning import clear_entries
5
+
6
+
7
+ class Command(BaseCommand):
8
+ help = "Clear all telescope entries"
9
+
10
+ def add_arguments(self, parser):
11
+ parser.add_argument(
12
+ "--type",
13
+ type=str,
14
+ default=None,
15
+ help="Only clear entries of this type (e.g. 'request', 'query')",
16
+ )
17
+
18
+ def handle(self, *args, **options):
19
+ entry_type = None
20
+ type_slug = options["type"]
21
+ if type_slug:
22
+ try:
23
+ entry_type = EntryType.from_slug(type_slug).value
24
+ except (KeyError, ValueError):
25
+ self.stderr.write(self.style.ERROR(f"Invalid type: {type_slug}"))
26
+ return
27
+
28
+ count = clear_entries(entry_type=entry_type)
29
+ self.stdout.write(self.style.SUCCESS(f"Cleared {count} telescope entries"))
@@ -0,0 +1,21 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from telescope.pruning import prune_entries
4
+ from telescope.settings import get_config
5
+
6
+
7
+ class Command(BaseCommand):
8
+ help = "Prune old telescope entries"
9
+
10
+ def add_arguments(self, parser):
11
+ parser.add_argument(
12
+ "--hours",
13
+ type=int,
14
+ default=None,
15
+ help=f"Delete entries older than N hours (default: {get_config('ENTRY_LIFETIME_HOURS')})",
16
+ )
17
+
18
+ def handle(self, *args, **options):
19
+ hours = options["hours"]
20
+ count = prune_entries(hours=hours)
21
+ self.stdout.write(self.style.SUCCESS(f"Pruned {count} telescope entries"))
@@ -0,0 +1,3 @@
1
+ from .telescope_middleware import TelescopeMiddleware
2
+
3
+ __all__ = ["TelescopeMiddleware"]