django-display-ids 0.3.0__py3-none-any.whl → 0.3.2__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.
@@ -26,7 +26,14 @@ Example:
26
26
  from typing import Any
27
27
 
28
28
  from .admin import DisplayIDSearchMixin
29
- from .converters import DisplayIDConverter, DisplayIDOrUUIDConverter, UUIDConverter
29
+ from .converters import (
30
+ DisplayIDConverter,
31
+ DisplayIDOrSlugConverter,
32
+ DisplayIDOrUUIDConverter,
33
+ DisplayIDOrUUIDOrSlugConverter,
34
+ make_display_id_or_slug_converter,
35
+ make_display_id_or_uuid_or_slug_converter,
36
+ )
30
37
  from .encoding import (
31
38
  decode_display_id,
32
39
  decode_uuid,
@@ -69,8 +76,11 @@ def __getattr__(name: str) -> Any:
69
76
  __all__ = [ # noqa: RUF022 - keep categorized order for readability
70
77
  # URL converters
71
78
  "DisplayIDConverter",
72
- "UUIDConverter",
79
+ "DisplayIDOrSlugConverter",
73
80
  "DisplayIDOrUUIDConverter",
81
+ "DisplayIDOrUUIDOrSlugConverter",
82
+ "make_display_id_or_slug_converter",
83
+ "make_display_id_or_uuid_or_slug_converter",
74
84
  # Encoding
75
85
  "encode_uuid",
76
86
  "decode_uuid",
@@ -14,19 +14,25 @@ from __future__ import annotations
14
14
  from typing import TYPE_CHECKING
15
15
 
16
16
  from django.conf import settings
17
+ from django.urls.converters import SlugConverter
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from .typing import StrategyName
20
21
 
21
22
  __all__ = [
22
23
  "DEFAULTS",
24
+ "SLUG_REGEX",
23
25
  "get_setting",
24
26
  ]
25
27
 
28
+ # Django's default slug regex pattern
29
+ SLUG_REGEX: str = SlugConverter.regex
30
+
26
31
  DEFAULTS: dict[str, str | tuple[str, ...]] = {
27
32
  "UUID_FIELD": "id",
28
33
  "SLUG_FIELD": "slug",
29
34
  "STRATEGIES": ("display_id", "uuid"),
35
+ "SLUG_REGEX": SLUG_REGEX,
30
36
  }
31
37
 
32
38
 
@@ -2,14 +2,37 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from .conf import SLUG_REGEX
6
+
5
7
  __all__ = [
8
+ "DISPLAY_ID_REGEX",
9
+ "SLUG_REGEX",
6
10
  "DisplayIDConverter",
11
+ "DisplayIDOrSlugConverter",
7
12
  "DisplayIDOrUUIDConverter",
8
- "UUIDConverter",
13
+ "DisplayIDOrUUIDOrSlugConverter",
14
+ "make_display_id_or_slug_converter",
15
+ "make_display_id_or_uuid_or_slug_converter",
9
16
  ]
10
17
 
18
+ # Regex pattern constants
19
+ DISPLAY_ID_REGEX = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
20
+ UUID_REGEX = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
21
+
22
+
23
+ class BaseConverter:
24
+ """Base class for URL path converters with pass-through conversion."""
25
+
26
+ def to_python(self, value: str) -> str:
27
+ """Convert the URL value to a Python object."""
28
+ return value
29
+
30
+ def to_url(self, value: str) -> str:
31
+ """Convert a Python object to a URL string."""
32
+ return value
33
+
11
34
 
12
- class DisplayIDConverter:
35
+ class DisplayIDConverter(BaseConverter):
13
36
  """Path converter for display IDs.
14
37
 
15
38
  Matches the format: {prefix}_{base62} where prefix is 1-16 lowercase
@@ -26,86 +49,138 @@ class DisplayIDConverter:
26
49
  ]
27
50
  """
28
51
 
29
- regex = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
52
+ regex = DISPLAY_ID_REGEX
30
53
 
31
- def to_python(self, value: str) -> str:
32
- """Convert the URL value to a Python object."""
33
- return value
34
54
 
35
- def to_url(self, value: str) -> str:
36
- """Convert a Python object to a URL string."""
37
- return value
55
+ class DisplayIDOrUUIDConverter(BaseConverter):
56
+ """Path converter for display IDs or UUIDs.
57
+
58
+ Matches either format:
59
+ - Display ID: {prefix}_{base62}
60
+ - UUID: hyphenated (e.g., 550e8400-e29b-41d4-a716-446655440000)
61
+
62
+ Example:
63
+ from django.urls import path, register_converter
64
+ from django_display_ids.converters import DisplayIDOrUUIDConverter
65
+
66
+ register_converter(DisplayIDOrUUIDConverter, "display_id_or_uuid")
67
+
68
+ urlpatterns = [
69
+ path("invoices/<display_id_or_uuid:id>/", InvoiceDetailView.as_view()),
70
+ ]
71
+ """
72
+
73
+ regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX})"
74
+
75
+
76
+ class DisplayIDOrSlugConverter(BaseConverter):
77
+ """Path converter for display IDs or slugs.
78
+
79
+ Matches either format:
80
+ - Display ID: {prefix}_{base62}
81
+ - Slug: Django's default slug pattern [-a-zA-Z0-9_]+
82
+
83
+ Example:
84
+ from django.urls import path, register_converter
85
+ from django_display_ids.converters import DisplayIDOrSlugConverter
86
+
87
+ register_converter(DisplayIDOrSlugConverter, "display_id_or_slug")
88
+
89
+ urlpatterns = [
90
+ path("products/<display_id_or_slug:id>/", ProductDetailView.as_view()),
91
+ ]
92
+ """
38
93
 
