django-display-ids 0.4.0__py3-none-any.whl → 0.5.0__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.
- django_display_ids/conf.py +2 -2
- django_display_ids/contrib/rest_framework/views.py +0 -6
- django_display_ids/converters.py +8 -9
- django_display_ids/exceptions.py +40 -6
- django_display_ids/managers.py +98 -41
- django_display_ids/resolver.py +21 -5
- django_display_ids/typing.py +4 -3
- django_display_ids/views.py +1 -11
- {django_display_ids-0.4.0.dist-info → django_display_ids-0.5.0.dist-info}/METADATA +2 -3
- {django_display_ids-0.4.0.dist-info → django_display_ids-0.5.0.dist-info}/RECORD +11 -11
- {django_display_ids-0.4.0.dist-info → django_display_ids-0.5.0.dist-info}/WHEEL +1 -1
django_display_ids/conf.py
CHANGED
|
@@ -5,7 +5,7 @@ Settings can be configured in Django settings under the DISPLAY_IDS namespace:
|
|
|
5
5
|
DISPLAY_IDS = {
|
|
6
6
|
"UUID_FIELD": "uid",
|
|
7
7
|
"SLUG_FIELD": "slug",
|
|
8
|
-
"STRATEGIES": ("display_id", "uuid"),
|
|
8
|
+
"STRATEGIES": ("display_id", "uuid", "slug"),
|
|
9
9
|
}
|
|
10
10
|
"""
|
|
11
11
|
|
|
@@ -37,7 +37,7 @@ SLUG_REGEX: str = SlugConverter.regex
|
|
|
37
37
|
DEFAULTS: dict[str, str | tuple[str, ...]] = {
|
|
38
38
|
"UUID_FIELD": "id",
|
|
39
39
|
"SLUG_FIELD": "slug",
|
|
40
|
-
"STRATEGIES": ("display_id", "uuid"),
|
|
40
|
+
"STRATEGIES": ("display_id", "uuid", "slug"),
|
|
41
41
|
"SLUG_REGEX": SLUG_REGEX,
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -13,9 +13,7 @@ from django_display_ids.conf import (
|
|
|
13
13
|
from django_display_ids.encoding import PREFIX_PATTERN
|
|
14
14
|
from django_display_ids.exceptions import (
|
|
15
15
|
DisplayIDLookupError,
|
|
16
|
-
InvalidIdentifierError,
|
|
17
16
|
ObjectNotFoundError,
|
|
18
|
-
UnknownPrefixError,
|
|
19
17
|
)
|
|
20
18
|
from django_display_ids.resolver import resolve_object
|
|
21
19
|
from django_display_ids.typing import StrategyName # noqa: TC001 - used at runtime
|
|
@@ -57,7 +55,6 @@ class DisplayIDMixin:
|
|
|
57
55
|
Example:
|
|
58
56
|
class InvoiceView(DisplayIDMixin, APIView):
|
|
59
57
|
lookup_url_kwarg = "id"
|
|
60
|
-
lookup_strategies = ("display_id", "uuid")
|
|
61
58
|
display_id_prefix = "inv"
|
|
62
59
|
|
|
63
60
|
def get(self, request, *args, **kwargs):
|
|
@@ -69,7 +66,6 @@ class DisplayIDMixin:
|
|
|
69
66
|
queryset = Invoice.objects.all()
|
|
70
67
|
serializer_class = InvoiceSerializer
|
|
71
68
|
lookup_url_kwarg = "pk"
|
|
72
|
-
lookup_strategies = ("display_id", "uuid")
|
|
73
69
|
"""
|
|
74
70
|
|
|
75
71
|
lookup_url_kwarg: str = "pk"
|
|
@@ -165,8 +161,6 @@ class DisplayIDMixin:
|
|
|
165
161
|
)
|
|
166
162
|
except ObjectNotFoundError as e:
|
|
167
163
|
raise NotFound(str(e)) from e
|
|
168
|
-
except (InvalidIdentifierError, UnknownPrefixError) as e:
|
|
169
|
-
raise ParseError(str(e)) from e
|
|
170
164
|
except DisplayIDLookupError as e:
|
|
171
165
|
raise ParseError(str(e)) from e
|
|
172
166
|
|
django_display_ids/converters.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from .conf import SLUG_REGEX
|
|
5
|
+
from .conf import SLUG_REGEX, get_setting
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"DISPLAY_ID_REGEX",
|
|
@@ -19,6 +19,9 @@ __all__ = [
|
|
|
19
19
|
DISPLAY_ID_REGEX = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
|
|
20
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
21
|
|
|
22
|
+
# Slug regex from settings (respects DISPLAY_IDS["SLUG_REGEX"] Django setting)
|
|
23
|
+
_SLUG_REGEX: str = str(get_setting("SLUG_REGEX"))
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class BaseConverter:
|
|
24
27
|
"""Base class for URL path converters with pass-through conversion."""
|
|
@@ -78,7 +81,7 @@ class DisplayIDOrSlugConverter(BaseConverter):
|
|
|
78
81
|
|
|
79
82
|
Matches either format:
|
|
80
83
|
- Display ID: {prefix}_{base62}
|
|
81
|
-
- Slug:
|
|
84
|
+
- Slug: matches DISPLAY_IDS["SLUG_REGEX"] setting (default: [-a-zA-Z0-9_]+)
|
|
82
85
|
|
|
83
86
|
Example:
|
|
84
87
|
from django.urls import path, register_converter
|
|
@@ -91,7 +94,7 @@ class DisplayIDOrSlugConverter(BaseConverter):
|
|
|
91
94
|
]
|
|
92
95
|
"""
|
|
93
96
|
|
|
94
|
-
regex = rf"(?:{DISPLAY_ID_REGEX}|{
|
|
97
|
+
regex = rf"(?:{DISPLAY_ID_REGEX}|{_SLUG_REGEX})"
|
|
95
98
|
|
|
96
99
|
|
|
97
100
|
class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
|
|
@@ -100,7 +103,7 @@ class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
|
|
|
100
103
|
Matches any of:
|
|
101
104
|
- Display ID: {prefix}_{base62}
|
|
102
105
|
- UUID: hyphenated (e.g., 550e8400-e29b-41d4-a716-446655440000)
|
|
103
|
-
- Slug:
|
|
106
|
+
- Slug: matches DISPLAY_IDS["SLUG_REGEX"] setting (default: [-a-zA-Z0-9_]+)
|
|
104
107
|
|
|
105
108
|
Example:
|
|
106
109
|
from django.urls import path, register_converter
|
|
@@ -113,7 +116,7 @@ class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
|
|
|
113
116
|
]
|
|
114
117
|
"""
|
|
115
118
|
|
|
116
|
-
regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{
|
|
119
|
+
regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{_SLUG_REGEX})"
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
def make_display_id_or_slug_converter(
|
|
@@ -140,8 +143,6 @@ def make_display_id_or_slug_converter(
|
|
|
140
143
|
path("products/<display_id_or_slug:id>/", ProductDetailView.as_view()),
|
|
141
144
|
]
|
|
142
145
|
"""
|
|
143
|
-
from .conf import get_setting
|
|
144
|
-
|
|
145
146
|
pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
|
|
146
147
|
|
|
147
148
|
class CustomDisplayIDOrSlugConverter(DisplayIDOrSlugConverter):
|
|
@@ -176,8 +177,6 @@ def make_display_id_or_uuid_or_slug_converter(
|
|
|
176
177
|
path("products/<identifier:id>/", ProductDetailView.as_view()),
|
|
177
178
|
]
|
|
178
179
|
"""
|
|
179
|
-
from .conf import get_setting
|
|
180
|
-
|
|
181
180
|
pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
|
|
182
181
|
|
|
183
182
|
class CustomDisplayIDOrUUIDOrSlugConverter(DisplayIDOrUUIDOrSlugConverter):
|
django_display_ids/exceptions.py
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
"""Typed exceptions for identifier lookup errors.
|
|
1
|
+
"""Typed exceptions for identifier lookup errors.
|
|
2
|
+
|
|
3
|
+
All exceptions inherit from both ``DisplayIDLookupError`` and a standard
|
|
4
|
+
Django/Python exception, so they integrate naturally with existing error
|
|
5
|
+
handling patterns::
|
|
6
|
+
|
|
7
|
+
# Catch with library-specific base
|
|
8
|
+
except DisplayIDLookupError: ...
|
|
9
|
+
|
|
10
|
+
# Or catch with standard Django/Python exceptions
|
|
11
|
+
except ObjectDoesNotExist: ... # catches ObjectNotFoundError
|
|
12
|
+
except ValueError: ... # catches InvalidIdentifierError, UnknownPrefixError
|
|
13
|
+
except ImproperlyConfigured: ... # catches MissingPrefixError
|
|
14
|
+
"""
|
|
2
15
|
|
|
3
16
|
from __future__ import annotations
|
|
4
17
|
|
|
18
|
+
from django.core.exceptions import (
|
|
19
|
+
ImproperlyConfigured,
|
|
20
|
+
MultipleObjectsReturned,
|
|
21
|
+
ObjectDoesNotExist,
|
|
22
|
+
)
|
|
23
|
+
|
|
5
24
|
__all__ = [
|
|
6
25
|
"AmbiguousIdentifierError",
|
|
7
26
|
"DisplayIDLookupError",
|
|
@@ -18,11 +37,14 @@ class DisplayIDLookupError(Exception):
|
|
|
18
37
|
pass
|
|
19
38
|
|
|
20
39
|
|
|
21
|
-
class InvalidIdentifierError(DisplayIDLookupError):
|
|
40
|
+
class InvalidIdentifierError(DisplayIDLookupError, ValueError):
|
|
22
41
|
"""Raised when an identifier has an invalid format.
|
|
23
42
|
|
|
24
43
|
This indicates the identifier string cannot be parsed as any
|
|
25
44
|
of the supported formats (UUID, display ID, or slug).
|
|
45
|
+
|
|
46
|
+
Inherits from ``ValueError`` because it represents bad input — the
|
|
47
|
+
caller provided a value that isn't a valid identifier.
|
|
26
48
|
"""
|
|
27
49
|
|
|
28
50
|
def __init__(self, value: str, message: str | None = None) -> None:
|
|
@@ -31,11 +53,14 @@ class InvalidIdentifierError(DisplayIDLookupError):
|
|
|
31
53
|
super().__init__(self.message)
|
|
32
54
|
|
|
33
55
|
|
|
34
|
-
class UnknownPrefixError(DisplayIDLookupError):
|
|
56
|
+
class UnknownPrefixError(DisplayIDLookupError, ValueError):
|
|
35
57
|
"""Raised when a display ID has an unexpected prefix.
|
|
36
58
|
|
|
37
59
|
This occurs when prefix enforcement is enabled and the
|
|
38
60
|
display ID's prefix doesn't match the expected value.
|
|
61
|
+
|
|
62
|
+
Inherits from ``ValueError`` because it represents bad input — the
|
|
63
|
+
caller provided a display ID with the wrong prefix.
|
|
39
64
|
"""
|
|
40
65
|
|
|
41
66
|
def __init__(self, value: str, actual: str, expected: str | None = None) -> None:
|
|
@@ -49,11 +74,14 @@ class UnknownPrefixError(DisplayIDLookupError):
|
|
|
49
74
|
super().__init__(message)
|
|
50
75
|
|
|
51
76
|
|
|
52
|
-
class MissingPrefixError(DisplayIDLookupError):
|
|
77
|
+
class MissingPrefixError(DisplayIDLookupError, ImproperlyConfigured):
|
|
53
78
|
"""Raised when a display ID lookup is attempted without a prefix.
|
|
54
79
|
|
|
55
80
|
This occurs when calling get_by_display_id() on a model that
|
|
56
81
|
doesn't have display_id_prefix configured.
|
|
82
|
+
|
|
83
|
+
Inherits from ``ImproperlyConfigured`` because it represents a
|
|
84
|
+
configuration problem — the model is missing a required setting.
|
|
57
85
|
"""
|
|
58
86
|
|
|
59
87
|
def __init__(self, model_name: str | None = None) -> None:
|
|
@@ -68,11 +96,14 @@ class MissingPrefixError(DisplayIDLookupError):
|
|
|
68
96
|
super().__init__(message)
|
|
69
97
|
|
|
70
98
|
|
|
71
|
-
class ObjectNotFoundError(DisplayIDLookupError):
|
|
99
|
+
class ObjectNotFoundError(DisplayIDLookupError, ObjectDoesNotExist):
|
|
72
100
|
"""Raised when no object matches the identifier.
|
|
73
101
|
|
|
74
102
|
This indicates the identifier was valid but no matching
|
|
75
103
|
database record exists.
|
|
104
|
+
|
|
105
|
+
Inherits from ``ObjectDoesNotExist`` so it integrates with Django's
|
|
106
|
+
built-in error handling (e.g., ``get_object_or_404``).
|
|
76
107
|
"""
|
|
77
108
|
|
|
78
109
|
def __init__(self, value: str, model_name: str | None = None) -> None:
|
|
@@ -85,11 +116,14 @@ class ObjectNotFoundError(DisplayIDLookupError):
|
|
|
85
116
|
super().__init__(message)
|
|
86
117
|
|
|
87
118
|
|
|
88
|
-
class AmbiguousIdentifierError(DisplayIDLookupError):
|
|
119
|
+
class AmbiguousIdentifierError(DisplayIDLookupError, MultipleObjectsReturned):
|
|
89
120
|
"""Raised when an identifier matches multiple objects.
|
|
90
121
|
|
|
91
122
|
This can occur with slug lookups if slugs are not unique,
|
|
92
123
|
or in edge cases with identifier collisions.
|
|
124
|
+
|
|
125
|
+
Inherits from ``MultipleObjectsReturned`` because the semantics
|
|
126
|
+
are identical — a lookup that expected one result found many.
|
|
93
127
|
"""
|
|
94
128
|
|
|
95
129
|
def __init__(self, value: str, count: int) -> None:
|
django_display_ids/managers.py
CHANGED
|
@@ -2,19 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Self, TypeVar
|
|
6
7
|
|
|
8
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
7
9
|
from django.db import models
|
|
8
10
|
from django.db.models import Q
|
|
9
11
|
|
|
10
12
|
from .conf import get_setting
|
|
11
13
|
from .encoding import decode_display_id
|
|
12
14
|
from .exceptions import (
|
|
13
|
-
|
|
14
|
-
InvalidIdentifierError,
|
|
15
|
+
DisplayIDLookupError,
|
|
15
16
|
MissingPrefixError,
|
|
16
|
-
ObjectNotFoundError,
|
|
17
|
-
UnknownPrefixError,
|
|
18
17
|
)
|
|
19
18
|
from .strategies import parse_identifier
|
|
20
19
|
|
|
@@ -49,57 +48,88 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
49
48
|
invoice = Invoice.objects.get_by_display_id("inv_1a2B3c4D5e6F7g8H")
|
|
50
49
|
"""
|
|
51
50
|
|
|
51
|
+
# Re-annotate inherited QuerySet methods with -> Self so that
|
|
52
|
+
# display ID methods remain visible to type checkers after chaining
|
|
53
|
+
# (e.g. Invoice.objects.filter(...).get_by_identifier(...)).
|
|
54
|
+
def filter(self, *args: Any, **kwargs: Any) -> Self:
|
|
55
|
+
return super().filter(*args, **kwargs)
|
|
56
|
+
|
|
57
|
+
def exclude(self, *args: Any, **kwargs: Any) -> Self:
|
|
58
|
+
return super().exclude(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
def select_related(self, *fields: Any) -> Self:
|
|
61
|
+
return super().select_related(*fields)
|
|
62
|
+
|
|
63
|
+
def prefetch_related(self, *lookups: Any) -> Self:
|
|
64
|
+
return super().prefetch_related(*lookups)
|
|
65
|
+
|
|
66
|
+
def order_by(self, *fields: Any) -> Self:
|
|
67
|
+
return super().order_by(*fields)
|
|
68
|
+
|
|
69
|
+
def distinct(self, *fields: Any) -> Self:
|
|
70
|
+
return super().distinct(*fields)
|
|
71
|
+
|
|
72
|
+
def all(self) -> Self:
|
|
73
|
+
return super().all()
|
|
74
|
+
|
|
75
|
+
def none(self) -> Self:
|
|
76
|
+
return super().none()
|
|
77
|
+
|
|
52
78
|
def get_by_display_id(
|
|
53
79
|
self,
|
|
54
|
-
value: str,
|
|
80
|
+
value: str | uuid.UUID,
|
|
55
81
|
*,
|
|
56
82
|
prefix: str | None = None,
|
|
57
83
|
) -> M:
|
|
58
84
|
"""Get an object by its display ID.
|
|
59
85
|
|
|
60
86
|
Args:
|
|
61
|
-
value: The display ID string (e.g., "inv_1a2B3c4D5e6F7g8H")
|
|
87
|
+
value: The display ID string (e.g., "inv_1a2B3c4D5e6F7g8H"),
|
|
88
|
+
or a UUID instance for direct UUID lookup.
|
|
62
89
|
prefix: Expected prefix for validation. If None, uses model's prefix.
|
|
63
90
|
|
|
64
91
|
Returns:
|
|
65
92
|
The matching model instance.
|
|
66
93
|
|
|
67
94
|
Raises:
|
|
95
|
+
Model.DoesNotExist: If no matching object exists, if the display ID
|
|
96
|
+
format is invalid, or if the prefix doesn't match.
|
|
68
97
|
MissingPrefixError: If no prefix is configured on the model.
|
|
69
|
-
InvalidIdentifierError: If the display ID format is invalid.
|
|
70
|
-
UnknownPrefixError: If the prefix doesn't match expected.
|
|
71
|
-
ObjectNotFoundError: If no matching object exists.
|
|
72
98
|
"""
|
|
73
|
-
# Get model config
|
|
74
99
|
model = self.model
|
|
75
100
|
uuid_field = self._get_uuid_field()
|
|
101
|
+
|
|
102
|
+
# UUID objects skip display ID parsing entirely
|
|
103
|
+
if isinstance(value, uuid.UUID):
|
|
104
|
+
return self.get(**{uuid_field: value})
|
|
105
|
+
|
|
106
|
+
# Get model config
|
|
76
107
|
expected_prefix = prefix or self._get_model_prefix()
|
|
77
108
|
|
|
78
109
|
# Require a prefix for display ID lookups
|
|
79
110
|
if expected_prefix is None:
|
|
80
111
|
raise MissingPrefixError(model_name=model.__name__)
|
|
81
112
|
|
|
82
|
-
# Decode the display ID
|
|
113
|
+
# Decode the display ID and validate prefix
|
|
83
114
|
try:
|
|
84
115
|
decoded_prefix, uuid_value = decode_display_id(value)
|
|
85
116
|
except ValueError as e:
|
|
86
|
-
raise
|
|
117
|
+
raise model.DoesNotExist( # type: ignore[attr-defined]
|
|
118
|
+
f"{model.__name__}: invalid display ID: {value!r}"
|
|
119
|
+
) from e
|
|
87
120
|
|
|
88
|
-
# Validate prefix
|
|
89
121
|
if decoded_prefix != expected_prefix:
|
|
90
|
-
raise
|
|
91
|
-
|
|
122
|
+
raise model.DoesNotExist( # type: ignore[attr-defined]
|
|
123
|
+
f"{model.__name__}: unknown prefix {decoded_prefix!r} "
|
|
124
|
+
f"in {value!r}, expected {expected_prefix!r}"
|
|
92
125
|
)
|
|
93
126
|
|
|
94
127
|
# Query the database
|
|
95
|
-
|
|
96
|
-
return self.get(**{uuid_field: uuid_value})
|
|
97
|
-
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
98
|
-
raise ObjectNotFoundError(value, model_name=model.__name__) from None
|
|
128
|
+
return self.get(**{uuid_field: uuid_value})
|
|
99
129
|
|
|
100
130
|
def get_by_identifier(
|
|
101
131
|
self,
|
|
102
|
-
value: str,
|
|
132
|
+
value: str | uuid.UUID,
|
|
103
133
|
*,
|
|
104
134
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
105
135
|
prefix: str | None = None,
|
|
@@ -109,7 +139,8 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
109
139
|
Tries each strategy in order and returns the first match.
|
|
110
140
|
|
|
111
141
|
Args:
|
|
112
|
-
value: The identifier string (display ID, UUID, or slug)
|
|
142
|
+
value: The identifier string (display ID, UUID, or slug),
|
|
143
|
+
or a UUID instance for direct UUID lookup.
|
|
113
144
|
strategies: Strategies to try. Defaults to settings.
|
|
114
145
|
prefix: Expected display ID prefix for validation.
|
|
115
146
|
|
|
@@ -117,21 +148,34 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
117
148
|
The matching model instance.
|
|
118
149
|
|
|
119
150
|
Raises:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
AmbiguousIdentifierError: If multiple objects match (slug).
|
|
151
|
+
Model.DoesNotExist: If the identifier cannot be parsed or
|
|
152
|
+
no matching object exists.
|
|
153
|
+
Model.MultipleObjectsReturned: If multiple objects match (slug).
|
|
124
154
|
"""
|
|
125
155
|
model = self.model
|
|
126
156
|
uuid_field = self._get_uuid_field()
|
|
157
|
+
|
|
158
|
+
# UUID objects skip strategy parsing entirely
|
|
159
|
+
if isinstance(value, uuid.UUID):
|
|
160
|
+
return self.get(**{uuid_field: value})
|
|
161
|
+
|
|
127
162
|
slug_field = self._get_slug_field()
|
|
128
163
|
expected_prefix = prefix or self._get_model_prefix()
|
|
129
164
|
lookup_strategies = strategies or self._get_strategies()
|
|
130
165
|
|
|
166
|
+
# Skip slug strategy if the model has no slug field
|
|
167
|
+
if not self._has_slug_field(slug_field):
|
|
168
|
+
lookup_strategies = tuple(s for s in lookup_strategies if s != "slug")
|
|
169
|
+
|
|
131
170
|
# Parse the identifier
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
171
|
+
try:
|
|
172
|
+
result = parse_identifier(
|
|
173
|
+
value, lookup_strategies, expected_prefix=expected_prefix
|
|
174
|
+
)
|
|
175
|
+
except DisplayIDLookupError as e:
|
|
176
|
+
raise model.DoesNotExist( # type: ignore[attr-defined]
|
|
177
|
+
f"{model.__name__}: {e}"
|
|
178
|
+
) from e
|
|
135
179
|
|
|
136
180
|
# Build the lookup
|
|
137
181
|
lookup: dict[str, Any]
|
|
@@ -141,17 +185,11 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
141
185
|
lookup = {slug_field: result.slug}
|
|
142
186
|
|
|
143
187
|
# Execute the query
|
|
144
|
-
|
|
145
|
-
return self.get(**lookup)
|
|
146
|
-
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
147
|
-
raise ObjectNotFoundError(value, model_name=model.__name__) from None
|
|
148
|
-
except model.MultipleObjectsReturned: # type: ignore[attr-defined]
|
|
149
|
-
count = self.filter(**lookup).count()
|
|
150
|
-
raise AmbiguousIdentifierError(value, count) from None
|
|
188
|
+
return self.get(**lookup)
|
|
151
189
|
|
|
152
190
|
def get_by_identifiers(
|
|
153
191
|
self,
|
|
154
|
-
values: Sequence[str],
|
|
192
|
+
values: Sequence[str | uuid.UUID],
|
|
155
193
|
*,
|
|
156
194
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
157
195
|
prefix: str | None = None,
|
|
@@ -162,7 +200,8 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
162
200
|
then executes a single database query using `__in` lookups.
|
|
163
201
|
|
|
164
202
|
Args:
|
|
165
|
-
values: A sequence of identifier strings (display IDs, UUIDs, or slugs)
|
|
203
|
+
values: A sequence of identifier strings (display IDs, UUIDs, or slugs)
|
|
204
|
+
or UUID instances. UUID instances skip strategy parsing.
|
|
166
205
|
strategies: Strategies to try. Defaults to settings.
|
|
167
206
|
prefix: Expected display ID prefix for validation.
|
|
168
207
|
|
|
@@ -178,6 +217,7 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
178
217
|
'inv_2aUyqjCzEIiEcYMKj7TZtw',
|
|
179
218
|
'inv_7kN3xPqRmLwYvTzJ5HfUaB',
|
|
180
219
|
'550e8400-e29b-41d4-a716-446655440000',
|
|
220
|
+
uuid.UUID('550e8400-e29b-41d4-a716-446655440000'),
|
|
181
221
|
])
|
|
182
222
|
"""
|
|
183
223
|
if not values:
|
|
@@ -188,11 +228,20 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
188
228
|
expected_prefix = prefix or self._get_model_prefix()
|
|
189
229
|
lookup_strategies = strategies or self._get_strategies()
|
|
190
230
|
|
|
231
|
+
# Skip slug strategy if the model has no slug field
|
|
232
|
+
if not self._has_slug_field(slug_field):
|
|
233
|
+
lookup_strategies = tuple(s for s in lookup_strategies if s != "slug")
|
|
234
|
+
|
|
191
235
|
# Collect UUIDs and slugs separately
|
|
192
236
|
uuids: list[Any] = []
|
|
193
237
|
slugs: list[str] = []
|
|
194
238
|
|
|
195
239
|
for value in values:
|
|
240
|
+
# UUID objects skip strategy parsing entirely
|
|
241
|
+
if isinstance(value, uuid.UUID):
|
|
242
|
+
uuids.append(value)
|
|
243
|
+
continue
|
|
244
|
+
|
|
196
245
|
result = parse_identifier(
|
|
197
246
|
value, lookup_strategies, expected_prefix=expected_prefix
|
|
198
247
|
)
|
|
@@ -228,6 +277,14 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
228
277
|
"""Get the default strategies."""
|
|
229
278
|
return get_setting("STRATEGIES") # type: ignore[return-value]
|
|
230
279
|
|
|
280
|
+
def _has_slug_field(self, slug_field: str) -> bool:
|
|
281
|
+
"""Check whether the model has the configured slug field."""
|
|
282
|
+
try:
|
|
283
|
+
self.model._meta.get_field(slug_field)
|
|
284
|
+
return True
|
|
285
|
+
except FieldDoesNotExist:
|
|
286
|
+
return False
|
|
287
|
+
|
|
231
288
|
def _get_model_prefix(self) -> str | None:
|
|
232
289
|
"""Get the display ID prefix from the model, if defined."""
|
|
233
290
|
if hasattr(self.model, "get_display_id_prefix"):
|
|
@@ -253,7 +310,7 @@ class DisplayIDManager(models.Manager[M]):
|
|
|
253
310
|
|
|
254
311
|
def get_by_display_id(
|
|
255
312
|
self,
|
|
256
|
-
value: str,
|
|
313
|
+
value: str | uuid.UUID,
|
|
257
314
|
*,
|
|
258
315
|
prefix: str | None = None,
|
|
259
316
|
) -> M:
|
|
@@ -265,7 +322,7 @@ class DisplayIDManager(models.Manager[M]):
|
|
|
265
322
|
|
|
266
323
|
def get_by_identifier(
|
|
267
324
|
self,
|
|
268
|
-
value: str,
|
|
325
|
+
value: str | uuid.UUID,
|
|
269
326
|
*,
|
|
270
327
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
271
328
|
prefix: str | None = None,
|
|
@@ -280,7 +337,7 @@ class DisplayIDManager(models.Manager[M]):
|
|
|
280
337
|
|
|
281
338
|
def get_by_identifiers(
|
|
282
339
|
self,
|
|
283
|
-
values: Sequence[str],
|
|
340
|
+
values: Sequence[str | uuid.UUID],
|
|
284
341
|
*,
|
|
285
342
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
286
343
|
prefix: str | None = None,
|
django_display_ids/resolver.py
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import uuid
|
|
5
6
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
7
|
|
|
8
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
7
9
|
from django.db import models
|
|
8
10
|
|
|
9
11
|
from .exceptions import AmbiguousIdentifierError, ObjectNotFoundError
|
|
@@ -23,7 +25,7 @@ M = TypeVar("M", bound=models.Model)
|
|
|
23
25
|
def resolve_object(
|
|
24
26
|
*,
|
|
25
27
|
model: type[M],
|
|
26
|
-
value: str,
|
|
28
|
+
value: str | uuid.UUID,
|
|
27
29
|
strategies: tuple[StrategyName, ...] = DEFAULT_STRATEGIES,
|
|
28
30
|
prefix: str | None = None,
|
|
29
31
|
uuid_field: str = "id",
|
|
@@ -36,7 +38,8 @@ def resolve_object(
|
|
|
36
38
|
|
|
37
39
|
Args:
|
|
38
40
|
model: The Django model class.
|
|
39
|
-
value: The identifier string (UUID, display ID, or slug)
|
|
41
|
+
value: The identifier string (UUID, display ID, or slug),
|
|
42
|
+
or a UUID instance for direct UUID lookup.
|
|
40
43
|
strategies: Tuple of strategy names to try in order.
|
|
41
44
|
prefix: Expected display ID prefix (for validation).
|
|
42
45
|
uuid_field: Name of the UUID field on the model.
|
|
@@ -53,9 +56,6 @@ def resolve_object(
|
|
|
53
56
|
AmbiguousIdentifierError: If multiple objects match (slug lookup).
|
|
54
57
|
TypeError: If queryset is not for the specified model.
|
|
55
58
|
"""
|
|
56
|
-
# Parse the identifier to determine type
|
|
57
|
-
result = parse_identifier(value, strategies, expected_prefix=prefix)
|
|
58
|
-
|
|
59
59
|
# Get the base queryset
|
|
60
60
|
if queryset is not None:
|
|
61
61
|
if queryset.model is not model:
|
|
@@ -67,6 +67,22 @@ def resolve_object(
|
|
|
67
67
|
else:
|
|
68
68
|
qs = model._default_manager.all()
|
|
69
69
|
|
|
70
|
+
# UUID objects skip strategy parsing entirely
|
|
71
|
+
if isinstance(value, uuid.UUID):
|
|
72
|
+
try:
|
|
73
|
+
return qs.get(**{uuid_field: value})
|
|
74
|
+
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
75
|
+
raise ObjectNotFoundError(str(value), model_name=model.__name__) from None
|
|
76
|
+
|
|
77
|
+
# Skip slug strategy if the model has no slug field
|
|
78
|
+
try:
|
|
79
|
+
model._meta.get_field(slug_field)
|
|
80
|
+
except FieldDoesNotExist:
|
|
81
|
+
strategies = tuple(s for s in strategies if s != "slug")
|
|
82
|
+
|
|
83
|
+
# Parse the identifier to determine type
|
|
84
|
+
result = parse_identifier(value, strategies, expected_prefix=prefix)
|
|
85
|
+
|
|
70
86
|
# Build the lookup based on strategy
|
|
71
87
|
lookup: dict[str, Any]
|
|
72
88
|
if result.strategy in ("uuid", "display_id"):
|
django_display_ids/typing.py
CHANGED
|
@@ -12,6 +12,7 @@ __all__ = [
|
|
|
12
12
|
# Supported lookup strategy names
|
|
13
13
|
StrategyName = Literal["uuid", "display_id", "slug"]
|
|
14
14
|
|
|
15
|
-
# Default strategy order: display_id first (most specific), then uuid
|
|
16
|
-
# Slug is
|
|
17
|
-
|
|
15
|
+
# Default strategy order: display_id first (most specific), then uuid, then slug
|
|
16
|
+
# Slug is a catch-all — it's safe to include by default because the manager
|
|
17
|
+
# and resolver automatically skip it for models without a slug field.
|
|
18
|
+
DEFAULT_STRATEGIES: tuple[StrategyName, ...] = ("display_id", "uuid", "slug")
|
django_display_ids/views.py
CHANGED
|
@@ -8,12 +8,7 @@ from django.http import Http404
|
|
|
8
8
|
|
|
9
9
|
from .conf import NOT_SET, get_setting, get_slug_field, get_uuid_field
|
|
10
10
|
from .encoding import PREFIX_PATTERN
|
|
11
|
-
from .exceptions import
|
|
12
|
-
DisplayIDLookupError,
|
|
13
|
-
InvalidIdentifierError,
|
|
14
|
-
ObjectNotFoundError,
|
|
15
|
-
UnknownPrefixError,
|
|
16
|
-
)
|
|
11
|
+
from .exceptions import DisplayIDLookupError
|
|
17
12
|
from .resolver import resolve_object
|
|
18
13
|
from .typing import StrategyName # noqa: TC001 - used at runtime in type hints
|
|
19
14
|
|
|
@@ -43,7 +38,6 @@ class DisplayIDMixin:
|
|
|
43
38
|
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
44
39
|
model = Invoice
|
|
45
40
|
lookup_param = "id"
|
|
46
|
-
lookup_strategies = ("display_id", "uuid")
|
|
47
41
|
display_id_prefix = "inv"
|
|
48
42
|
"""
|
|
49
43
|
|
|
@@ -136,9 +130,5 @@ class DisplayIDMixin:
|
|
|
136
130
|
slug_field=self._get_slug_field(),
|
|
137
131
|
queryset=qs,
|
|
138
132
|
)
|
|
139
|
-
except ObjectNotFoundError as e:
|
|
140
|
-
raise Http404(str(e)) from e
|
|
141
|
-
except (InvalidIdentifierError, UnknownPrefixError) as e:
|
|
142
|
-
raise Http404(str(e)) from e
|
|
143
133
|
except DisplayIDLookupError as e:
|
|
144
134
|
raise Http404(str(e)) from e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: django-display-ids
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -19,6 +19,7 @@ Requires-Dist: django>=4.2
|
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Project-URL: Documentation, https://django-display-ids.readthedocs.io/
|
|
21
21
|
Project-URL: Repository, https://github.com/josephabrahams/django-display-ids
|
|
22
|
+
Project-URL: Changelog, https://github.com/josephabrahams/django-display-ids/blob/main/CHANGELOG.md
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
24
25
|
# django-display-ids
|
|
@@ -71,7 +72,6 @@ from django_display_ids import DisplayIDMixin
|
|
|
71
72
|
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
72
73
|
model = Invoice
|
|
73
74
|
lookup_param = "id"
|
|
74
|
-
lookup_strategies = ("display_id", "uuid", "slug")
|
|
75
75
|
display_id_prefix = "inv"
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -85,7 +85,6 @@ class InvoiceViewSet(DisplayIDMixin, ModelViewSet):
|
|
|
85
85
|
queryset = Invoice.objects.all()
|
|
86
86
|
serializer_class = InvoiceSerializer
|
|
87
87
|
lookup_url_kwarg = "id"
|
|
88
|
-
lookup_strategies = ("display_id", "uuid", "slug")
|
|
89
88
|
display_id_prefix = "inv"
|
|
90
89
|
```
|
|
91
90
|
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
django_display_ids/__init__.py,sha256=zFd0XNNOeVgpIImWBe_b-HD59svbVwkDVYq8X6lsLug,3256
|
|
2
2
|
django_display_ids/admin.py,sha256=UPmU-kGsZ5x_-r9n99P17La5lr-3BLvB9qTfWJgklt8,2569
|
|
3
3
|
django_display_ids/apps.py,sha256=UqblGiYNONOIEH-giEAuKp1YDgxl2yf0jS0ELMj1iig,315
|
|
4
|
-
django_display_ids/conf.py,sha256=
|
|
4
|
+
django_display_ids/conf.py,sha256=cBL_b8AW13hUYmN7mMViNQyndZisBxXU-1vSLLIxzv4,2231
|
|
5
5
|
django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
|
|
6
6
|
django_display_ids/contrib/drf_spectacular/__init__.py,sha256=9swSJge_dQldC4AZMkYI3M04LzU20eJ_2oz3XABviFA,5427
|
|
7
7
|
django_display_ids/contrib/rest_framework/__init__.py,sha256=pKV6BVB7k0dJm29C7XMr0M-wK2lXPUgW8Hf4BZHHwuE,198
|
|
8
8
|
django_display_ids/contrib/rest_framework/serializers.py,sha256=Jp-z7qHafxkGNYv30YC9rrqLt936SrhONJv3rqfQaC0,4049
|
|
9
|
-
django_display_ids/contrib/rest_framework/views.py,sha256=
|
|
10
|
-
django_display_ids/converters.py,sha256=
|
|
9
|
+
django_display_ids/contrib/rest_framework/views.py,sha256=88qe3w5QL0WNCOY17WjbXl8vrVzLDD149QIEfSl6tCY,5689
|
|
10
|
+
django_display_ids/converters.py,sha256=KGD5FYWkuXztO-6TvTtPwMP7xbnAXAaYuk9LvIpxkBM,5918
|
|
11
11
|
django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
|
|
12
12
|
django_display_ids/examples.py,sha256=gap5NNPTmE7B5uxiYKoMoK8G-OEtL1Ek0W039l6oJ9I,2689
|
|
13
|
-
django_display_ids/exceptions.py,sha256=
|
|
14
|
-
django_display_ids/managers.py,sha256=
|
|
13
|
+
django_display_ids/exceptions.py,sha256=68Kmp9Qdyg3LtzmXfDHIWRuI7MW3M2acZ3C6Wi8zFEw,4489
|
|
14
|
+
django_display_ids/managers.py,sha256=tQ1ryAIClAg-mhDp5CgkCEqxld9XVMe6fl-u__DSiOI,11831
|
|
15
15
|
django_display_ids/models.py,sha256=_hmgAR4HC3I-5wU2DND6uNLjEllu1Y9eaXeBQ9dWMNI,4313
|
|
16
16
|
django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
django_display_ids/resolver.py,sha256=
|
|
17
|
+
django_display_ids/resolver.py,sha256=blyLo06tIYrYqOA9Ir260tzs9njX1GMAE6ICDJTwfGk,3485
|
|
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
|
-
django_display_ids/typing.py,sha256=
|
|
22
|
-
django_display_ids/views.py,sha256=
|
|
23
|
-
django_display_ids-0.
|
|
24
|
-
django_display_ids-0.
|
|
25
|
-
django_display_ids-0.
|
|
21
|
+
django_display_ids/typing.py,sha256=hxX2QVYtkvFXZBH9ZskbV98N-uOhX_XAMoO-UOBRnEA,568
|
|
22
|
+
django_display_ids/views.py,sha256=OOhv5G6ake8Siv-yAFFA1s2wOuNng9NYETS1feDygio,4662
|
|
23
|
+
django_display_ids-0.5.0.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
24
|
+
django_display_ids-0.5.0.dist-info/METADATA,sha256=twAX7DSsXtTAbGl5IJbZaJ7MrjndlQV1TVin1nhsLhI,5249
|
|
25
|
+
django_display_ids-0.5.0.dist-info/RECORD,,
|