django-api-usage 0.1.7__tar.gz → 0.1.9__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.
- {django_api_usage-0.1.7/src/django_api_usage.egg-info → django_api_usage-0.1.9}/PKG-INFO +28 -16
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/README.md +27 -15
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/pyproject.toml +1 -1
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/__init__.py +1 -1
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/admin.py +170 -13
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/apps.py +6 -7
- django_api_usage-0.1.9/src/django_api_usage/locale/eu/LC_MESSAGES/django.mo +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/locale/eu/LC_MESSAGES/django.po +60 -30
- django_api_usage-0.1.9/src/django_api_usage/management/commands/api_usage_backfill_paths.py +59 -0
- django_api_usage-0.1.9/src/django_api_usage/migrations/0004_remove_clientapp_accounts_clientappaccount.py +77 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/models.py +31 -6
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/resolvers.py +15 -5
- {django_api_usage-0.1.7 → django_api_usage-0.1.9/src/django_api_usage.egg-info}/PKG-INFO +28 -16
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage.egg-info/SOURCES.txt +5 -1
- django_api_usage-0.1.9/tests/test_backfill_paths.py +66 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_client_app.py +36 -2
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_filters.py +47 -0
- django_api_usage-0.1.9/tests/test_sites.py +92 -0
- django_api_usage-0.1.7/src/django_api_usage/locale/eu/LC_MESSAGES/django.mo +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/LICENSE +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/setup.cfg +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/buffers.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/checks.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/conf.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/deprecation.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/drf.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/maintenance.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/management/__init__.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/management/commands/__init__.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/management/commands/api_usage_flush.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/management/commands/api_usage_report.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/middleware.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/migrations/0001_initial.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/migrations/0002_endpoint_route_path.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/migrations/0003_alter_endpointstat_unique_together_and_more.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/migrations/__init__.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/tasks.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage.egg-info/dependency_links.txt +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage.egg-info/requires.txt +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/src/django_api_usage.egg-info/top_level.txt +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_admin.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_checks.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_commands.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_csv_export.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_drf.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_middleware.py +0 -0
- {django_api_usage-0.1.7 → django_api_usage-0.1.9}/tests/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-api-usage
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Lightweight, privacy-first usage metering for Django APIs, with an optional deprecation lifecycle layer.
|
|
5
5
|
Author-email: CodeSyntax <teknika@codesyntax.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -130,7 +130,9 @@ similar):
|
|
|
130
130
|
method or replacement), filters by date, client type, status class, and
|
|
131
131
|
**Application** / **Method**, plus the **sum of the `count` column** for the
|
|
132
132
|
rows matching the current filters. The Method filter lists the riskiest verbs
|
|
133
|
-
first (`DELETE`, `POST`, ...),
|
|
133
|
+
first (`DELETE`, `POST`, ...), and the list shows **Application** and
|
|
134
|
+
**Method** as columns — the verb as a coloured badge (writes amber, deletes
|
|
135
|
+
red), so the dangerous traffic is obvious at a glance.
|
|
134
136
|
* **Endpoint** changelist: the same Application / Method filters, plus
|
|
135
137
|
deprecation state, the site and the CSV export.
|
|
136
138
|
* **Flush now**: moves counters buffered in the cache into the database, so they
|
|
@@ -142,23 +144,33 @@ similar):
|
|
|
142
144
|
and method), so the file can be fed straight to pandas or a spreadsheet. Use
|
|
143
145
|
the "select all" link to export every row matching the current filters.
|
|
144
146
|
|
|
147
|
+
If you upgraded from an earlier release, run
|
|
148
|
+
`manage.py api_usage_backfill_paths` once: endpoints recorded before the path was
|
|
149
|
+
captured only have their route name, and the command resolves it back to the
|
|
150
|
+
path (`--dry-run` first if you want to see what it would do).
|
|
151
|
+
|
|
145
152
|
## Client applications
|
|
146
153
|
|
|
147
154
|
Not every caller is a person: ERPs, partners, your own web front end and the
|
|
148
|
-
native mobile apps also hit the API. `ClientApp` is an **editable table**,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
| Rule | Matched against | Use it for |
|
|
152
|
-
|
|
153
|
-
| `
|
|
154
|
-
| `domains` | the `Origin`/`Referer` host, subdomains included | your own web front end |
|
|
155
|
-
| `ip_networks` | the client address, against a CIDR (one per line) | internal networks and servers |
|
|
156
|
-
| `user_agent_patterns` | a case-insensitive substring of `User-Agent` | native mobile apps |
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
`
|
|
155
|
+
native mobile apps also hit the API. `ClientApp` is an **editable table**, and
|
|
156
|
+
the rules are checked in this order:
|
|
157
|
+
|
|
158
|
+
| # | Rule | Matched against | Use it for |
|
|
159
|
+
|---|---|---|---|
|
|
160
|
+
| 1 | `ClientAppAccount` | the authenticated account (and therefore its DRF token) | one dedicated token per integration (ERP, partner...) |
|
|
161
|
+
| 2 | `domains` | the `Origin`/`Referer` host, subdomains included | your own web front end |
|
|
162
|
+
| 3 | `ip_networks` | the client address, against a CIDR (one per line) | internal networks and servers |
|
|
163
|
+
| 4 | `user_agent_patterns` | a case-insensitive substring of `User-Agent` | native mobile apps |
|
|
164
|
+
|
|
165
|
+
An explicit assignment always wins: if the token's account belongs to an
|
|
166
|
+
application, that is the answer. Accounts without an assignment (an app user
|
|
167
|
+
reading the news) fall through to the user agent, which is what identifies the
|
|
168
|
+
native apps. `priority` decides between applications (lower wins), and a caller
|
|
169
|
+
matching nothing stays unattributed.
|
|
170
|
+
|
|
171
|
+
Assignments are one row per account (a point lookup, indexed) instead of a
|
|
172
|
+
many-to-many list holding every account, so nothing grows with the number of
|
|
173
|
+
users: `EndpointStat.client_app` only ever holds a `ClientApp.slug`.
|
|
162
174
|
|
|
163
175
|
Counters, the CSV export and the admin filters all carry the application, so
|
|
164
176
|
"which application calls this endpoint?" is one filter away. The **Endpoint
|
|
@@ -91,7 +91,9 @@ similar):
|
|
|
91
91
|
method or replacement), filters by date, client type, status class, and
|
|
92
92
|
**Application** / **Method**, plus the **sum of the `count` column** for the
|
|
93
93
|
rows matching the current filters. The Method filter lists the riskiest verbs
|
|
94
|
-
first (`DELETE`, `POST`, ...),
|
|
94
|
+
first (`DELETE`, `POST`, ...), and the list shows **Application** and
|
|
95
|
+
**Method** as columns — the verb as a coloured badge (writes amber, deletes
|
|
96
|
+
red), so the dangerous traffic is obvious at a glance.
|
|
95
97
|
* **Endpoint** changelist: the same Application / Method filters, plus
|
|
96
98
|
deprecation state, the site and the CSV export.
|
|
97
99
|
* **Flush now**: moves counters buffered in the cache into the database, so they
|
|
@@ -103,23 +105,33 @@ similar):
|
|
|
103
105
|
and method), so the file can be fed straight to pandas or a spreadsheet. Use
|
|
104
106
|
the "select all" link to export every row matching the current filters.
|
|
105
107
|
|
|
108
|
+
If you upgraded from an earlier release, run
|
|
109
|
+
`manage.py api_usage_backfill_paths` once: endpoints recorded before the path was
|
|
110
|
+
captured only have their route name, and the command resolves it back to the
|
|
111
|
+
path (`--dry-run` first if you want to see what it would do).
|
|
112
|
+
|
|
106
113
|
## Client applications
|
|
107
114
|
|
|
108
115
|
Not every caller is a person: ERPs, partners, your own web front end and the
|
|
109
|
-
native mobile apps also hit the API. `ClientApp` is an **editable table**,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
| Rule | Matched against | Use it for |
|
|
113
|
-
|
|
114
|
-
| `
|
|
115
|
-
| `domains` | the `Origin`/`Referer` host, subdomains included | your own web front end |
|
|
116
|
-
| `ip_networks` | the client address, against a CIDR (one per line) | internal networks and servers |
|
|
117
|
-
| `user_agent_patterns` | a case-insensitive substring of `User-Agent` | native mobile apps |
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`
|
|
116
|
+
native mobile apps also hit the API. `ClientApp` is an **editable table**, and
|
|
117
|
+
the rules are checked in this order:
|
|
118
|
+
|
|
119
|
+
| # | Rule | Matched against | Use it for |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| 1 | `ClientAppAccount` | the authenticated account (and therefore its DRF token) | one dedicated token per integration (ERP, partner...) |
|
|
122
|
+
| 2 | `domains` | the `Origin`/`Referer` host, subdomains included | your own web front end |
|
|
123
|
+
| 3 | `ip_networks` | the client address, against a CIDR (one per line) | internal networks and servers |
|
|
124
|
+
| 4 | `user_agent_patterns` | a case-insensitive substring of `User-Agent` | native mobile apps |
|
|
125
|
+
|
|
126
|
+
An explicit assignment always wins: if the token's account belongs to an
|
|
127
|
+
application, that is the answer. Accounts without an assignment (an app user
|
|
128
|
+
reading the news) fall through to the user agent, which is what identifies the
|
|
129
|
+
native apps. `priority` decides between applications (lower wins), and a caller
|
|
130
|
+
matching nothing stays unattributed.
|
|
131
|
+
|
|
132
|
+
Assignments are one row per account (a point lookup, indexed) instead of a
|
|
133
|
+
many-to-many list holding every account, so nothing grows with the number of
|
|
134
|
+
users: `EndpointStat.client_app` only ever holds a `ClientApp.slug`.
|
|
123
135
|
|
|
124
136
|
Counters, the CSV export and the admin filters all carry the application, so
|
|
125
137
|
"which application calls this endpoint?" is one filter away. The **Endpoint
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-api-usage"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "Lightweight, privacy-first usage metering for Django APIs, with an optional deprecation lifecycle layer."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -14,6 +14,7 @@ Everything is plain Django: no extra dependency beyond Django itself.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import csv
|
|
17
|
+
import time
|
|
17
18
|
|
|
18
19
|
from django.contrib import admin, messages
|
|
19
20
|
from django.db.models import Count, OuterRef, Subquery, Sum
|
|
@@ -21,11 +22,12 @@ from django.http import HttpResponse, HttpResponseRedirect
|
|
|
21
22
|
from django.template.response import TemplateResponse
|
|
22
23
|
from django.urls import path, reverse
|
|
23
24
|
from django.utils import timezone
|
|
25
|
+
from django.utils.html import format_html
|
|
24
26
|
from django.utils.text import format_lazy
|
|
25
27
|
from django.utils.translation import gettext_lazy as _
|
|
26
28
|
|
|
27
29
|
from .buffers import flush
|
|
28
|
-
from .models import ClientApp, Consumer, Endpoint, EndpointStat
|
|
30
|
+
from .models import ClientApp, ClientAppAccount, Consumer, Endpoint, EndpointStat
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class CsvExportMixin:
|
|
@@ -64,6 +66,70 @@ class CsvExportMixin:
|
|
|
64
66
|
#: write/destructive calls (POST, DELETE, ...) instead of the read ones.
|
|
65
67
|
METHOD_ORDER = ("DELETE", "POST", "PUT", "PATCH", "GET", "HEAD", "OPTIONS")
|
|
66
68
|
|
|
69
|
+
#: Pill colours per verb: reads green/blue, writes amber, deletes red.
|
|
70
|
+
METHOD_COLORS = {
|
|
71
|
+
"GET": ("#d1e7dd", "#0f5132"),
|
|
72
|
+
"POST": ("#cfe2ff", "#084298"),
|
|
73
|
+
"PUT": ("#fff3cd", "#664d03"),
|
|
74
|
+
"PATCH": ("#fff3cd", "#664d03"),
|
|
75
|
+
"DELETE": ("#f8d7da", "#842029"),
|
|
76
|
+
"HEAD": ("#e2e3e5", "#41464b"),
|
|
77
|
+
"OPTIONS": ("#e2e3e5", "#41464b"),
|
|
78
|
+
}
|
|
79
|
+
DEFAULT_METHOD_COLORS = ("#e2e3e5", "#41464b")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
#: Site labels are read for every row of the changelist, so they are cached for
|
|
83
|
+
#: a short while (the table changes very rarely).
|
|
84
|
+
SITE_LABELS_TTL = 60
|
|
85
|
+
_site_labels_cache = {"loaded_at": 0.0, "value": None}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_site_labels():
|
|
89
|
+
"""``{pk: domain}`` when django.contrib.sites is installed, else ``{}``."""
|
|
90
|
+
try:
|
|
91
|
+
from django.contrib.sites.models import Site
|
|
92
|
+
except (ImportError, RuntimeError):
|
|
93
|
+
return {}
|
|
94
|
+
return dict(Site.objects.values_list("pk", "domain"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _site_labels():
|
|
98
|
+
now = time.monotonic()
|
|
99
|
+
if (
|
|
100
|
+
_site_labels_cache["value"] is None
|
|
101
|
+
or now - _site_labels_cache["loaded_at"] > SITE_LABELS_TTL
|
|
102
|
+
):
|
|
103
|
+
_site_labels_cache["value"] = _load_site_labels()
|
|
104
|
+
_site_labels_cache["loaded_at"] = now
|
|
105
|
+
return _site_labels_cache["value"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def reset_site_labels_cache(**kwargs):
|
|
109
|
+
"""Forget the cached site domains (used by tests and after editing sites)."""
|
|
110
|
+
_site_labels_cache["value"] = None
|
|
111
|
+
_site_labels_cache["loaded_at"] = 0.0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _site_label(site_id):
|
|
115
|
+
"""Domain of a site, falling back to the raw id (or ``None``)."""
|
|
116
|
+
if site_id is None:
|
|
117
|
+
return None
|
|
118
|
+
return _site_labels().get(site_id, str(site_id))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _method_badge_html(method):
|
|
122
|
+
"""Coloured pill for an HTTP verb, so writes stand out at a glance."""
|
|
123
|
+
verb = (method or "").upper()
|
|
124
|
+
background, color = METHOD_COLORS.get(verb, DEFAULT_METHOD_COLORS)
|
|
125
|
+
return format_html(
|
|
126
|
+
'<span style="background:{};color:{};padding:1px 7px;border-radius:9px;'
|
|
127
|
+
'font-size:11px;font-weight:600;white-space:nowrap">{}</span>',
|
|
128
|
+
background,
|
|
129
|
+
color,
|
|
130
|
+
verb,
|
|
131
|
+
)
|
|
132
|
+
|
|
67
133
|
|
|
68
134
|
class AppLabelListFilter(admin.SimpleListFilter):
|
|
69
135
|
"""Distinct application labels, under a readable title."""
|
|
@@ -122,6 +188,42 @@ class StatMethodListFilter(MethodListFilter):
|
|
|
122
188
|
field_path = "endpoint__method"
|
|
123
189
|
|
|
124
190
|
|
|
191
|
+
class SiteListFilter(admin.SimpleListFilter):
|
|
192
|
+
"""Sites, shown by domain when django.contrib.sites is available.
|
|
193
|
+
|
|
194
|
+
Useful when several front ends (several sites) call the same backend: the
|
|
195
|
+
endpoint's site tells them apart.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
title = _("Site")
|
|
199
|
+
parameter_name = "site"
|
|
200
|
+
field_path = "site_id"
|
|
201
|
+
|
|
202
|
+
def lookups(self, request, model_admin):
|
|
203
|
+
labels = _site_labels()
|
|
204
|
+
if labels:
|
|
205
|
+
return [(pk, domain) for pk, domain in sorted(labels.items())]
|
|
206
|
+
used = (
|
|
207
|
+
model_admin.model.objects.order_by()
|
|
208
|
+
.values_list(self.field_path, flat=True)
|
|
209
|
+
.distinct()
|
|
210
|
+
)
|
|
211
|
+
return [
|
|
212
|
+
(value, str(value)) for value in sorted(v for v in used if v is not None)
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
def queryset(self, request, queryset):
|
|
216
|
+
if not self.value():
|
|
217
|
+
return queryset
|
|
218
|
+
return queryset.filter(**{self.field_path: self.value()})
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class StatSiteListFilter(SiteListFilter):
|
|
222
|
+
"""Site filter for the stats changelist (through the endpoint)."""
|
|
223
|
+
|
|
224
|
+
field_path = "endpoint__site_id"
|
|
225
|
+
|
|
226
|
+
|
|
125
227
|
#: Sentinel for the calls that could not be attributed to any application.
|
|
126
228
|
UNATTRIBUTED = "__none__"
|
|
127
229
|
|
|
@@ -180,8 +282,8 @@ class EndpointAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
|
180
282
|
"app_label",
|
|
181
283
|
"route_path",
|
|
182
284
|
"route_name",
|
|
183
|
-
"
|
|
184
|
-
"
|
|
285
|
+
"method_badge",
|
|
286
|
+
"site",
|
|
185
287
|
"deprecated",
|
|
186
288
|
"sunset_date",
|
|
187
289
|
"replacement",
|
|
@@ -191,11 +293,19 @@ class EndpointAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
|
191
293
|
"deprecated",
|
|
192
294
|
AppLabelListFilter,
|
|
193
295
|
MethodListFilter,
|
|
194
|
-
|
|
296
|
+
SiteListFilter,
|
|
195
297
|
)
|
|
196
298
|
search_fields = ("route_path", "route_name", "replacement", "owner", "notes")
|
|
197
299
|
list_editable = ("deprecated", "sunset_date", "replacement", "owner")
|
|
198
300
|
|
|
301
|
+
@admin.display(description=_("Method"), ordering="method")
|
|
302
|
+
def method_badge(self, obj):
|
|
303
|
+
return _method_badge_html(obj.method)
|
|
304
|
+
|
|
305
|
+
@admin.display(description=_("Site"), ordering="site_id")
|
|
306
|
+
def site(self, obj):
|
|
307
|
+
return _site_label(obj.site_id)
|
|
308
|
+
|
|
199
309
|
|
|
200
310
|
@admin.register(EndpointStat)
|
|
201
311
|
class EndpointStatAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
@@ -216,7 +326,10 @@ class EndpointStatAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
|
216
326
|
)
|
|
217
327
|
list_display = (
|
|
218
328
|
"date",
|
|
219
|
-
"
|
|
329
|
+
"site",
|
|
330
|
+
"application",
|
|
331
|
+
"method_badge",
|
|
332
|
+
"endpoint_path",
|
|
220
333
|
"client_type",
|
|
221
334
|
"client_app",
|
|
222
335
|
"status_class",
|
|
@@ -227,6 +340,7 @@ class EndpointStatAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
|
227
340
|
"client_type",
|
|
228
341
|
ClientAppListFilter,
|
|
229
342
|
"status_class",
|
|
343
|
+
StatSiteListFilter,
|
|
230
344
|
StatAppLabelListFilter,
|
|
231
345
|
StatMethodListFilter,
|
|
232
346
|
)
|
|
@@ -246,6 +360,26 @@ class EndpointStatAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
|
246
360
|
)
|
|
247
361
|
change_list_template = "admin/django_api_usage/endpointstat/change_list.html"
|
|
248
362
|
|
|
363
|
+
# -- columns ------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
@admin.display(description=_("Application"), ordering="endpoint__app_label")
|
|
366
|
+
def application(self, obj):
|
|
367
|
+
return obj.endpoint.app_label
|
|
368
|
+
|
|
369
|
+
@admin.display(description=_("Site"), ordering="endpoint__site_id")
|
|
370
|
+
def site(self, obj):
|
|
371
|
+
return _site_label(obj.endpoint.site_id)
|
|
372
|
+
|
|
373
|
+
@admin.display(description=_("Method"), ordering="endpoint__method")
|
|
374
|
+
def method_badge(self, obj):
|
|
375
|
+
return _method_badge_html(obj.endpoint.method)
|
|
376
|
+
|
|
377
|
+
@admin.display(description=_("Endpoint"), ordering="endpoint__route_path")
|
|
378
|
+
def endpoint_path(self, obj):
|
|
379
|
+
"""Just the path: the verb and the app already have their own columns."""
|
|
380
|
+
endpoint = obj.endpoint
|
|
381
|
+
return (endpoint.route_path or endpoint.route_name or "").strip("^$")
|
|
382
|
+
|
|
249
383
|
def has_add_permission(self, request):
|
|
250
384
|
return False
|
|
251
385
|
|
|
@@ -388,22 +522,17 @@ class ClientAppAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
|
388
522
|
)
|
|
389
523
|
list_filter = ("is_active",)
|
|
390
524
|
search_fields = ("slug", "name", "description")
|
|
391
|
-
filter_horizontal = ("accounts",)
|
|
392
525
|
fieldsets = (
|
|
393
526
|
(None, {"fields": ("slug", "name", "description", "priority", "is_active")}),
|
|
394
527
|
(
|
|
395
528
|
_("Matching rules"),
|
|
396
529
|
{
|
|
397
|
-
"fields": (
|
|
398
|
-
"accounts",
|
|
399
|
-
"domains",
|
|
400
|
-
"ip_networks",
|
|
401
|
-
"user_agent_patterns",
|
|
402
|
-
),
|
|
530
|
+
"fields": ("domains", "ip_networks", "user_agent_patterns"),
|
|
403
531
|
"description": _(
|
|
404
532
|
"Checked in this order: account, request host, client "
|
|
405
533
|
"network, user agent. Between applications, the lowest "
|
|
406
|
-
"priority wins."
|
|
534
|
+
"priority wins. Accounts are assigned from the "
|
|
535
|
+
"'Client application accounts' table."
|
|
407
536
|
),
|
|
408
537
|
},
|
|
409
538
|
),
|
|
@@ -431,3 +560,31 @@ class ClientAppAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
|
431
560
|
@admin.display(description=_("Counters"), ordering="counters_total")
|
|
432
561
|
def counters_total(self, obj):
|
|
433
562
|
return obj.counters_total or 0
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@admin.register(ClientAppAccount)
|
|
566
|
+
class ClientAppAccountAdmin(CsvExportMixin, admin.ModelAdmin):
|
|
567
|
+
"""One row per assigned account: search, do not scroll a giant list."""
|
|
568
|
+
|
|
569
|
+
actions = ("export_as_csv",)
|
|
570
|
+
csv_columns = (
|
|
571
|
+
("client_app", "client_app__slug"),
|
|
572
|
+
("username", "user__username"),
|
|
573
|
+
("email", "user__email"),
|
|
574
|
+
("notes", "notes"),
|
|
575
|
+
)
|
|
576
|
+
list_display = ("client_app", "account", "notes")
|
|
577
|
+
list_filter = ("client_app",)
|
|
578
|
+
search_fields = (
|
|
579
|
+
"user__username",
|
|
580
|
+
"user__email",
|
|
581
|
+
"user__first_name",
|
|
582
|
+
"user__last_name",
|
|
583
|
+
"notes",
|
|
584
|
+
)
|
|
585
|
+
raw_id_fields = ("user",)
|
|
586
|
+
list_select_related = ("user", "client_app")
|
|
587
|
+
|
|
588
|
+
@admin.display(description=_("Account"), ordering="user__username")
|
|
589
|
+
def account(self, obj):
|
|
590
|
+
return obj.user
|
|
@@ -13,8 +13,12 @@ class DjangoApiUsageConfig(AppConfig):
|
|
|
13
13
|
self._connect_client_app_signals()
|
|
14
14
|
|
|
15
15
|
def _connect_client_app_signals(self):
|
|
16
|
-
"""Drop the cached ClientApp rules as soon as the table changes.
|
|
17
|
-
|
|
16
|
+
"""Drop the cached ClientApp rules as soon as the table changes.
|
|
17
|
+
|
|
18
|
+
Account assignments are not cached (they are a single indexed lookup),
|
|
19
|
+
so only the rule table needs to invalidate anything.
|
|
20
|
+
"""
|
|
21
|
+
from django.db.models.signals import post_delete, post_save
|
|
18
22
|
|
|
19
23
|
from .models import ClientApp
|
|
20
24
|
from .resolvers import reset_client_app_cache
|
|
@@ -27,8 +31,3 @@ class DjangoApiUsageConfig(AppConfig):
|
|
|
27
31
|
sender=ClientApp,
|
|
28
32
|
dispatch_uid="api_usage_app_delete",
|
|
29
33
|
)
|
|
30
|
-
m2m_changed.connect(
|
|
31
|
-
reset_client_app_cache,
|
|
32
|
-
sender=ClientApp.accounts.through,
|
|
33
|
-
dispatch_uid="api_usage_app_accounts",
|
|
34
|
-
)
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: django-api-usage\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: \n"
|
|
10
|
-
"POT-Creation-Date: 2026-10-08
|
|
10
|
+
"POT-Creation-Date: 2026-10-08 08:11-0500\n"
|
|
11
11
|
"PO-Revision-Date: 2026-10-08 06:55-0500\n"
|
|
12
12
|
"Last-Translator: Urtzi Odriozola <uodriozola@codesyntax.com>\n"
|
|
13
13
|
"Language-Team: Basque\n"
|
|
@@ -17,27 +17,33 @@ msgstr ""
|
|
|
17
17
|
"Content-Transfer-Encoding: 8bit\n"
|
|
18
18
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
19
19
|
|
|
20
|
-
#: src/django_api_usage/admin.py:
|
|
20
|
+
#: src/django_api_usage/admin.py:48
|
|
21
21
|
msgid "Export selected rows to CSV"
|
|
22
22
|
msgstr "Esportatu hautatutako errenkadak CSVra"
|
|
23
23
|
|
|
24
|
-
#: src/django_api_usage/admin.py:
|
|
24
|
+
#: src/django_api_usage/admin.py:131 src/django_api_usage/admin.py:359
|
|
25
25
|
msgid "Application"
|
|
26
26
|
msgstr "Aplikazioa"
|
|
27
27
|
|
|
28
|
-
#: src/django_api_usage/admin.py:
|
|
28
|
+
#: src/django_api_usage/admin.py:153 src/django_api_usage/admin.py:295
|
|
29
|
+
#: src/django_api_usage/admin.py:367
|
|
29
30
|
msgid "Method"
|
|
30
31
|
msgstr "Metodoa"
|
|
31
32
|
|
|
32
|
-
#: src/django_api_usage/admin.py:
|
|
33
|
+
#: src/django_api_usage/admin.py:192 src/django_api_usage/admin.py:299
|
|
34
|
+
#: src/django_api_usage/admin.py:363
|
|
35
|
+
msgid "Site"
|
|
36
|
+
msgstr "Webgunea"
|
|
37
|
+
|
|
38
|
+
#: src/django_api_usage/admin.py:232 src/django_api_usage/models.py:100
|
|
33
39
|
msgid "Client application"
|
|
34
40
|
msgstr "Bezero-aplikazioa"
|
|
35
41
|
|
|
36
|
-
#: src/django_api_usage/admin.py:
|
|
42
|
+
#: src/django_api_usage/admin.py:248
|
|
37
43
|
msgid "Not attributed"
|
|
38
44
|
msgstr "Esleitu gabea"
|
|
39
45
|
|
|
40
|
-
#: src/django_api_usage/admin.py:
|
|
46
|
+
#: src/django_api_usage/admin.py:352
|
|
41
47
|
msgid ""
|
|
42
48
|
"Search by endpoint path, route name, app, method or replacement (for example "
|
|
43
49
|
"'herriak' or 'artikuluak')."
|
|
@@ -45,50 +51,60 @@ msgstr ""
|
|
|
45
51
|
"Bilatu endpointaren bidearen, route-izenaren, aplikazioaren, metodoaren edo "
|
|
46
52
|
"ordezkoaren arabera (adibidez 'herriak' edo 'artikuluak')."
|
|
47
53
|
|
|
48
|
-
#: src/django_api_usage/admin.py:
|
|
54
|
+
#: src/django_api_usage/admin.py:371
|
|
55
|
+
msgid "Endpoint"
|
|
56
|
+
msgstr "Endpointa"
|
|
57
|
+
|
|
58
|
+
#: src/django_api_usage/admin.py:427
|
|
49
59
|
#, python-brace-format
|
|
50
60
|
msgid "Flushed {count} buffered counter(s)."
|
|
51
61
|
msgstr "{count} kontagailu eraman dira bufferretik datu-basera."
|
|
52
62
|
|
|
53
|
-
#: src/django_api_usage/admin.py:
|
|
63
|
+
#: src/django_api_usage/admin.py:434
|
|
54
64
|
msgid ""
|
|
55
65
|
"Nothing was buffered: with BUFFER_BACKEND='db' counters are already in the "
|
|
56
66
|
"database."
|
|
57
67
|
msgstr ""
|
|
58
|
-
"Ez zegoen ezer bufferrean: BUFFER_BACKEND='db' denean kontagailuak jada "
|
|
59
|
-
"
|
|
68
|
+
"Ez zegoen ezer bufferrean: BUFFER_BACKEND='db' denean kontagailuak jada datu-"
|
|
69
|
+
"basean daude."
|
|
60
70
|
|
|
61
|
-
#: src/django_api_usage/admin.py:
|
|
71
|
+
#: src/django_api_usage/admin.py:446
|
|
62
72
|
#: src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html:12
|
|
63
73
|
#: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:8
|
|
64
74
|
msgid "Clear statistics"
|
|
65
75
|
msgstr "Estatistikak hustu"
|
|
66
76
|
|
|
67
|
-
#: src/django_api_usage/admin.py:
|
|
77
|
+
#: src/django_api_usage/admin.py:458
|
|
68
78
|
#, python-brace-format
|
|
69
79
|
msgid "Deleted {count} statistic row(s)."
|
|
70
80
|
msgstr "{count} estatistika-errenkada ezabatu dira."
|
|
71
81
|
|
|
72
|
-
#: src/django_api_usage/admin.py:
|
|
82
|
+
#: src/django_api_usage/admin.py:522
|
|
73
83
|
msgid "Matching rules"
|
|
74
84
|
msgstr "Bat-etortze arauak"
|
|
75
85
|
|
|
76
|
-
#: src/django_api_usage/admin.py:
|
|
86
|
+
#: src/django_api_usage/admin.py:526
|
|
77
87
|
msgid ""
|
|
78
88
|
"Checked in this order: account, request host, client network, user agent. "
|
|
79
|
-
"Between applications, the lowest priority wins."
|
|
89
|
+
"Between applications, the lowest priority wins. Accounts are assigned from "
|
|
90
|
+
"the 'Client application accounts' table."
|
|
80
91
|
msgstr ""
|
|
81
|
-
"Ordena honetan egiaztatzen da: kontua, eskaeraren ostalaria, bezeroaren
|
|
82
|
-
"eta user agent-a. Aplikazioen artean, lehentasun txikiena duenak
|
|
92
|
+
"Ordena honetan egiaztatzen da: kontua, eskaeraren ostalaria, bezeroaren "
|
|
93
|
+
"sarea eta user agent-a. Aplikazioen artean, lehentasun txikiena duenak "
|
|
94
|
+
"irabazten du. Kontuak 'Bezero-aplikazioko kontuak' taulatik esleitzen dira."
|
|
83
95
|
|
|
84
|
-
#: src/django_api_usage/admin.py:
|
|
96
|
+
#: src/django_api_usage/admin.py:550
|
|
85
97
|
msgid "Accounts"
|
|
86
98
|
msgstr "Kontuak"
|
|
87
99
|
|
|
88
|
-
#: src/django_api_usage/admin.py:
|
|
100
|
+
#: src/django_api_usage/admin.py:554
|
|
89
101
|
msgid "Counters"
|
|
90
102
|
msgstr "Kontagailuak"
|
|
91
103
|
|
|
104
|
+
#: src/django_api_usage/admin.py:582 src/django_api_usage/models.py:134
|
|
105
|
+
msgid "Account"
|
|
106
|
+
msgstr "Kontua"
|
|
107
|
+
|
|
92
108
|
#: src/django_api_usage/models.py:79
|
|
93
109
|
msgid "Short, stable label stored in the counters (e.g. 'mugikorra')."
|
|
94
110
|
msgstr ""
|
|
@@ -98,26 +114,36 @@ msgstr ""
|
|
|
98
114
|
msgid "Lower wins when several apps could match."
|
|
99
115
|
msgstr "Hainbat aplikaziok bat etorri ahalketenean, txikienak irabazten du."
|
|
100
116
|
|
|
101
|
-
#: src/django_api_usage/models.py:
|
|
102
|
-
msgid "Users (and therefore DRF tokens) that belong to this app."
|
|
103
|
-
msgstr "Aplikazio honi dagozkion erabiltzaileak (eta, beraz, haien DRF tokenak)."
|
|
104
|
-
|
|
105
|
-
#: src/django_api_usage/models.py:95
|
|
117
|
+
#: src/django_api_usage/models.py:89
|
|
106
118
|
msgid "One host per line; matched against the Origin/Referer host."
|
|
107
|
-
msgstr "
|
|
119
|
+
msgstr ""
|
|
120
|
+
"Ostalari bat lerroko; Origin/Referer ostalariaren aurka egiaztatzen da."
|
|
108
121
|
|
|
109
|
-
#: src/django_api_usage/models.py:
|
|
122
|
+
#: src/django_api_usage/models.py:92
|
|
110
123
|
msgid "One CIDR per line, e.g. 10.0.0.0/8."
|
|
111
124
|
msgstr "CIDR bat lerroko, adib. 10.0.0.0/8."
|
|
112
125
|
|
|
113
|
-
#: src/django_api_usage/models.py:
|
|
126
|
+
#: src/django_api_usage/models.py:96
|
|
114
127
|
msgid "One substring per line, matched case-insensitively."
|
|
115
|
-
msgstr "
|
|
128
|
+
msgstr ""
|
|
129
|
+
"Azpikate bat lerroko; maiuskulak/minuskulak bereizi gabe egiaztatzen da."
|
|
116
130
|
|
|
117
|
-
#: src/django_api_usage/models.py:
|
|
131
|
+
#: src/django_api_usage/models.py:101
|
|
118
132
|
msgid "Client applications"
|
|
119
133
|
msgstr "Bezero-aplikazioak"
|
|
120
134
|
|
|
135
|
+
#: src/django_api_usage/models.py:135
|
|
136
|
+
msgid "One application per account, so the token is unambiguous."
|
|
137
|
+
msgstr "Aplikazio bat kontu bakoitzeko, tokena zalantzarik gabea izan dadin."
|
|
138
|
+
|
|
139
|
+
#: src/django_api_usage/models.py:140
|
|
140
|
+
msgid "Client application account"
|
|
141
|
+
msgstr "Bezero-aplikazioko kontua"
|
|
142
|
+
|
|
143
|
+
#: src/django_api_usage/models.py:141
|
|
144
|
+
msgid "Client application accounts"
|
|
145
|
+
msgstr "Bezero-aplikazioko kontuak"
|
|
146
|
+
|
|
121
147
|
#: src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html:8
|
|
122
148
|
msgid "Flush now"
|
|
123
149
|
msgstr "Flush orain"
|
|
@@ -167,3 +193,7 @@ msgstr "Bai, estatistikak hustu"
|
|
|
167
193
|
#: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:25
|
|
168
194
|
msgid "Cancel"
|
|
169
195
|
msgstr ""
|
|
196
|
+
|
|
197
|
+
#~ msgid "Users (and therefore DRF tokens) that belong to this app."
|
|
198
|
+
#~ msgstr ""
|
|
199
|
+
#~ "Aplikazio honi dagozkion erabiltzaileak (eta, beraz, haien DRF tokenak)."
|