django-display-ids 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

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.
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from django_display_ids.conf import get_setting
8
+ from django_display_ids.encoding import PREFIX_PATTERN
8
9
  from django_display_ids.exceptions import (
9
10
  InvalidIdentifierError,
10
11
  LookupError,
@@ -21,6 +22,8 @@ __all__ = [
21
22
  "DisplayIDLookupMixin",
22
23
  ]
23
24
 
25
+ _NOT_SET: Any = object()
26
+
24
27
 
25
28
  def _get_drf_exceptions() -> tuple[type[Exception], type[Exception]]:
26
29
  """Lazily import DRF exceptions to avoid hard dependency."""
@@ -68,7 +71,7 @@ class DisplayIDLookupMixin:
68
71
 
69
72
  lookup_url_kwarg: str = "pk"
70
73
  lookup_strategies: tuple[StrategyName, ...] | None = None
71
- display_id_prefix: str | None = None
74
+ display_id_prefix: str | None = _NOT_SET
72
75
  uuid_field: str | None = None
73
76
  slug_field: str | None = None
74
77
 
@@ -91,6 +94,24 @@ class DisplayIDLookupMixin:
91
94
  return self.lookup_strategies
92
95
  return get_setting("STRATEGIES") # type: ignore[return-value]
93
96
 
97
+ def _get_display_id_prefix(self, model: type[models.Model]) -> str | None:
98
+ """Get the display ID prefix.
99
+
100
+ Returns the viewset's display_id_prefix if set (including None to
101
+ explicitly disable), otherwise falls back to the model's
102
+ display_id_prefix attribute.
103
+ """
104
+ if self.display_id_prefix is not _NOT_SET:
105
+ if self.display_id_prefix is not None and not PREFIX_PATTERN.match(
106
+ self.display_id_prefix
107
+ ):
108
+ raise ValueError(
109
+ f"display_id_prefix must be 1-16 lowercase letters, "
110
+ f"got: {self.display_id_prefix!r}"
111
+ )
112
+ return self.display_id_prefix
113
+ return getattr(model, "display_id_prefix", None)
114
+
94
115
  def get_queryset(self) -> Any:
95
116
  """Get the base queryset.
96
117
 
@@ -138,7 +159,7 @@ class DisplayIDLookupMixin:
138
159
  model=model,
139
160
  value=str(value),
140
161
  strategies=self._get_strategies(),
141
- prefix=self.display_id_prefix,
162
+ prefix=self._get_display_id_prefix(model),
142
163
  uuid_field=self._get_uuid_field(),
143
164
  slug_field=self._get_slug_field(),
144
165
  queryset=queryset,
@@ -7,7 +7,7 @@ from typing import ClassVar
7
7
  from django.db import models
8
8
 
9
9
  from .conf import get_setting
10
- from .encoding import encode_display_id
10
+ from .encoding import PREFIX_PATTERN, encode_display_id
11
11
 
12
12
  __all__ = [
13
13
  "DisplayIDMixin",
@@ -92,6 +92,11 @@ class DisplayIDMixin(models.Model):
92
92
  if "display_id_prefix" in cls.__dict__:
93
93
  prefix = cls.__dict__["display_id_prefix"]
94
94
  if prefix is not None:
95
+ if not PREFIX_PATTERN.match(prefix):
96
+ raise ValueError(
97
+ f"{cls.__name__}.display_id_prefix must be 1-16 "
98
+ f"lowercase letters, got: {prefix!r}"
99
+ )
95
100
  _register_prefix(prefix, cls.__name__)
96
101
 
97
102
  @classmethod
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
7
7
  from django.http import Http404
8
8
 
9
9
  from .conf import get_setting
10
+ from .encoding import PREFIX_PATTERN
10
11
  from .exceptions import (
11
12
  InvalidIdentifierError,
12
13
  LookupError,
@@ -23,6 +24,8 @@ __all__ = [
23
24
  "DisplayIDObjectMixin",
24
25
  ]
25
26
 
27
+ _NOT_SET: Any = object()
28
+
26
29
 
27
30
  class DisplayIDObjectMixin:
28
31
  """Mixin for Django CBVs that resolves objects by display ID, UUID, or slug.
@@ -49,7 +52,7 @@ class DisplayIDObjectMixin:
49
52
  model: type[models.Model] | None = None
50
53
  lookup_param: str = "pk"
51
54
  lookup_strategies: tuple[StrategyName, ...] | None = None
52
- display_id_prefix: str | None = None
55
+ display_id_prefix: str | None = _NOT_SET
53
56
  uuid_field: str | None = None
54
57
  slug_field: str | None = None
55
58
 
@@ -68,6 +71,26 @@ class DisplayIDObjectMixin:
68
71
  return self.lookup_strategies
69
72
  return get_setting("STRATEGIES") # type: ignore[return-value]
70
73
 
74
+ def _get_display_id_prefix(self) -> str | None:
75
+ """Get the display ID prefix.
76
+
77
+ Returns the view's display_id_prefix if set (including None to
78
+ explicitly disable), otherwise falls back to the model's
79
+ display_id_prefix attribute.
80
+ """
81
+ if self.display_id_prefix is not _NOT_SET:
82
+ if self.display_id_prefix is not None and not PREFIX_PATTERN.match(
83
+ self.display_id_prefix
84
+ ):
85
+ raise ValueError(
86
+ f"display_id_prefix must be 1-16 lowercase letters, "
87
+ f"got: {self.display_id_prefix!r}"
88
+ )
89
+ return self.display_id_prefix
90
+ if self.model is not None:
91
+ return getattr(self.model, "display_id_prefix", None)
92
+ return None
93
+
71
94
  # These may be provided by parent classes
72
95
  kwargs: dict[str, Any]
73
96
 
@@ -114,7 +137,7 @@ class DisplayIDObjectMixin:
114
137
  model=self.model,
115
138
  value=str(value),
116
139
  strategies=self._get_strategies(),
117
- prefix=self.display_id_prefix,
140
+ prefix=self._get_display_id_prefix(),
118
141
  uuid_field=self._get_uuid_field(),
119
142
  slug_field=self._get_slug_field(),
120
143
  queryset=qs,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-display-ids
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
5
5
  Keywords: django,stripe,uuid,base62,prefixed-id,drf,shortuuid,nanoid,ulid
6
6
  License: ISC
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Dist: django>=4.2
19
19
  Requires-Python: >=3.12
20
- Project-URL: Source, https://joseph.is/django-display-ids
20
+ Project-URL: Homepage, https://joseph.is/django-display-ids
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
  # django-display-ids
@@ -34,6 +34,8 @@ This library focuses on **lookup only** — it works with your existing UUID fie
34
34
  pip install django-display-ids
35
35
  ```
36
36
 
37
+ No `INSTALLED_APPS` entry required — just import and use.
38
+
37
39
  ## Quick Start
38
40
 
39
41
  ```python
@@ -249,11 +251,13 @@ The `display_id` strategy requires a prefix. If no prefix is configured, the str
249
251
  |-----------|---------|-------------|
250
252
  | `lookup_param` / `lookup_url_kwarg` | `"pk"` | URL parameter name |
251
253
  | `lookup_strategies` | from settings | Strategies to try |
252
- | `display_id_prefix` | `None` | Expected prefix |
254
+ | `display_id_prefix` | from model | Expected prefix (falls back to model's `display_id_prefix`) |
253
255
  | `uuid_field` | `"id"` | UUID field name on model |
254
256
  | `slug_field` | `"slug"` | Slug field name on model |
255
257
 
256
- ### Django Settings
258
+ ### Django Settings (Optional)
259
+
260
+ All settings have sensible defaults. Only add this if you need to override them:
257
261
 
258
262
  ```python
259
263
  # settings.py
@@ -298,10 +302,16 @@ Run tests:
298
302
  uv run pytest
299
303
  ```
300
304
 
305
+ Run tests with coverage:
306
+
307
+ ```bash
308
+ uv run pytest --cov=src/django_display_ids
309
+ ```
310
+
301
311
  Run tests across Python and Django versions:
302
312
 
303
313
  ```bash
304
- uvx nox -p
314
+ uvx nox
305
315
  ```
306
316
 
307
317
  Lint and format:
@@ -3,16 +3,16 @@ django_display_ids/admin.py,sha256=uRyPH3q5e9D5oMxs6PtCHq0syBXs--i1alRoe-EcIJo,2
3
3
  django_display_ids/conf.py,sha256=qTsCzKeNBdJpEVeEkx2kFeWHBFa_NZwV_tpt-UTyRR0,1132
4
4
  django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
5
5
  django_display_ids/contrib/rest_framework/__init__.py,sha256=hBk-6m01T66J5bar3GKwApktOnHhDtK-gUpjghlKoJo,148
6
- django_display_ids/contrib/rest_framework/views.py,sha256=SuUDs4FIyEf8MI2-gTPly8RgL_OshvvRLHGUKXWFexc,5187
6
+ django_display_ids/contrib/rest_framework/views.py,sha256=mu-twvdRbJedzSfLWVNn2BazFpAQzwRB5Eq3w2bxvvs,6056
7
7
  django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
8
8
  django_display_ids/exceptions.py,sha256=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
9
9
  django_display_ids/managers.py,sha256=EFvlQxsSFXeM8TVvV4NZKeKMC7QB3C0zYGgZ6bvSr4k,6884
10
- django_display_ids/models.py,sha256=bTuD6kMkuoMDH8P9wTgIvTktEnw1VAEFq-NuiTkP_K0,3891
10
+ django_display_ids/models.py,sha256=_IXxaFlVw2MqobQUw4Cy4rB66LsTz6kiI5YpoSpnkCY,4156
11
11
  django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  django_display_ids/resolver.py,sha256=oCoA6jbGCFS8SMrkfD_oSSBQNrSxnxdooK5j933eA9Y,2494
13
13
  django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
14
14
  django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
15
- django_display_ids/views.py,sha256=mqkWPW8wIuYMeUd76n3ZA9oS4Fep7azmjHaOnu9s48U,4216
16
- django_display_ids-0.1.2.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
17
- django_display_ids-0.1.2.dist-info/METADATA,sha256=foJtkEHg4lJ_hTDLOenjgKMfn4kHbHPITXQOGNic_RM,9366
18
- django_display_ids-0.1.2.dist-info/RECORD,,
15
+ django_display_ids/views.py,sha256=sLsJm8Tpe3Qk1gOLcDzfpazxuaVqTCAdgVIXOONFnKQ,5096
16
+ django_display_ids-0.1.3.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
17
+ django_display_ids-0.1.3.dist-info/METADATA,sha256=AjMU5ctNag1QVDINfx4gggW0VWfYkkIEE1brlokAUqo,9649
18
+ django_display_ids-0.1.3.dist-info/RECORD,,