varanus 0.1.0.dev3__tar.gz → 0.1.0.dev4__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.
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/PKG-INFO +1 -1
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/pyproject.toml +1 -1
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/client.py +72 -22
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/loggers.py +4 -2
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/middleware.py +9 -13
- varanus-0.1.0.dev4/src/varanus/client/transport/database.py +55 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/events.py +33 -6
- {varanus-0.1.0.dev3/src/varanus/server → varanus-0.1.0.dev4/src/varanus}/search/__init__.py +3 -2
- varanus-0.1.0.dev4/src/varanus/search/base.py +159 -0
- varanus-0.1.0.dev4/src/varanus/search/fields.py +189 -0
- varanus-0.1.0.dev4/src/varanus/search/templates/search/daterange.html +11 -0
- varanus-0.1.0.dev4/src/varanus/search/templates/search/filter.html +1 -0
- varanus-0.1.0.dev4/src/varanus/search/templates/search/hidden.html +12 -0
- varanus-0.1.0.dev4/src/varanus/search/templates/search/multifacet.html +6 -0
- varanus-0.1.0.dev4/src/varanus/search/templates/search/search.html +23 -0
- varanus-0.1.0.dev4/src/varanus/search/utils.py +38 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/admin.py +40 -14
- varanus-0.1.0.dev4/src/varanus/server/context_processors.py +9 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/migrations/0001_initial.py +157 -60
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/models.py +87 -16
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/router.py +3 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/settings.py +32 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/base.html +2 -2
- varanus-0.1.0.dev4/src/varanus/server/templates/dashboard.html +11 -0
- varanus-0.1.0.dev4/src/varanus/server/templates/site/base.html +65 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/details/error.html +1 -1
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/details/metric.html +4 -4
- varanus-0.1.0.dev4/src/varanus/server/templates/site/details/node_env.html +24 -0
- varanus-0.1.0.dev3/src/varanus/server/templates/site/details/node.html → varanus-0.1.0.dev4/src/varanus/server/templates/site/details/node_packages.html +1 -1
- varanus-0.1.0.dev4/src/varanus/server/templates/site/details/node_settings.html +24 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/details/query.html +17 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/details/request.html +27 -17
- varanus-0.1.0.dev4/src/varanus/server/templates/site/errors.html +44 -0
- varanus-0.1.0.dev4/src/varanus/server/templates/site/logs.html +47 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/metrics.html +17 -5
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/overview.html +10 -6
- varanus-0.1.0.dev4/src/varanus/server/templates/site/queries.html +64 -0
- varanus-0.1.0.dev4/src/varanus/server/templates/site/requests.html +54 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templatetags/varanus.py +7 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/urls.py +19 -2
- varanus-0.1.0.dev4/src/varanus/server/utils.py +8 -0
- varanus-0.1.0.dev4/src/varanus/server/views/dashboard.py +30 -0
- varanus-0.1.0.dev4/src/varanus/server/views/site.py +254 -0
- varanus-0.1.0.dev3/src/varanus/client/transport/database.py +0 -31
- varanus-0.1.0.dev3/src/varanus/server/context_processors.py +0 -7
- varanus-0.1.0.dev3/src/varanus/server/migrations/0002_context_node_error_node_log_node_metric_node_and_more.py +0 -79
- varanus-0.1.0.dev3/src/varanus/server/migrations/0003_alter_log_level.py +0 -28
- varanus-0.1.0.dev3/src/varanus/server/search/base.py +0 -116
- varanus-0.1.0.dev3/src/varanus/server/search/date.py +0 -33
- varanus-0.1.0.dev3/src/varanus/server/search/facet.py +0 -63
- varanus-0.1.0.dev3/src/varanus/server/templates/dashboard.html +0 -4
- varanus-0.1.0.dev3/src/varanus/server/templates/search/daterange.html +0 -10
- varanus-0.1.0.dev3/src/varanus/server/templates/search/multifacet.html +0 -11
- varanus-0.1.0.dev3/src/varanus/server/templates/site/base.html +0 -57
- varanus-0.1.0.dev3/src/varanus/server/templates/site/errors.html +0 -30
- varanus-0.1.0.dev3/src/varanus/server/templates/site/logs.html +0 -32
- varanus-0.1.0.dev3/src/varanus/server/templates/site/queries.html +0 -36
- varanus-0.1.0.dev3/src/varanus/server/templates/site/requests.html +0 -32
- varanus-0.1.0.dev3/src/varanus/server/views/dashboard.py +0 -11
- varanus-0.1.0.dev3/src/varanus/server/views/site.py +0 -165
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/README.md +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/apps.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/context.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/transport/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/transport/base.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/client/transport/http.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/__main__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/apps.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/asgi.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/management/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/management/commands/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/management/commands/migrateall.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/middleware.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/migrations/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/static/css/varanus.css +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/static/js/varanus.js +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/registration/login.html +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/details/environment_nodes.html +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/details/log.html +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templates/site/details/node_environments.html +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/templatetags/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/views/__init__.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/views/api.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/views/base.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/server/wsgi.py +0 -0
- {varanus-0.1.0.dev3 → varanus-0.1.0.dev4}/src/varanus/utils.py +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import platform
|
|
2
3
|
from importlib.metadata import distributions
|
|
4
|
+
from typing import Iterable
|
|
3
5
|
from urllib.parse import urlsplit
|
|
4
6
|
|
|
5
7
|
from varanus.events import Context, NodeInfo
|
|
@@ -18,23 +20,29 @@ def install_query_logger(logger):
|
|
|
18
20
|
return handler
|
|
19
21
|
|
|
20
22
|
|
|
23
|
+
def resolve_include_exclude(
|
|
24
|
+
items: Iterable[str],
|
|
25
|
+
include: Iterable[str] | bool,
|
|
26
|
+
exclude: Iterable[str] | None,
|
|
27
|
+
) -> set[str]:
|
|
28
|
+
if not include:
|
|
29
|
+
return set()
|
|
30
|
+
|
|
31
|
+
if include is True:
|
|
32
|
+
resolved = set(items)
|
|
33
|
+
else:
|
|
34
|
+
resolved = set(items).intersection(include)
|
|
35
|
+
|
|
36
|
+
if exclude is not None:
|
|
37
|
+
resolved.difference_update(exclude)
|
|
38
|
+
|
|
39
|
+
return resolved
|
|
40
|
+
|
|
41
|
+
|
|
21
42
|
class VaranusClient:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
request_attr: str
|
|
26
|
-
logger_name: str
|
|
27
|
-
tags: dict
|
|
28
|
-
|
|
29
|
-
include_headers: list | bool | None
|
|
30
|
-
exclude_headers: list | None
|
|
31
|
-
sensitive_headers = set(
|
|
32
|
-
[
|
|
33
|
-
"authorization",
|
|
34
|
-
"cookie",
|
|
35
|
-
"proxy-authorization",
|
|
36
|
-
]
|
|
37
|
-
)
|
|
43
|
+
sensitive_headers = set(["authorization", "cookie", "proxy-authorization"])
|
|
44
|
+
sensitive_settings = set(["DATABASES"])
|
|
45
|
+
sensitive_env = set(["PGPASSWORD"])
|
|
38
46
|
|
|
39
47
|
scheme_transports = {
|
|
40
48
|
"http": "varanus.client.transport.http.ThreadedHttpTransport",
|
|
@@ -42,22 +50,27 @@ class VaranusClient:
|
|
|
42
50
|
"db": "varanus.client.transport.database.ModelTransport",
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
send_all: bool
|
|
46
53
|
configured = False
|
|
47
54
|
|
|
48
55
|
def setup(
|
|
49
56
|
self,
|
|
50
57
|
dsn: str,
|
|
58
|
+
environment: str,
|
|
51
59
|
node: str | None = None,
|
|
52
|
-
environment: str = "",
|
|
53
60
|
transport_class: str | type[BaseTransport] | None = None,
|
|
54
61
|
request_attr: str = "varanus",
|
|
55
62
|
logger_name: str = "varanus.request",
|
|
56
63
|
tags: dict | None = None,
|
|
57
|
-
include_headers:
|
|
58
|
-
exclude_headers:
|
|
64
|
+
include_headers: Iterable[str] | bool = False,
|
|
65
|
+
exclude_headers: Iterable[str] | None = None,
|
|
66
|
+
include_settings: Iterable[str] | bool = False,
|
|
67
|
+
exclude_settings: Iterable[str] | None = None,
|
|
68
|
+
include_default_settings: bool = False,
|
|
69
|
+
include_env: Iterable[str] | bool = False,
|
|
70
|
+
exclude_env: Iterable[str] | None = None,
|
|
59
71
|
log_queries: bool | int = False,
|
|
60
72
|
log_query_params: bool = False,
|
|
73
|
+
log_query_stack: bool = False,
|
|
61
74
|
query_metrics: bool | str = False,
|
|
62
75
|
send_all: bool = False,
|
|
63
76
|
install: list | None = None,
|
|
@@ -83,7 +96,20 @@ class VaranusClient:
|
|
|
83
96
|
self.logger_name = logger_name
|
|
84
97
|
self.tags = tags or {}
|
|
85
98
|
self.include_headers = include_headers
|
|
86
|
-
self.exclude_headers =
|
|
99
|
+
self.exclude_headers = (
|
|
100
|
+
self.sensitive_headers if exclude_headers is None else set(exclude_headers)
|
|
101
|
+
)
|
|
102
|
+
self.include_settings = include_settings
|
|
103
|
+
self.exclude_settings = (
|
|
104
|
+
self.sensitive_settings
|
|
105
|
+
if exclude_settings is None
|
|
106
|
+
else set(exclude_settings)
|
|
107
|
+
)
|
|
108
|
+
self.include_default_settings = include_default_settings
|
|
109
|
+
self.include_env = include_env
|
|
110
|
+
self.exclude_env = (
|
|
111
|
+
self.sensitive_env if exclude_env is None else set(exclude_env)
|
|
112
|
+
)
|
|
87
113
|
if log_queries or query_metrics:
|
|
88
114
|
try:
|
|
89
115
|
# The logger is installed as early as possible, and for all connections.
|
|
@@ -93,6 +119,7 @@ class VaranusClient:
|
|
|
93
119
|
self.query_logger = QueryLogger(
|
|
94
120
|
log_queries,
|
|
95
121
|
log_query_params,
|
|
122
|
+
log_query_stack,
|
|
96
123
|
query_metrics,
|
|
97
124
|
)
|
|
98
125
|
# Install it in each new connection (if it's not already installed).
|
|
@@ -122,12 +149,35 @@ class VaranusClient:
|
|
|
122
149
|
self.transport.send(e)
|
|
123
150
|
|
|
124
151
|
def ping(self):
|
|
152
|
+
from django import get_version
|
|
153
|
+
from django.conf import settings
|
|
154
|
+
|
|
155
|
+
include_settings = resolve_include_exclude(
|
|
156
|
+
[s for s in dir(settings) if s == s.upper()],
|
|
157
|
+
self.include_settings,
|
|
158
|
+
self.exclude_settings,
|
|
159
|
+
)
|
|
160
|
+
include_env = resolve_include_exclude(
|
|
161
|
+
os.environ.keys(),
|
|
162
|
+
self.include_env,
|
|
163
|
+
self.exclude_env,
|
|
164
|
+
)
|
|
165
|
+
|
|
125
166
|
self.transport.ping(
|
|
126
167
|
NodeInfo(
|
|
127
168
|
name=self.node,
|
|
128
169
|
platform=platform.platform(),
|
|
129
|
-
|
|
170
|
+
language=platform.python_implementation(),
|
|
171
|
+
language_version=platform.python_version(),
|
|
172
|
+
framework="Django",
|
|
173
|
+
framework_version=get_version(),
|
|
130
174
|
packages={d.name: d.version for d in distributions()},
|
|
175
|
+
settings={
|
|
176
|
+
s: repr(getattr(settings, s))
|
|
177
|
+
for s in include_settings
|
|
178
|
+
if self.include_default_settings or settings.is_overridden(s)
|
|
179
|
+
},
|
|
180
|
+
environment={e: os.environ[e] for e in include_env},
|
|
131
181
|
)
|
|
132
182
|
)
|
|
133
183
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
|
-
from ..events import Log, Query, now
|
|
4
|
+
from ..events import Log, Query, capture_stack, now
|
|
5
5
|
from .context import ONE_MS, current_context
|
|
6
6
|
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ class VaranusHandler(logging.Handler):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class QueryLogger:
|
|
15
|
-
def __init__(self, threshold, log_params, metrics):
|
|
15
|
+
def __init__(self, threshold, log_params, log_stack, metrics):
|
|
16
16
|
# TODO: add callback for tagging?
|
|
17
17
|
if threshold is True:
|
|
18
18
|
self.threshold = 0
|
|
@@ -21,6 +21,7 @@ class QueryLogger:
|
|
|
21
21
|
else:
|
|
22
22
|
self.threshold = int(threshold)
|
|
23
23
|
self.log_params = log_params
|
|
24
|
+
self.log_stack = log_stack
|
|
24
25
|
if isinstance(metrics, str):
|
|
25
26
|
self.metrics_name = metrics
|
|
26
27
|
else:
|
|
@@ -55,5 +56,6 @@ class QueryLogger:
|
|
|
55
56
|
db=context["connection"].alias,
|
|
56
57
|
elapsed_ms=elapsed_ms,
|
|
57
58
|
success=success,
|
|
59
|
+
stack=capture_stack(1) if self.log_stack else [],
|
|
58
60
|
)
|
|
59
61
|
)
|
|
@@ -4,7 +4,7 @@ from django.core.exceptions import MiddlewareNotUsed
|
|
|
4
4
|
from django.http import HttpRequest, HttpResponse
|
|
5
5
|
|
|
6
6
|
from ..events import Request
|
|
7
|
-
from .client import client
|
|
7
|
+
from .client import client, resolve_include_exclude
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def get_ip(request: HttpRequest):
|
|
@@ -22,17 +22,12 @@ def get_ip(request: HttpRequest):
|
|
|
22
22
|
|
|
23
23
|
def request_headers(request: HttpRequest):
|
|
24
24
|
headers = {}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if client.exclude_headers is None:
|
|
32
|
-
exclude = client.sensitive_headers
|
|
33
|
-
else:
|
|
34
|
-
exclude = set(name.lower() for name in client.exclude_headers)
|
|
35
|
-
for name in sorted(include - exclude):
|
|
25
|
+
include_headers = resolve_include_exclude(
|
|
26
|
+
[name.lower() for name in request.headers],
|
|
27
|
+
client.include_headers,
|
|
28
|
+
client.exclude_headers,
|
|
29
|
+
)
|
|
30
|
+
for name in include_headers:
|
|
36
31
|
value = request.headers.get(name)
|
|
37
32
|
if value is not None:
|
|
38
33
|
headers[name] = value
|
|
@@ -59,8 +54,9 @@ class VaranusMiddleware:
|
|
|
59
54
|
# TODO: any need for request tags separate from context tags?
|
|
60
55
|
varanus.request = Request(
|
|
61
56
|
host=request.get_host(),
|
|
62
|
-
path=request.path,
|
|
63
57
|
method=request.method or "",
|
|
58
|
+
path=request.path,
|
|
59
|
+
query=request.META.get("QUERY_STRING", ""),
|
|
64
60
|
status=response.status_code,
|
|
65
61
|
headers=request_headers(request),
|
|
66
62
|
size=(
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
import uuid
|
|
3
|
+
from urllib.parse import SplitResult
|
|
4
|
+
|
|
5
|
+
from varanus import events
|
|
6
|
+
|
|
7
|
+
from .base import BaseTransport
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from varanus.server.models import Node, Site
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelTransport(BaseTransport):
|
|
14
|
+
site: "Site"
|
|
15
|
+
node: "Node"
|
|
16
|
+
|
|
17
|
+
def __init__(self, url: SplitResult, environment: str, node: str):
|
|
18
|
+
self.slug = url.netloc
|
|
19
|
+
self.environment = environment
|
|
20
|
+
|
|
21
|
+
def ensure_site(self):
|
|
22
|
+
if hasattr(self, "site"):
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
from varanus.server.models import Site
|
|
26
|
+
|
|
27
|
+
self.site, created = Site.objects.get_or_create(
|
|
28
|
+
slug=self.slug,
|
|
29
|
+
defaults={
|
|
30
|
+
"name": self.slug,
|
|
31
|
+
"schema_name": self.slug,
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def ping(self, info: events.NodeInfo):
|
|
36
|
+
from varanus.server.models import Node
|
|
37
|
+
|
|
38
|
+
self.ensure_site()
|
|
39
|
+
|
|
40
|
+
with self.site.activated():
|
|
41
|
+
self.node, created, updates = Node.update(
|
|
42
|
+
info, site=self.site, environment=self.environment
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def send(self, event: events.Context):
|
|
46
|
+
from varanus.server.models import Context
|
|
47
|
+
|
|
48
|
+
with self.site.activated():
|
|
49
|
+
Context.from_event(
|
|
50
|
+
event,
|
|
51
|
+
event_id=uuid.uuid4(),
|
|
52
|
+
site=self.site,
|
|
53
|
+
environment=self.environment,
|
|
54
|
+
node=self.node,
|
|
55
|
+
)
|
|
@@ -17,11 +17,16 @@ class Event(Struct, kw_only=True, omit_defaults=True):
|
|
|
17
17
|
class NodeInfo(Struct):
|
|
18
18
|
name: str
|
|
19
19
|
platform: str
|
|
20
|
-
|
|
20
|
+
language: str
|
|
21
|
+
language_version: str
|
|
22
|
+
framework: str
|
|
23
|
+
framework_version: str
|
|
21
24
|
packages: dict[str, str]
|
|
25
|
+
settings: dict[str, str]
|
|
26
|
+
environment: dict[str, str]
|
|
22
27
|
|
|
23
28
|
|
|
24
|
-
class
|
|
29
|
+
class StackLine(Struct):
|
|
25
30
|
file: str | None
|
|
26
31
|
lineno: int | None
|
|
27
32
|
function: str | None
|
|
@@ -30,11 +35,31 @@ class ErrorLine(Struct):
|
|
|
30
35
|
locals: dict[str, str]
|
|
31
36
|
|
|
32
37
|
|
|
38
|
+
def capture_stack(skip: int = 0, include_locals: bool = False) -> list[StackLine]:
|
|
39
|
+
lines = []
|
|
40
|
+
for frame in inspect.stack()[skip + 1 :]:
|
|
41
|
+
lines.append(
|
|
42
|
+
StackLine(
|
|
43
|
+
file=frame.filename,
|
|
44
|
+
lineno=frame.lineno,
|
|
45
|
+
linesrc=frame.code_context[0].strip() if frame.code_context else "",
|
|
46
|
+
function=frame.function,
|
|
47
|
+
module=frame.frame.f_globals.get("__name__", ""),
|
|
48
|
+
locals=(
|
|
49
|
+
{name: repr(val) for name, val in frame.frame.f_locals.items()}
|
|
50
|
+
if include_locals
|
|
51
|
+
else {}
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return lines
|
|
56
|
+
|
|
57
|
+
|
|
33
58
|
class Error(Event):
|
|
34
59
|
kind: str
|
|
35
60
|
module: str
|
|
36
61
|
message: str
|
|
37
|
-
|
|
62
|
+
stack: list[StackLine] = []
|
|
38
63
|
|
|
39
64
|
@classmethod
|
|
40
65
|
def from_exception(cls, exc_info, tags=None):
|
|
@@ -63,7 +88,7 @@ class Error(Event):
|
|
|
63
88
|
pass
|
|
64
89
|
module = f_globals.get("__name__", "")
|
|
65
90
|
lines.append(
|
|
66
|
-
|
|
91
|
+
StackLine(
|
|
67
92
|
file=abs_path,
|
|
68
93
|
lineno=lineno,
|
|
69
94
|
function=function,
|
|
@@ -78,7 +103,7 @@ class Error(Event):
|
|
|
78
103
|
kind=kind,
|
|
79
104
|
module=module,
|
|
80
105
|
message=message,
|
|
81
|
-
|
|
106
|
+
stack=lines,
|
|
82
107
|
)
|
|
83
108
|
|
|
84
109
|
|
|
@@ -144,12 +169,14 @@ class Query(Event):
|
|
|
144
169
|
db: str
|
|
145
170
|
elapsed_ms: int
|
|
146
171
|
success: bool
|
|
172
|
+
stack: list[StackLine] = []
|
|
147
173
|
|
|
148
174
|
|
|
149
175
|
class Request(Event):
|
|
150
176
|
host: str
|
|
151
|
-
path: str
|
|
152
177
|
method: str
|
|
178
|
+
path: str
|
|
179
|
+
query: str
|
|
153
180
|
status: int
|
|
154
181
|
headers: dict = {}
|
|
155
182
|
size: int | None = None
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Callable, ClassVar, Iterable, Literal, Self
|
|
2
|
+
|
|
3
|
+
from django.db.models import QuerySet
|
|
4
|
+
from django.http import HttpRequest
|
|
5
|
+
from django.template import loader
|
|
6
|
+
from django.utils.datastructures import MultiValueDict
|
|
7
|
+
from django.utils.functional import Promise
|
|
8
|
+
from django.utils.safestring import SafeString
|
|
9
|
+
|
|
10
|
+
from .utils import Namer, StringValues, string_data
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SkipRender(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SearchField:
|
|
18
|
+
template_name: str
|
|
19
|
+
prefixed = False
|
|
20
|
+
default = False
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
label=None,
|
|
25
|
+
field_name: str | Iterable[str] | None = None,
|
|
26
|
+
empty_label="NULL",
|
|
27
|
+
# These belong on Facet
|
|
28
|
+
choice_label: Callable[..., str | Promise] | None = None,
|
|
29
|
+
order: Literal["value", "count"] = "value",
|
|
30
|
+
default: bool = False,
|
|
31
|
+
):
|
|
32
|
+
self.name = ""
|
|
33
|
+
self.label = label or ""
|
|
34
|
+
self.field_name = field_name or ""
|
|
35
|
+
self.prefix = ""
|
|
36
|
+
self.id = ""
|
|
37
|
+
self.empty_label = empty_label
|
|
38
|
+
self.choice_label = choice_label
|
|
39
|
+
self.order = order
|
|
40
|
+
self.default = default
|
|
41
|
+
|
|
42
|
+
def __set_name__(self, owner, name: str):
|
|
43
|
+
assert issubclass(owner, Search)
|
|
44
|
+
self.name = name
|
|
45
|
+
self.prefix = f"{name}_" if self.prefixed else ""
|
|
46
|
+
self.id = f"id_{name}"
|
|
47
|
+
if not self.label:
|
|
48
|
+
self.label = name.capitalize().replace("__", " ")
|
|
49
|
+
if not self.field_name:
|
|
50
|
+
self.field_name = name
|
|
51
|
+
if not hasattr(owner, "_fields"):
|
|
52
|
+
owner._fields = []
|
|
53
|
+
owner._fields.append(self)
|
|
54
|
+
|
|
55
|
+
def __get__(self, instance: "Search", owner=None) -> StringValues | Self:
|
|
56
|
+
if owner is None:
|
|
57
|
+
return self
|
|
58
|
+
if self.prefixed:
|
|
59
|
+
return {
|
|
60
|
+
key[len(self.prefix) :]: values
|
|
61
|
+
for key, values in instance._data.items()
|
|
62
|
+
if key.startswith(self.prefix)
|
|
63
|
+
}
|
|
64
|
+
else:
|
|
65
|
+
return (
|
|
66
|
+
{self.name: instance._data[self.name]}
|
|
67
|
+
if self.name in instance._data
|
|
68
|
+
else {}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def __set__(self, instance, value):
|
|
72
|
+
raise AttributeError(f"`{self.name}` is read-only")
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def named(self):
|
|
76
|
+
"""
|
|
77
|
+
Allows for {{ field.named.something }} in templates.
|
|
78
|
+
"""
|
|
79
|
+
return Namer(self.prefix)
|
|
80
|
+
|
|
81
|
+
def apply(
|
|
82
|
+
self,
|
|
83
|
+
queryset: QuerySet,
|
|
84
|
+
field_data: StringValues,
|
|
85
|
+
request: HttpRequest | None = None,
|
|
86
|
+
) -> QuerySet:
|
|
87
|
+
raise NotImplementedError()
|
|
88
|
+
|
|
89
|
+
def has_value(
|
|
90
|
+
self,
|
|
91
|
+
queryset: QuerySet,
|
|
92
|
+
field_data: StringValues,
|
|
93
|
+
request: HttpRequest | None = None,
|
|
94
|
+
) -> bool:
|
|
95
|
+
return any(any(v) for v in field_data.values())
|
|
96
|
+
|
|
97
|
+
def get_context(
|
|
98
|
+
self,
|
|
99
|
+
queryset: QuerySet,
|
|
100
|
+
field_data: StringValues,
|
|
101
|
+
request: HttpRequest | None = None,
|
|
102
|
+
) -> dict:
|
|
103
|
+
return {
|
|
104
|
+
"field": self,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def render(
|
|
108
|
+
self,
|
|
109
|
+
queryset: QuerySet,
|
|
110
|
+
field_data: StringValues,
|
|
111
|
+
request: HttpRequest | None = None,
|
|
112
|
+
) -> SafeString:
|
|
113
|
+
return loader.render_to_string(
|
|
114
|
+
self.template_name,
|
|
115
|
+
self.get_context(queryset, field_data, request=request),
|
|
116
|
+
request,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Search:
|
|
121
|
+
_fields: ClassVar[list[SearchField]]
|
|
122
|
+
template_name = "search/search.html"
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
queryset: QuerySet,
|
|
127
|
+
data: dict | MultiValueDict | None = None,
|
|
128
|
+
request: HttpRequest | None = None,
|
|
129
|
+
):
|
|
130
|
+
self._queryset = queryset
|
|
131
|
+
self._data = string_data(data)
|
|
132
|
+
self._request = request
|
|
133
|
+
|
|
134
|
+
def queryset(self, for_field=None):
|
|
135
|
+
qs = self._queryset
|
|
136
|
+
for field in self._fields:
|
|
137
|
+
if field == for_field:
|
|
138
|
+
continue
|
|
139
|
+
field_data = getattr(self, field.name)
|
|
140
|
+
qs = field.apply(qs, field_data, request=self._request)
|
|
141
|
+
return qs
|
|
142
|
+
|
|
143
|
+
def render(self) -> SafeString:
|
|
144
|
+
context = {"fields": []}
|
|
145
|
+
initial = not bool(self._data)
|
|
146
|
+
for field in self._fields:
|
|
147
|
+
qs = self.queryset(for_field=field)
|
|
148
|
+
field_data = getattr(self, field.name)
|
|
149
|
+
try:
|
|
150
|
+
rendered = field.render(qs, field_data, request=self._request)
|
|
151
|
+
default = (initial and field.default) or field.has_value(
|
|
152
|
+
qs,
|
|
153
|
+
field_data,
|
|
154
|
+
request=self._request,
|
|
155
|
+
)
|
|
156
|
+
context["fields"].append((field, default, rendered))
|
|
157
|
+
except SkipRender:
|
|
158
|
+
continue
|
|
159
|
+
return loader.render_to_string(self.template_name, context, self._request)
|