94
+ regex = rf"(?:{DISPLAY_ID_REGEX}|{SLUG_REGEX})"
39
95
 
40
- class UUIDConverter:
41
- """Path converter for UUIDs.
42
96
 
43
- Matches UUIDs in both hyphenated and unhyphenated formats:
44
- - 550e8400-e29b-41d4-a716-446655440000 (hyphenated)
45
- - 550e8400e29b41d4a716446655440000 (unhyphenated)
97
+ class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
98
+ """Path converter for display IDs, UUIDs, or slugs.
99
+
100
+ Matches any of:
101
+ - Display ID: {prefix}_{base62}
102
+ - UUID: hyphenated (e.g., 550e8400-e29b-41d4-a716-446655440000)
103
+ - Slug: Django's default slug pattern [-a-zA-Z0-9_]+
46
104
 
47
105
  Example:
48
106
  from django.urls import path, register_converter
49
- from django_display_ids.converters import UUIDConverter
107
+ from django_display_ids.converters import DisplayIDOrUUIDOrSlugConverter
50
108
 
51
- register_converter(UUIDConverter, "uuid")
109
+ register_converter(DisplayIDOrUUIDOrSlugConverter, "identifier")
52
110
 
53
111
  urlpatterns = [
54
- path("invoices/<uuid:id>/", InvoiceDetailView.as_view()),
112
+ path("products/<identifier:id>/", ProductDetailView.as_view()),
55
113
  ]
