django-api-usage 0.1.5__tar.gz → 0.1.7__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_usage-0.1.5/src/django_api_usage.egg-info → django_api_usage-0.1.7}/PKG-INFO +29 -3
  2. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/README.md +28 -2
  3. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/pyproject.toml +6 -2
  4. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/__init__.py +1 -1
  5. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/admin.py +195 -5
  6. django_api_usage-0.1.7/src/django_api_usage/apps.py +34 -0
  7. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/buffers.py +2 -0
  8. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/conf.py +1 -0
  9. django_api_usage-0.1.7/src/django_api_usage/locale/eu/LC_MESSAGES/django.mo +0 -0
  10. django_api_usage-0.1.7/src/django_api_usage/locale/eu/LC_MESSAGES/django.po +169 -0
  11. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/middleware.py +1 -0
  12. django_api_usage-0.1.7/src/django_api_usage/migrations/0003_alter_endpointstat_unique_together_and_more.py +96 -0
  13. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/models.py +80 -4
  14. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/resolvers.py +116 -0
  15. {django_api_usage-0.1.5 → django_api_usage-0.1.7/src/django_api_usage.egg-info}/PKG-INFO +29 -3
  16. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage.egg-info/SOURCES.txt +5 -0
  17. django_api_usage-0.1.7/tests/test_client_app.py +159 -0
  18. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/tests/test_csv_export.py +5 -3
  19. django_api_usage-0.1.7/tests/test_filters.py +124 -0
  20. django_api_usage-0.1.5/src/django_api_usage/apps.py +0 -11
  21. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/LICENSE +0 -0
  22. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/setup.cfg +0 -0
  23. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/checks.py +0 -0
  24. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/deprecation.py +0 -0
  25. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/drf.py +0 -0
  26. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/maintenance.py +0 -0
  27. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/management/__init__.py +0 -0
  28. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/management/commands/__init__.py +0 -0
  29. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/management/commands/api_usage_flush.py +0 -0
  30. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/management/commands/api_usage_report.py +0 -0
  31. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/migrations/0001_initial.py +0 -0
  32. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/migrations/0002_endpoint_route_path.py +0 -0
  33. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/migrations/__init__.py +0 -0
  34. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/tasks.py +0 -0
  35. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html +0 -0
  36. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html +0 -0
  37. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage.egg-info/dependency_links.txt +0 -0
  38. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage.egg-info/requires.txt +0 -0
  39. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/src/django_api_usage.egg-info/top_level.txt +0 -0
  40. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/tests/test_admin.py +0 -0
  41. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/tests/test_checks.py +0 -0
  42. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/tests/test_commands.py +0 -0
  43. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/tests/test_drf.py +0 -0
  44. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/tests/test_middleware.py +0 -0
  45. {django_api_usage-0.1.5 → django_api_usage-0.1.7}/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.5
3
+ Version: 0.1.7
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
@@ -127,8 +127,12 @@ Django — **no dependency beyond Django itself** (no `django-object-actions` or
127
127
  similar):
128
128
 
129
129
  * **Endpoint stats** changelist: a search box (by endpoint path, route name, app,
130
- method or replacement), filters by date, profile and role, and the **sum of
131
- the `count` column** for the rows matching the current filters.
130
+ method or replacement), filters by date, client type, status class, and
131
+ **Application** / **Method**, plus the **sum of the `count` column** for the
132
+ rows matching the current filters. The Method filter lists the riskiest verbs
133
+ first (`DELETE`, `POST`, ...), so write and destructive calls stand out.
134
+ * **Endpoint** changelist: the same Application / Method filters, plus
135
+ deprecation state, the site and the CSV export.
132
136
  * **Flush now**: moves counters buffered in the cache into the database, so they
133
137
  appear immediately when you run with `BUFFER_BACKEND = "cache"`.
134
138
  * **Clear statistics**: empties the aggregated counters behind a confirmation
@@ -138,6 +142,28 @@ similar):
138
142
  and method), so the file can be fed straight to pandas or a spreadsheet. Use