114
+ """
56
115
 
57
- Note:
58
- Django's built-in UUIDConverter only accepts hyphenated UUIDs.
59
- This converter is more permissive.
116
+ regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{SLUG_REGEX})"
117
+
118
+
119
+ def make_display_id_or_slug_converter(
120
+ slug_regex: str | None = None,
121
+ ) -> type[DisplayIDOrSlugConverter]:
122
+ """Create a DisplayIDOrSlugConverter with a custom slug regex.
123
+
124
+ Args:
125
+ slug_regex: Custom slug regex pattern. If None, uses the
126
+ DISPLAY_IDS["SLUG_REGEX"] setting (defaults to Django's pattern).
127
+
128
+ Returns:
129
+ A DisplayIDOrSlugConverter subclass with the custom regex.
130
+
131
+ Example:
132
+ from django.urls import path, register_converter
133
+ from django_display_ids.converters import make_display_id_or_slug_converter
134
+
135
+ # Lowercase slugs only
136
+ LowercaseConverter = make_display_id_or_slug_converter(r"[a-z0-9-]+")
137
+ register_converter(LowercaseConverter, "display_id_or_slug")
138
+
139
+ urlpatterns = [
140
+ path("products/<display_id_or_slug:id>/", ProductDetailView.as_view()),
141
+ ]
60
142
  """
143
+ from .conf import get_setting
61
144
 
62
- # Hyphenated: 8-4-4-4-12 hex chars with hyphens
63
- # Unhyphenated: 32 hex chars
64
- # Note: Parentheses group the alternatives so ^ and $ anchor correctly
65
- regex = (
66
- r"(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32})"
67
- )
145
+ pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
68
146
 
69
- def to_python(self, value: str) -> str:
70
- """Convert the URL value to a Python object."""
71
- return value
147
+ class CustomDisplayIDOrSlugConverter(DisplayIDOrSlugConverter):
148
+ regex = rf"(?:{DISPLAY_ID_REGEX}|{pattern})"
72
149
 
73
- def to_url(self, value: str) -> str:
74
- """Convert a Python object to a URL string."""
75
- return value
150
+ return CustomDisplayIDOrSlugConverter
76
151
 
77
152
 
78
- class DisplayIDOrUUIDConverter:
79
- """Path converter for display IDs or UUIDs.
153
+ def make_display_id_or_uuid_or_slug_converter(
154
+ slug_regex: str | None = None,
155
+ ) -> type[DisplayIDOrUUIDOrSlugConverter]:
156
+ """Create a DisplayIDOrUUIDOrSlugConverter with a custom slug regex.
80
157
 
81
- Matches either format:
82
- - Display ID: {prefix}_{base62}
83
- - UUID: hyphenated or unhyphenated
158
+ Args:
159
+ slug_regex: Custom slug regex pattern. If None, uses the
160
+ DISPLAY_IDS["SLUG_REGEX"] setting (defaults to Django's pattern).
161
+
162
+ Returns:
163
+ A DisplayIDOrUUIDOrSlugConverter subclass with the custom regex.
84
164
 
85
165
  Example:
86
166
  from django.urls import path, register_converter
87
- from django_display_ids.converters import DisplayIDOrUUIDConverter
167
+ from django_display_ids.converters import (
168
+ make_display_id_or_uuid_or_slug_converter,
169
+ )
88
170
 
89
- register_converter(DisplayIDOrUUIDConverter, "display_id_or_uuid")
171
+ # Lowercase slugs only
172
+ Converter = make_display_id_or_uuid_or_slug_converter(r"[a-z0-9-]+")
173
+ register_converter(Converter, "identifier")
90
174
 
91
175
  urlpatterns = [
92
- path("invoices/<display_id_or_uuid:id>/", InvoiceDetailView.as_view()),
176
+ path("products/<identifier:id>/", ProductDetailView.as_view()),
93
177
  ]
94
178
  """
179
+ from .conf import get_setting
95
180
 
96
- # Note: Parentheses group the alternatives so ^ and $ anchor correctly
97
- regex = (
98
- r"(?:"
99
- r"[a-z]{1,16}_[0-9A-Za-z]{22}"
100
- r"|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
101
- r"|[0-9a-f]{32}"
102
- r")"
103
- )
181
+ pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
104
182
 
105
- def to_python(self, value: str) -> str:
106
- """Convert the URL value to a Python object."""
107
- return value
183
+ class CustomDisplayIDOrUUIDOrSlugConverter(DisplayIDOrUUIDOrSlugConverter):
184
+ regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{pattern})"
108
185
 
109
- def to_url(self, value: str) -> str:
110
- """Convert a Python object to a URL string."""
111
- return value
186
+ return CustomDisplayIDOrUUIDOrSlugConverter
@@ -125,12 +125,15 @@ class DisplayIDMixin(models.Model):
125
125
  """Generate the display ID for this instance.
126
126
 
127
127
  Returns:
128
- Display ID in format {prefix}_{base62(uuid)}, or None if no prefix.
128
+ Display ID in format {prefix}_{base62(uuid)}, or None if no prefix
129
+ or if the UUID field is None (e.g., unsaved instance).
129
130
  """
130
131
  prefix = self.get_display_id_prefix()
131
132
  if prefix is None:
132
133
  return None
133
134
  uuid_value = getattr(self, self._get_uuid_field())
135
+ if uuid_value is None:
136
+ return None
134
137
  return encode_display_id(prefix, uuid_value)
135
138
 
136
139
  # Django admin display configuration
@@ -51,12 +51,21 @@ def resolve_object(
51
51
  UnknownPrefixError: If display ID prefix doesn't match expected.
52
52
  ObjectNotFoundError: If no matching object exists.
53
53
  AmbiguousIdentifierError: If multiple objects match (slug lookup).
54
+ TypeError: If queryset is not for the specified model.
54
55
  """
55
56
  # Parse the identifier to determine type
56
57
  result = parse_identifier(value, strategies, expected_prefix=prefix)
57
58
 
58
59
  # Get the base queryset
59
- qs: QuerySet[M] = queryset if queryset is not None else model._default_manager.all()
60
+ if queryset is not None:
61
+ if queryset.model is not model:
62
+ raise TypeError(
63
+ f"queryset must be for {model.__name__}, "
64
+ f"got queryset for {queryset.model.__name__}"
65
+ )
66
+ qs: QuerySet[M] = queryset
67
+ else:
68
+ qs = model._default_manager.all()
60
69
 
61
70
  # Build the lookup based on strategy
62
71
  lookup: dict[str, Any]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-display-ids
3
- Version: 0.3.0
3
+ Version: 0.3.2
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: MIT
@@ -117,14 +117,14 @@ Full documentation at [django-display-ids.readthedocs.io](https://django-display
117
117
 
118
118
  ## Contributing
119
119
 
120
- See the [contributing guide](https://django-display-ids.readthedocs.io/en/latest/contributing.html).
120
+ See the [contributing guide](https://django-display-ids.readthedocs.io/en/latest/contributing/).
121
121
 
122
122
  ## Related Projects
123
123
 
124
124
  If you need ID generation and storage (custom model fields), consider:
125
125
 
126
- - **[django-prefix-id](https://github.com/jaddison/django-prefix-id)** — PrefixIDField that generates and stores base62-encoded UUIDs
127
- - **[django-spicy-id](https://github.com/mik3y/django-spicy-id)** — Drop-in AutoField replacement
128
- - **[django-charid-field](https://github.com/yunojuno/django-charid-field)** — CharField wrapper supporting cuid, ksuid, ulid
126
+ - [django-prefix-id](https://github.com/jaddison/django-prefix-id) — PrefixIDField that generates and stores base62-encoded UUIDs
127
+ - [django-spicy-id](https://github.com/mik3y/django-spicy-id) — Drop-in AutoField replacement
128
+ - [django-charid-field](https://github.com/yunojuno/django-charid-field) — CharField wrapper supporting cuid, ksuid, ulid
129
129
 
130
130
  **django-display-ids** works with existing UUID fields and handles resolution only — no migrations required.
@@ -1,25 +1,25 @@
1
- django_display_ids/__init__.py,sha256=2qX0YC7Jwpoom5rLOhvOewR3GQEIbUhu5ZYwjfEoklo,3207
1
+ django_display_ids/__init__.py,sha256=wwFvtGjdQia56uY893Jz5iSwaqyH4KXbgvd3nxdzGss,3496
2
2
  django_display_ids/admin.py,sha256=_voqWbr8AwPRC_uCTJWTcEhAhc7RZUgvs7DyVsutDuw,3046
3
3
  django_display_ids/apps.py,sha256=UqblGiYNONOIEH-giEAuKp1YDgxl2yf0jS0ELMj1iig,315
4
- django_display_ids/conf.py,sha256=Mg5ZIhTRSF5dzNO_iYndoF4_g5Umo-5oUw1vB76M7fw,1230
4
+ django_display_ids/conf.py,sha256=6bHdUwDkuroYEPmOlPiAc49EcxlOG_QO0aHHawckOe8,1404
5
5
  django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
6
6
  django_display_ids/contrib/drf_spectacular/__init__.py,sha256=21n56CH7tp2lKq4EGW99KVEgVB9fJ5GnfllUW_KrIe8,4003
7
7
  django_display_ids/contrib/rest_framework/__init__.py,sha256=Xun6zMhCZzQJZQ7ywvBYoKhTJIZEV84AntrbSWfBjYI,1567
8
8
  django_display_ids/contrib/rest_framework/serializers.py,sha256=Jp-z7qHafxkGNYv30YC9rrqLt936SrhONJv3rqfQaC0,4049
9
9
  django_display_ids/contrib/rest_framework/views.py,sha256=nDaze7MJwVqL0MrxQLOur3sPVrRXud_WT4ijGR02jDY,6087
10
- django_display_ids/converters.py,sha256=9xP3vKW1keN1glJvwmZun0BaBRLMKio0aD7Coc888ms,3197
10
+ django_display_ids/converters.py,sha256=ElwrfA7DXiadSZ-Sjvl6ZALgH7tfEZ-tLI7UdE6MsAs,5797
11
11
  django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
12
12
  django_display_ids/examples.py,sha256=gap5NNPTmE7B5uxiYKoMoK8G-OEtL1Ek0W039l6oJ9I,2689
13
13
  django_display_ids/exceptions.py,sha256=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
14
14
  django_display_ids/managers.py,sha256=PymcK4BZL6UsUOtoloHP34MCRNmvNHSKEcOImhZxGag,9779
15
- django_display_ids/models.py,sha256=r2SGrP-6g2LeiZZ4yC1Zp8CcJQ5VvpXS8kQMXl2FBgU,4196
15
+ django_display_ids/models.py,sha256=XI73N4bvxy1Pr2oHeTaJP3uq3huyCX67CFZ2T8mefsA,4317
16
16
  django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- django_display_ids/resolver.py,sha256=TJub6nT6JFThanxETbH8kXVliScjFiksD2kGpu0OvXA,2554
17
+ django_display_ids/resolver.py,sha256=ZlDVoxX0PmVf0MSwPyiNNwQVzdqJGDGE8fm2iyV7QjE,2848
18
18
  django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
19
19
  django_display_ids/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  django_display_ids/templatetags/display_ids.py,sha256=4KHE8r8mgSKb7LgIuXJaJB_3UGrzRZvTdLqSCYQtb5I,1157
21
21
  django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
22
22
  django_display_ids/views.py,sha256=-y_Zwo4QLU0lPRPjABpijsze5vsG0CBvJtrVwVtuLwM,5127
23
- django_display_ids-0.3.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
24
- django_display_ids-0.3.0.dist-info/METADATA,sha256=9QObcv7ll6Nt2c-WzRF-KCm3olO0RNheX-xKUfqzLEc,5299
25
- django_display_ids-0.3.0.dist-info/RECORD,,
23
+ django_display_ids-0.3.2.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
24
+ django_display_ids-0.3.2.dist-info/METADATA,sha256=qlsCntznDOhR1wMFyPhU2AAnWyodabq1nMHtQm2HczA,5283
25
+ django_display_ids-0.3.2.dist-info/RECORD,,