139
143
  the "select all" link to export every row matching the current filters.
140
144
 
145
+ ## Client applications
146
+
147
+ 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**, so a
149
+ new consumer is recognised from the admin, without a deploy:
150
+
151
+ | Rule | Matched against | Use it for |
152
+ |---|---|---|
153
+ | `accounts` | the authenticated user (and therefore its DRF token) | one dedicated token per integration (ERP, partner...) |
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
+ Rules are checked in that order, and `priority` decides between applications
159
+ (lower wins). A caller matching nothing stays unattributed, so the dimension
160
+ cannot grow out of control: `EndpointStat.client_app` only ever holds a
161
+ `ClientApp.slug`.
162
+
163
+ Counters, the CSV export and the admin filters all carry the application, so
164
+ "which application calls this endpoint?" is one filter away. The **Endpoint
165
+ stats** changelist also offers *Not attributed*: the callers still to classify.
166
+
141
167
  ## Deprecation lifecycle
142
168
 
143
169
  `Endpoint` carries `deprecated`, `sunset_date`, `replacement` and `owner`, so the
@@ -88,8 +88,12 @@ Django — **no dependency beyond Django itself** (no `django-object-actions` or
88
88
  similar):
89
89
 
90
90
  * **Endpoint stats** changelist: a search box (by endpoint path, route name, app,
91
- method or replacement), filters by date, profile and role, and the **sum of
92
- the `count` column** for the rows matching the current filters.
91
+ method or replacement), filters by date, client type, status class, and
92
+ **Application** / **Method**, plus the **sum of the `count` column** for the
93
+ rows matching the current filters. The Method filter lists the riskiest verbs
94
+ first (`DELETE`, `POST`, ...), so write and destructive calls stand out.
95
+ * **Endpoint** changelist: the same Application / Method filters, plus
96
+ deprecation state, the site and the CSV export.
93
97
  * **Flush now**: moves counters buffered in the cache into the database, so they
94
98
  appear immediately when you run with `BUFFER_BACKEND = "cache"`.
95
99
  * **Clear statistics**: empties the aggregated counters behind a confirmation
@@ -99,6 +103,28 @@ similar):
99
103
  and method), so the file can be fed straight to pandas or a spreadsheet. Use
100
104
  the "select all" link to export every row matching the current filters.
101
105
 
106
+ ## Client applications
107
+
108
+ 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**, so a
110
+ new consumer is recognised from the admin, without a deploy:
111
+
112
+ | Rule | Matched against | Use it for |
113
+ |---|---|---|
114
+ | `accounts` | the authenticated user (and therefore its DRF token) | one dedicated token per integration (ERP, partner...) |
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
+ Rules are checked in that order, and `priority` decides between applications
120
+ (lower wins). A caller matching nothing stays unattributed, so the dimension
121
+ cannot grow out of control: `EndpointStat.client_app` only ever holds a
122
+ `ClientApp.slug`.
123
+
124
+ Counters, the CSV export and the admin filters all carry the application, so
125
+ "which application calls this endpoint?" is one filter away. The **Endpoint
126
+ stats** changelist also offers *Not attributed*: the callers still to classify.
127
+
102
128
  ## Deprecation lifecycle
103
129
 
104
130
  `Endpoint` carries `deprecated`, `sunset_date`, `replacement` and `owner`, so the
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-api-usage"
7
- version = "0.1.5"
7
+ version = "0.1.7"
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"
@@ -42,7 +42,11 @@ Issues = "https://github.com/codesyntax/django-api-usage/issues"
42
42
  where = ["src"]
43
43
 
44
44
  [tool.setuptools.package-data]
45
- django_api_usage = ["templates/admin/django_api_usage/endpointstat/*.html"]
45
+ django_api_usage = [
46
+ "templates/admin/django_api_usage/endpointstat/*.html",
47
+ "locale/*/LC_MESSAGES/*.po",
48
+ "locale/*/LC_MESSAGES/*.mo",
49
+ ]
46
50
 
47
51
  [tool.black]
48
52
  line-length = 88
@@ -1,3 +1,3 @@
1
1
  """django-api-usage: usage metering for Django APIs, with deprecation support."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.7"
@@ -16,7 +16,7 @@ Everything is plain Django: no extra dependency beyond Django itself.
16
16
  import csv
17
17
 
18
18
  from django.contrib import admin, messages
19
- from django.db.models import Sum
19
+ from django.db.models import Count, OuterRef, Subquery, Sum
20
20
  from django.http import HttpResponse, HttpResponseRedirect
21
21
  from django.template.response import TemplateResponse
22
22
  from django.urls import path, reverse
@@ -25,7 +25,7 @@ from django.utils.text import format_lazy
25
25
  from django.utils.translation import gettext_lazy as _
26
26
 
27
27
  from .buffers import flush
28
- from .models import Consumer, Endpoint, EndpointStat
28
+ from .models import ClientApp, Consumer, Endpoint, EndpointStat
29
29
 
30
30
 
31
31
  class CsvExportMixin:
@@ -60,6 +60,107 @@ class CsvExportMixin:
60
60
  return response
61
61
 
62
62
 
63
+ #: HTTP methods, riskiest first: the point of the filter is spotting the
64
+ #: write/destructive calls (POST, DELETE, ...) instead of the read ones.
65
+ METHOD_ORDER = ("DELETE", "POST", "PUT", "PATCH", "GET", "HEAD", "OPTIONS")
66
+
67
+
68
+ class AppLabelListFilter(admin.SimpleListFilter):
69
+ """Distinct application labels, under a readable title."""
70
+
71
+ title = _("Application")
72
+ parameter_name = "app_label"
73
+ #: Lookup against the admin's model; overridden for related filters.
74
+ field_path = "app_label"
75
+
76
+ def lookups(self, request, model_admin):
77
+ values = (
78
+ model_admin.model.objects.order_by()
79
+ .values_list(self.field_path, flat=True)
80
+ .distinct()
81
+ )
82
+ return [(value, value) for value in sorted(v for v in values if v)]
83
+
84
+ def queryset(self, request, queryset):
85
+ if not self.value():
86
+ return queryset
87
+ return queryset.filter(**{self.field_path: self.value()})
88
+
89
+
90
+ class MethodListFilter(admin.SimpleListFilter):
91
+ """HTTP methods, riskiest (DELETE, POST, ...) first."""
92
+
93
+ title = _("Method")
94
+ parameter_name = "method"
95
+ field_path = "method"
96
+
97
+ def lookups(self, request, model_admin):
98
+ values = set(
99
+ model_admin.model.objects.order_by()
100
+ .values_list(self.field_path, flat=True)
101
+ .distinct()
102
+ )
103
+ ordered = [method for method in METHOD_ORDER if method in values]
104
+ rest = sorted(m for m in values if m and m not in METHOD_ORDER)
105
+ return [(method, method) for method in ordered + rest]
106
+
107
+ def queryset(self, request, queryset):
108
+ if not self.value():
109
+ return queryset
110
+ return queryset.filter(**{self.field_path: self.value()})
111
+
112
+
113
+ class StatAppLabelListFilter(AppLabelListFilter):
114
+ """Application filter for the stats changelist (through the endpoint)."""
115
+
116
+ field_path = "endpoint__app_label"
117
+
118
+
119
+ class StatMethodListFilter(MethodListFilter):
120
+ """Method filter for the stats changelist (through the endpoint)."""
121
+
122
+ field_path = "endpoint__method"
123
+
124
+
125
+ #: Sentinel for the calls that could not be attributed to any application.
126
+ UNATTRIBUTED = "__none__"
127
+
128
+
129
+ class ClientAppListFilter(admin.SimpleListFilter):
130
+ """Client applications, plus the calls that were not attributed.
131
+
132
+ Applications defined in the table are listed even before they have traffic,
133
+ which is what you want while checking that a new rule works.
134
+ """
135
+
136
+ title = _("Client application")
137
+ parameter_name = "client_app"
138
+
139
+ def lookups(self, request, model_admin):
140
+ used = set(
141
+ model_admin.model.objects.order_by()
142
+ .values_list("client_app", flat=True)
143
+ .distinct()
144
+ )
145
+ names = dict(ClientApp.objects.values_list("slug", "name"))
146
+ defined = set(
147
+ ClientApp.objects.filter(is_active=True).values_list("slug", flat=True)
148
+ )
149
+ choices = [
150
+ (slug, names.get(slug) or slug) for slug in sorted((used | defined) - {""})
151
+ ]
152
+ choices.append((UNATTRIBUTED, _("Not attributed")))
153
+ return choices
154
+
155
+ def queryset(self, request, queryset):
156
+ value = self.value()
157
+ if value == UNATTRIBUTED:
158
+ return queryset.filter(client_app="")
159
+ if value:
160
+ return queryset.filter(client_app=value)
161
+ return queryset
162
+
163
+
63
164
  @admin.register(Endpoint)
64
165
  class EndpointAdmin(CsvExportMixin, admin.ModelAdmin):
65
166
  actions = ("export_as_csv",)
@@ -86,7 +187,12 @@ class EndpointAdmin(CsvExportMixin, admin.ModelAdmin):
86
187
  "replacement",
87
188
  "owner",
88
189
  )
89
- list_filter = ("deprecated", "app_label", "method", "site_id")
190
+ list_filter = (
191
+ "deprecated",
192
+ AppLabelListFilter,
193
+ MethodListFilter,
194
+ "site_id",
195
+ )
90
196
  search_fields = ("route_path", "route_name", "replacement", "owner", "notes")
91
197
  list_editable = ("deprecated", "sunset_date", "replacement", "owner")
92
198
 
@@ -104,11 +210,26 @@ class EndpointStatAdmin(CsvExportMixin, admin.ModelAdmin):
104
210
  ("method", "endpoint__method"),
105
211
  ("site_id", "endpoint__site_id"),
106
212
  ("client_type", "client_type"),
213
+ ("client_app", "client_app"),
107
214
  ("status_class", "status_class"),
108
215
  ("count", "count"),
109
216
  )
110
- list_display = ("date", "endpoint", "client_type", "status_class", "count")
111
- list_filter = ("date", "client_type", "status_class")
217
+ list_display = (
218
+ "date",
219
+ "endpoint",
220
+ "client_type",
221
+ "client_app",
222
+ "status_class",
223
+ "count",
224
+ )
225
+ list_filter = (
226
+ "date",
227
+ "client_type",
228
+ ClientAppListFilter,
229
+ "status_class",
230
+ StatAppLabelListFilter,
231
+ StatMethodListFilter,
232
+ )
112
233
  list_select_related = ("endpoint",)
113
234
  date_hierarchy = "date"
114
235
  # Search across the related endpoint (case-insensitive "contains").
@@ -241,3 +362,72 @@ class ConsumerAdmin(CsvExportMixin, admin.ModelAdmin):
241
362
  @admin.display(description="Reference")
242
363
  def ref_short(self, obj):
243
364
  return obj.ref_hash[:12]
365
+
366
+
367
+ @admin.register(ClientApp)
368
+ class ClientAppAdmin(CsvExportMixin, admin.ModelAdmin):
369
+ """Editable table: recognise a new consumer without deploying anything."""
370
+
371
+ actions = ("export_as_csv",)
372
+ csv_columns = (
373
+ ("slug", "slug"),
374
+ ("name", "name"),
375
+ ("priority", "priority"),
376
+ ("is_active", "is_active"),
377
+ ("domains", "domains"),
378
+ ("ip_networks", "ip_networks"),
379
+ ("user_agent_patterns", "user_agent_patterns"),
380
+ )
381
+ list_display = (
382
+ "slug",
383
+ "name",
384
+ "priority",
385
+ "is_active",
386
+ "accounts_total",
387
+ "counters_total",
388
+ )
389
+ list_filter = ("is_active",)
390
+ search_fields = ("slug", "name", "description")
391
+ filter_horizontal = ("accounts",)
392
+ fieldsets = (
393
+ (None, {"fields": ("slug", "name", "description", "priority", "is_active")}),
394
+ (
395
+ _("Matching rules"),
396
+ {
397
+ "fields": (
398
+ "accounts",
399
+ "domains",
400
+ "ip_networks",
401
+ "user_agent_patterns",
402
+ ),
403
+ "description": _(
404
+ "Checked in this order: account, request host, client "
405
+ "network, user agent. Between applications, the lowest "
406
+ "priority wins."
407
+ ),
408
+ },
409
+ ),
410
+ )
411
+
412
+ def get_queryset(self, request):
413
+ counters = (
414
+ EndpointStat.objects.filter(client_app=OuterRef("slug"))
415
+ .order_by()
416
+ .values("client_app")
417
+ .annotate(total=Sum("count"))
418
+ .values("total")
419
+ )
420
+ return (
421
+ super()
422
+ .get_queryset(request)
423
+ .annotate(accounts_total=Count("accounts", distinct=True))
424
+ .annotate(counters_total=Subquery(counters))
425
+ )
426
+
427
+ @admin.display(description=_("Accounts"), ordering="accounts_total")
428
+ def accounts_total(self, obj):
429
+ return obj.accounts_total
430
+
431
+ @admin.display(description=_("Counters"), ordering="counters_total")
432
+ def counters_total(self, obj):
433
+ return obj.counters_total or 0
@@ -0,0 +1,34 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoApiUsageConfig(AppConfig):
5
+ name = "django_api_usage"
6
+ verbose_name = "Django API usage"
7
+ default_auto_field = "django.db.models.BigAutoField"
8
+
9
+ def ready(self):
10
+ # Register system checks (middleware installed, consumer salt, ...).
11
+ from . import checks # noqa: F401
12
+
13
+ self._connect_client_app_signals()
14
+
15
+ def _connect_client_app_signals(self):
16
+ """Drop the cached ClientApp rules as soon as the table changes."""
17
+ from django.db.models.signals import m2m_changed, post_delete, post_save
18
+
19
+ from .models import ClientApp
20
+ from .resolvers import reset_client_app_cache
21
+
22
+ post_save.connect(
23
+ reset_client_app_cache, sender=ClientApp, dispatch_uid="api_usage_app_save"
24
+ )
25
+ post_delete.connect(
26
+ reset_client_app_cache,
27
+ sender=ClientApp,
28
+ dispatch_uid="api_usage_app_delete",
29
+ )
30
+ m2m_changed.connect(
31
+ reset_client_app_cache,
32
+ sender=ClientApp.accounts.through,
33
+ dispatch_uid="api_usage_app_accounts",
34
+ )
@@ -30,6 +30,7 @@ _FIELDS = (
30
30
  "route_path",
31
31
  "method",
32
32
  "client_type",
33
+ "client_app",
33
34
  )
34
35
 
35
36
 
@@ -126,6 +127,7 @@ def _write_to_db(dimensions, status_code_class, count):
126
127
  endpoint=endpoint,
127
128
  date=timezone.localdate(),
128
129
  client_type=dimensions.get("client_type") or "anon",
130
+ client_app=dimensions.get("client_app") or "",
129
131
  status_class=status_code_class,
130
132
  defaults={"count": count},
131
133
  )
@@ -33,6 +33,7 @@ DEFAULTS = {
33
33
  "APP_LABEL_RESOLVER": "django_api_usage.resolvers.default_app_label",
34
34
  "ROUTE_NAME_RESOLVER": "django_api_usage.resolvers.default_route_name",
35
35
  "ROUTE_PATH_RESOLVER": "django_api_usage.resolvers.default_route_path",
36
+ "CLIENT_APP_RESOLVER": "django_api_usage.resolvers.client_app_from_rules",
36
37
  "CLIENT_TYPE_RESOLVER": "django_api_usage.resolvers.default_client_type",
37
38
  "SITE_RESOLVER": "django_api_usage.resolvers.default_site_id",
38
39
  "CONSUMER_RESOLVER": "django_api_usage.resolvers.default_consumer",
@@ -0,0 +1,169 @@
1
+ # Basque translation of django-api-usage.
2
+ # Copyright (C) 2026 CodeSyntax
3
+ # This file is distributed under the same license as the django-api-usage package.
4
+ # Urtzi Odriozola <uodriozola@codesyntax.com>, 2026.
5
+ #
6
+ msgid ""
7
+ msgstr ""
8
+ "Project-Id-Version: django-api-usage\n"
9
+ "Report-Msgid-Bugs-To: \n"
10
+ "POT-Creation-Date: 2026-10-08 06:45-0500\n"
11
+ "PO-Revision-Date: 2026-10-08 06:55-0500\n"
12
+ "Last-Translator: Urtzi Odriozola <uodriozola@codesyntax.com>\n"
13
+ "Language-Team: Basque\n"
14
+ "Language: eu\n"
15
+ "MIME-Version: 1.0\n"
16
+ "Content-Type: text/plain; charset=UTF-8\n"
17
+ "Content-Transfer-Encoding: 8bit\n"
18
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19
+
20
+ #: src/django_api_usage/admin.py:46
21
+ msgid "Export selected rows to CSV"
22
+ msgstr "Esportatu hautatutako errenkadak CSVra"
23
+
24
+ #: src/django_api_usage/admin.py:71
25
+ msgid "Application"
26
+ msgstr "Aplikazioa"
27
+
28
+ #: src/django_api_usage/admin.py:93
29
+ msgid "Method"
30
+ msgstr "Metodoa"
31
+
32
+ #: src/django_api_usage/admin.py:136 src/django_api_usage/models.py:106
33
+ msgid "Client application"
34
+ msgstr "Bezero-aplikazioa"
35
+
36
+ #: src/django_api_usage/admin.py:152
37
+ msgid "Not attributed"
38
+ msgstr "Esleitu gabea"
39
+
40
+ #: src/django_api_usage/admin.py:244
41
+ msgid ""
42
+ "Search by endpoint path, route name, app, method or replacement (for example "
43
+ "'herriak' or 'artikuluak')."
44
+ msgstr ""
45
+ "Bilatu endpointaren bidearen, route-izenaren, aplikazioaren, metodoaren edo "
46
+ "ordezkoaren arabera (adibidez 'herriak' edo 'artikuluak')."
47
+
48
+ #: src/django_api_usage/admin.py:299
49
+ #, python-brace-format
50
+ msgid "Flushed {count} buffered counter(s)."
51
+ msgstr "{count} kontagailu eraman dira bufferretik datu-basera."
52
+
53
+ #: src/django_api_usage/admin.py:306
54
+ msgid ""
55
+ "Nothing was buffered: with BUFFER_BACKEND='db' counters are already in the "
56
+ "database."
57
+ msgstr ""
58
+ "Ez zegoen ezer bufferrean: BUFFER_BACKEND='db' denean kontagailuak jada "
59
+ "datu-basean daude."
60
+
61
+ #: src/django_api_usage/admin.py:318
62
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html:12
63
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:8
64
+ msgid "Clear statistics"
65
+ msgstr "Estatistikak hustu"
66
+
67
+ #: src/django_api_usage/admin.py:330
68
+ #, python-brace-format
69
+ msgid "Deleted {count} statistic row(s)."
70
+ msgstr "{count} estatistika-errenkada ezabatu dira."
71
+
72
+ #: src/django_api_usage/admin.py:395
73
+ msgid "Matching rules"
74
+ msgstr "Bat-etortze arauak"
75
+
76
+ #: src/django_api_usage/admin.py:404
77
+ msgid ""
78
+ "Checked in this order: account, request host, client network, user agent. "
79
+ "Between applications, the lowest priority wins."
80
+ msgstr ""
81
+ "Ordena honetan egiaztatzen da: kontua, eskaeraren ostalaria, bezeroaren sarea "
82
+ "eta user agent-a. Aplikazioen artean, lehentasun txikiena duenak irabazten du."
83
+
84
+ #: src/django_api_usage/admin.py:427
85
+ msgid "Accounts"
86
+ msgstr "Kontuak"
87
+
88
+ #: src/django_api_usage/admin.py:431
89
+ msgid "Counters"
90
+ msgstr "Kontagailuak"
91
+
92
+ #: src/django_api_usage/models.py:79
93
+ msgid "Short, stable label stored in the counters (e.g. 'mugikorra')."
94
+ msgstr ""
95
+ "Kontagailuetan gordetzen den etiketa labur eta egonkorra (adib. 'mugikorra')."
96
+
97
+ #: src/django_api_usage/models.py:85
98
+ msgid "Lower wins when several apps could match."
99
+ msgstr "Hainbat aplikaziok bat etorri ahalketenean, txikienak irabazten du."
100
+
101
+ #: src/django_api_usage/models.py:91
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
106
+ msgid "One host per line; matched against the Origin/Referer host."
107
+ msgstr "Ostalari bat lerroko; Origin/Referer ostalariaren aurka egiaztatzen da."
108
+
109
+ #: src/django_api_usage/models.py:98
110
+ msgid "One CIDR per line, e.g. 10.0.0.0/8."
111
+ msgstr "CIDR bat lerroko, adib. 10.0.0.0/8."
112
+
113
+ #: src/django_api_usage/models.py:102
114
+ msgid "One substring per line, matched case-insensitively."
115
+ msgstr "Azpikate bat lerroko; maiuskulak/minuskulak bereizi gabe egiaztatzen da."
116
+
117
+ #: src/django_api_usage/models.py:107
118
+ msgid "Client applications"
119
+ msgstr "Bezero-aplikazioak"
120
+
121
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html:8
122
+ msgid "Flush now"
123
+ msgstr "Flush orain"
124
+
125
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/change_list.html:21
126
+ #, python-format
127
+ msgid "TOTAL COUNT: %(total)s"
128
+ msgstr "GUZTIRA: %(total)s"
129
+
130
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:6
131
+ msgid "Home"
132
+ msgstr ""
133
+
134
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:7
135
+ msgid "Endpoint stats"
136
+ msgstr "Endpoint estatistikak"
137
+
138
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:14
139
+ #, python-format
140
+ msgid ""
141
+ "\n"
142
+ " This will delete the only aggregated statistic row. Endpoints and "
143
+ "their\n"
144
+ " deprecation metadata are kept.\n"
145
+ " "
146
+ msgid_plural ""
147
+ "\n"
148
+ " This will delete all %(counter)s aggregated statistic rows. Endpoints\n"
149
+ " and their deprecation metadata are kept.\n"
150
+ " "
151
+ msgstr[0] ""
152
+ "\n"
153
+ " Estatistika-errenkada agregatu bakarra ezabatuko da. Endpointak eta "
154
+ "haien\n"
155
+ " deprecation-metadatuak mantendu egingo dira.\n"
156
+ " "
157
+ msgstr[1] ""
158
+ "\n"
159
+ " %(counter)s estatistika-errenkada agregatu ezabatuko dira. Endpointak\n"
160
+ " eta haien deprecation-metadatuak mantendu egingo dira.\n"
161
+ " "
162
+
163
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:24
164
+ msgid "Yes, clear statistics"
165
+ msgstr "Bai, estatistikak hustu"
166
+
167
+ #: src/django_api_usage/templates/admin/django_api_usage/endpointstat/confirm_clear.html:25
168
+ msgid "Cancel"
169
+ msgstr ""
@@ -36,6 +36,7 @@ class ApiUsageMiddleware:
36
36
  "route_path": api_settings.ROUTE_PATH_RESOLVER(request),
37
37
  "method": request.method,
38
38
  "client_type": api_settings.CLIENT_TYPE_RESOLVER(request),
39
+ "client_app": api_settings.CLIENT_APP_RESOLVER(request),
39
40
  }
40
41
  consumer = None
41
42
  if api_settings.TRACK_CONSUMERS: