django-display-ids 0.2.0__py3-none-any.whl → 0.3.1__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.
@@ -23,7 +23,10 @@ Example:
23
23
  display_id_prefix = "inv"
24
24
  """
25
25
 
26
+ from typing import Any
27
+
26
28
  from .admin import DisplayIDSearchMixin
29
+ from .converters import DisplayIDConverter, DisplayIDOrUUIDConverter, UUIDConverter
27
30
  from .encoding import (
28
31
  decode_display_id,
29
32
  decode_uuid,
@@ -45,12 +48,29 @@ from .exceptions import (
45
48
  UnknownPrefixError,
46
49
  )
47
50
  from .managers import DisplayIDManager, DisplayIDQuerySet
48
- from .models import DisplayIDMixin, get_model_for_prefix
49
51
  from .resolver import resolve_object
50
52
  from .typing import DEFAULT_STRATEGIES, StrategyName
51
53
  from .views import DisplayIDObjectMixin
52
54
 
55
+
56
+ def __getattr__(name: str) -> Any:
57
+ """Lazy import for model-related items to avoid app registry issues."""
58
+ if name == "DisplayIDMixin":
59
+ from .models import DisplayIDMixin
60
+
61
+ return DisplayIDMixin
62
+ if name == "get_model_for_prefix":
63
+ from .models import get_model_for_prefix
64
+
65
+ return get_model_for_prefix
66
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
67
+
68
+
53
69
  __all__ = [ # noqa: RUF022 - keep categorized order for readability
70
+ # URL converters
71
+ "DisplayIDConverter",
72
+ "UUIDConverter",
73
+ "DisplayIDOrUUIDConverter",
54
74
  # Encoding
55
75
  "encode_uuid",
56
76
  "decode_uuid",
@@ -85,10 +105,8 @@ __all__ = [ # noqa: RUF022 - keep categorized order for readability
85
105
  "DEFAULT_STRATEGIES",
86
106
  ]
87
107
 
88
- __version__ = "0.1.1"
89
-
90
108
 
91
- def get_drf_mixin():
109
+ def get_drf_mixin() -> type:
92
110
  """Lazily import the DRF mixin to avoid hard dependency.
93
111
 
94
112
  Returns:
@@ -4,12 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import contextlib
6
6
  import uuid
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from .encoding import decode_display_id
10
10
 
11
11
  if TYPE_CHECKING:
12
- from django.db.models import QuerySet
12
+ from django.db.models import Model, QuerySet
13
13
  from django.http import HttpRequest
14
14
 
15
15
  __all__ = ["DisplayIDSearchMixin"]
@@ -39,13 +39,15 @@ class DisplayIDSearchMixin:
39
39
  """
40
40
 
41
41
  uuid_field: str | None = None
42
+ model: type[Model]
42
43
 
43
44
  def _get_uuid_field(self) -> str:
44
45
  """Get the UUID field name to search."""
45
46
  if self.uuid_field is not None:
46
47
  return self.uuid_field
47
48
  # Try to get from model's uuid_field attribute
48
- return getattr(self.model, "uuid_field", None) or "id"
49
+ uuid_field: str | None = getattr(self.model, "uuid_field", None)
50
+ return uuid_field or "id"
49
51
 
50
52
  def _try_parse_uuid(self, value: str) -> uuid.UUID | None:
51
53
  """Try to parse a string as a UUID."""
@@ -57,16 +59,16 @@ class DisplayIDSearchMixin:
57
59
  def get_search_results(
58
60
  self,
59
61
  request: HttpRequest,
60
- queryset: QuerySet,
62
+ queryset: QuerySet[Any],
61
63
  search_term: str,
62
- ) -> tuple[QuerySet, bool]:
64
+ ) -> tuple[QuerySet[Any], bool]:
63
65
  """Extend search to handle display IDs and raw UUIDs.
64
66
 
65
67
  Tries to match the search term as:
66
68
  1. A display ID (prefix_base62uuid) if it contains an underscore
67
69
  2. A raw UUID if it looks like a UUID format
68
70
  """
69
- queryset, use_distinct = super().get_search_results(
71
+ queryset, use_distinct = super().get_search_results( # type: ignore[misc]
70
72
  request, queryset, search_term
71
73
  )
72
74
 
@@ -0,0 +1,11 @@
1
+ """Django app configuration for django-display-ids."""
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class DjangoDisplayIdsConfig(AppConfig):
7
+ """App configuration for django-display-ids."""
8
+
9
+ name = "django_display_ids"
10
+ verbose_name = "Django Display IDs"
11
+ default_auto_field = "django.db.models.BigAutoField"
@@ -45,5 +45,8 @@ def get_setting(name: str) -> str | tuple[StrategyName, ...]:
45
45
  if name not in DEFAULTS:
46
46
  raise KeyError(f"Unknown setting: {name}")
47
47
 
48
- user_settings = getattr(settings, "DISPLAY_IDS", {})
49
- return user_settings.get(name, DEFAULTS[name])
48
+ user_settings: dict[str, str | tuple[str, ...]] = getattr(
49
+ settings, "DISPLAY_IDS", {}
50
+ )
51
+ result = user_settings.get(name, DEFAULTS[name])
52
+ return result # type: ignore[return-value]
@@ -6,16 +6,21 @@ proper OpenAPI schema generation for DisplayIDField.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ from typing import TYPE_CHECKING, Any
10
+
9
11
  try:
10
12
  from drf_spectacular.extensions import OpenApiSerializerFieldExtension
11
13
  except ImportError:
12
14
  # drf-spectacular not installed, skip extension registration
13
15
  pass
14
16
  else:
17
+ if TYPE_CHECKING:
18
+ from drf_spectacular.openapi import AutoSchema
19
+
15
20
  from django_display_ids.encoding import ENCODED_UUID_LENGTH, encode_uuid
16
21
  from django_display_ids.examples import example_uuid_for_prefix
17
22
 
18
- class DisplayIDFieldExtension(OpenApiSerializerFieldExtension):
23
+ class DisplayIDFieldExtension(OpenApiSerializerFieldExtension): # type: ignore[no-untyped-call]
19
24
  """OpenAPI schema extension for DisplayIDField.
20
25
 
21
26
  Generates schema with correct prefix example based on the field's
@@ -27,7 +32,7 @@ else:
27
32
  )
28
33
  match_subclasses = True
29
34
 
30
- def _get_model_from_view(self, auto_schema):
35
+ def _get_model_from_view(self, auto_schema: AutoSchema | None) -> Any:
31
36
  """Try to get model from the view's queryset."""
32
37
  if auto_schema is None:
33
38
  return None
@@ -48,7 +53,9 @@ else:
48
53
  return queryset.model
49
54
  return None
50
55
 
51
- def map_serializer_field(self, auto_schema, direction):
56
+ def map_serializer_field(
57
+ self, auto_schema: AutoSchema, direction: str
58
+ ) -> dict[str, Any]:
52
59
  """Generate OpenAPI schema for DisplayIDField."""
53
60
  # Get prefix from field override or try to get from model
54
61
  prefix = self.target._prefix_override
@@ -95,9 +95,9 @@ class DisplayIDField(serializers.SerializerMethodField):
95
95
  # If using prefix override, generate display_id with that prefix
96
96
  if self._prefix_override is not None:
97
97
  # Get uuid_field name from model, then fall back to settings
98
- uuid_field_name = getattr(obj, "uuid_field", None)
98
+ uuid_field_name: str | None = getattr(obj, "uuid_field", None)
99
99
  if uuid_field_name is None:
100
- uuid_field_name = get_setting("UUID_FIELD")
100
+ uuid_field_name = str(get_setting("UUID_FIELD"))
101
101
  uuid_value = getattr(obj, uuid_field_name, None)
102
102
  if uuid_value is None:
103
103
  raise ValueError(
@@ -108,7 +108,8 @@ class DisplayIDField(serializers.SerializerMethodField):
108
108
 
109
109
  # Use the model's display_id property
110
110
  if hasattr(obj, "display_id"):
111
- return obj.display_id
111
+ display_id: str = obj.display_id
112
+ return display_id
112
113
 
113
114
  raise ValueError(
114
115
  f"Cannot generate display_id: {obj.__class__.__name__} "
@@ -174,4 +174,4 @@ class DisplayIDLookupMixin:
174
174
  # Check object-level permissions
175
175
  self.check_object_permissions(self.request, obj)
176
176
 
177
- return obj
177
+ return obj # type: ignore[no-any-return]
@@ -0,0 +1,111 @@
1
+ """Django URL path converters for display IDs and UUIDs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "DisplayIDConverter",
7
+ "DisplayIDOrUUIDConverter",
8
+ "UUIDConverter",
9
+ ]
10
+
11
+
12
+ class DisplayIDConverter:
13
+ """Path converter for display IDs.
14
+
15
+ Matches the format: {prefix}_{base62} where prefix is 1-16 lowercase
16
+ letters and base62 is exactly 22 alphanumeric characters.
17
+
18
+ Example:
19
+ from django.urls import path, register_converter
20
+ from django_display_ids.converters import DisplayIDConverter
21
+
22
+ register_converter(DisplayIDConverter, "display_id")
23
+
24
+ urlpatterns = [
25
+ path("invoices/<display_id:id>/", InvoiceDetailView.as_view()),
26
+ ]
27
+ """
28
+
29
+ regex = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
30
+
31
+ def to_python(self, value: str) -> str:
32
+ """Convert the URL value to a Python object."""
33
+ return value
34
+
35
+ def to_url(self, value: str) -> str:
36
+ """Convert a Python object to a URL string."""
37
+ return value
38
+
39
+
40
+ class UUIDConverter:
41
+ """Path converter for UUIDs.
42
+
43
+ Matches UUIDs in both hyphenated and unhyphenated formats:
44
+ - 550e8400-e29b-41d4-a716-446655440000 (hyphenated)
45
+ - 550e8400e29b41d4a716446655440000 (unhyphenated)
46
+
47
+ Example:
48
+ from django.urls import path, register_converter
49
+ from django_display_ids.converters import UUIDConverter
50
+
51
+ register_converter(UUIDConverter, "uuid")
52
+
53
+ urlpatterns = [
54
+ path("invoices/<uuid:id>/", InvoiceDetailView.as_view()),
55
+ ]
56
+
57
+ Note:
58
+ Django's built-in UUIDConverter only accepts hyphenated UUIDs.
59
+ This converter is more permissive.
60
+ """
61
+
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
+ )
68
+
69
+ def to_python(self, value: str) -> str:
70
+ """Convert the URL value to a Python object."""
71
+ return value
72
+
73
+ def to_url(self, value: str) -> str:
74
+ """Convert a Python object to a URL string."""
75
+ return value
76
+
77
+
78
+ class DisplayIDOrUUIDConverter:
79
+ """Path converter for display IDs or UUIDs.
80
+
81
+ Matches either format:
82
+ - Display ID: {prefix}_{base62}
83
+ - UUID: hyphenated or unhyphenated
84
+
85
+ Example:
86
+ from django.urls import path, register_converter
87
+ from django_display_ids.converters import DisplayIDOrUUIDConverter
88
+
89
+ register_converter(DisplayIDOrUUIDConverter, "display_id_or_uuid")
90
+
91
+ urlpatterns = [
92
+ path("invoices/<display_id_or_uuid:id>/", InvoiceDetailView.as_view()),
93
+ ]
94
+ """
95
+
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
+ )
104
+
105
+ def to_python(self, value: str) -> str:
106
+ """Convert the URL value to a Python object."""
107
+ return value
108
+
109
+ def to_url(self, value: str) -> str:
110
+ """Convert a Python object to a URL string."""
111
+ return value
@@ -26,7 +26,7 @@ def _get_prefix(prefix_or_model: str | type[Model]) -> str:
26
26
  if isinstance(prefix_or_model, str):
27
27
  return prefix_or_model
28
28
  # It's a model class
29
- prefix = getattr(prefix_or_model, "display_id_prefix", None)
29
+ prefix: str | None = getattr(prefix_or_model, "display_id_prefix", None)
30
30
  if prefix is None:
31
31
  raise ValueError(f"Model {prefix_or_model.__name__} has no display_id_prefix")
32
32
  return prefix
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any, TypeVar
6
6
 
7
7
  from django.db import models
8
+ from django.db.models import Q
8
9
 
9
10
  from .conf import get_setting
10
11
  from .encoding import decode_display_id
@@ -18,6 +19,8 @@ from .exceptions import (
18
19
  from .strategies import parse_identifier
19
20
 
20
21
  if TYPE_CHECKING:
22
+ from collections.abc import Sequence
23
+
21
24
  from .typing import StrategyName
22
25
 
23
26
  __all__ = [
@@ -91,7 +94,7 @@ class DisplayIDQuerySet(models.QuerySet[M]):
91
94
  # Query the database
92
95
  try:
93
96
  return self.get(**{uuid_field: uuid_value})
94
- except model.DoesNotExist:
97
+ except model.DoesNotExist: # type: ignore[attr-defined]
95
98
  raise ObjectNotFoundError(value, model_name=model.__name__) from None
96
99
 
97
100
  def get_by_identifier(
@@ -140,22 +143,85 @@ class DisplayIDQuerySet(models.QuerySet[M]):
140
143
  # Execute the query
141
144
  try:
142
145
  return self.get(**lookup)
143
- except model.DoesNotExist:
146
+ except model.DoesNotExist: # type: ignore[attr-defined]
144
147
  raise ObjectNotFoundError(value, model_name=model.__name__) from None
145
- except model.MultipleObjectsReturned:
148
+ except model.MultipleObjectsReturned: # type: ignore[attr-defined]
146
149
  count = self.filter(**lookup).count()
147
150
  raise AmbiguousIdentifierError(value, count) from None
148
151
 
152
+ def get_by_identifiers(
153
+ self,
154
+ values: Sequence[str],
155
+ *,
156
+ strategies: tuple[StrategyName, ...] | None = None,
157
+ prefix: str | None = None,
158
+ ) -> DisplayIDQuerySet[M]:
159
+ """Get multiple objects by any supported identifier type in a single query.
160
+
161
+ Parses each identifier to determine its type (display ID, UUID, or slug),
162
+ then executes a single database query using `__in` lookups.
163
+
164
+ Args:
165
+ values: A sequence of identifier strings (display IDs, UUIDs, or slugs).
166
+ strategies: Strategies to try. Defaults to settings.
167
+ prefix: Expected display ID prefix for validation.
168
+
169
+ Returns:
170
+ A queryset containing matching objects. Order is not guaranteed
171
+ to match input order. Missing identifiers are silently excluded.
172
+
173
+ Raises:
174
+ InvalidIdentifierError: If any identifier cannot be parsed.
175
+
176
+ Example:
177
+ invoices = Invoice.objects.get_by_identifiers([
178
+ 'inv_2aUyqjCzEIiEcYMKj7TZtw',
179
+ 'inv_7kN3xPqRmLwYvTzJ5HfUaB',
180
+ '550e8400-e29b-41d4-a716-446655440000',
181
+ ])
182
+ """
183
+ if not values:
184
+ return self.none()
185
+
186
+ uuid_field = self._get_uuid_field()
187
+ slug_field = self._get_slug_field()
188
+ expected_prefix = prefix or self._get_model_prefix()
189
+ lookup_strategies = strategies or self._get_strategies()
190
+
191
+ # Collect UUIDs and slugs separately
192
+ uuids: list[Any] = []
193
+ slugs: list[str] = []
194
+
195
+ for value in values:
196
+ result = parse_identifier(
197
+ value, lookup_strategies, expected_prefix=expected_prefix
198
+ )
199
+ if result.strategy in ("uuid", "display_id"):
200
+ uuids.append(result.uuid)
201
+ else:
202
+ slugs.append(result.slug) # type: ignore[arg-type]
203
+
204
+ # Build query with OR conditions
205
+ query = Q()
206
+ if uuids:
207
+ query |= Q(**{f"{uuid_field}__in": uuids})
208
+ if slugs:
209
+ query |= Q(**{f"{slug_field}__in": slugs})
210
+
211
+ return self.filter(query)
212
+
149
213
  def _get_uuid_field(self) -> str:
150
214
  """Get the UUID field name for this model."""
151
215
  if hasattr(self.model, "_get_uuid_field"):
152
- return self.model._get_uuid_field() # type: ignore[attr-defined]
216
+ result: str = self.model._get_uuid_field() # type: ignore[attr-defined]
217
+ return result
153
218
  return str(get_setting("UUID_FIELD"))
154
219
 
155
220
  def _get_slug_field(self) -> str:
156
221
  """Get the slug field name for this model."""
157
222
  if hasattr(self.model, "_get_slug_field"):
158
- return self.model._get_slug_field() # type: ignore[attr-defined]
223
+ result: str = self.model._get_slug_field() # type: ignore[attr-defined]
224
+ return result
159
225
  return str(get_setting("SLUG_FIELD"))
160
226
 
161
227
  def _get_strategies(self) -> tuple[StrategyName, ...]:
@@ -166,7 +232,8 @@ class DisplayIDQuerySet(models.QuerySet[M]):
166
232
  """Get the display ID prefix from the model, if defined."""
167
233
  if hasattr(self.model, "get_display_id_prefix"):
168
234
  try:
169
- return self.model.get_display_id_prefix() # type: ignore[attr-defined]
235
+ result: str | None = self.model.get_display_id_prefix() # type: ignore[attr-defined]
236
+ return result
170
237
  except NotImplementedError:
171
238
  return None
172
239
  return None
@@ -210,3 +277,18 @@ class DisplayIDManager(models.Manager[M]):
210
277
  return self.get_queryset().get_by_identifier(
211
278
  value, strategies=strategies, prefix=prefix
212
279
  )
280
+
281
+ def get_by_identifiers(
282
+ self,
283
+ values: Sequence[str],
284
+ *,
285
+ strategies: tuple[StrategyName, ...] | None = None,
286
+ prefix: str | None = None,
287
+ ) -> DisplayIDQuerySet[M]:
288
+ """Get multiple objects by any supported identifier type.
289
+
290
+ See DisplayIDQuerySet.get_by_identifiers for details.
291
+ """
292
+ return self.get_queryset().get_by_identifiers(
293
+ values, strategies=strategies, prefix=prefix
294
+ )
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import ClassVar
5
+ from typing import Any, ClassVar
6
6
 
7
7
  from django.db import models
8
8
 
@@ -84,7 +84,7 @@ class DisplayIDMixin(models.Model):
84
84
  class Meta:
85
85
  abstract = True
86
86
 
87
- def __init_subclass__(cls, **kwargs) -> None:
87
+ def __init_subclass__(cls, **kwargs: Any) -> None:
88
88
  """Register prefix when subclass is created."""
89
89
  super().__init_subclass__(**kwargs)
90
90
 
@@ -125,13 +125,16 @@ 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
137
- display_id.fget.short_description = "Display ID"
140
+ display_id.fget.short_description = "Display ID" # type: ignore[attr-defined]
@@ -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]
@@ -70,8 +79,8 @@ def resolve_object(
70
79
  # Execute the query
71
80
  try:
72
81
  return qs.get(**lookup)
73
- except model.DoesNotExist:
82
+ except model.DoesNotExist: # type: ignore[attr-defined]
74
83
  raise ObjectNotFoundError(value, model_name=model.__name__) from None
75
- except model.MultipleObjectsReturned:
84
+ except model.MultipleObjectsReturned: # type: ignore[attr-defined]
76
85
  count = qs.filter(**lookup).count()
77
86
  raise AmbiguousIdentifierError(value, count) from None
File without changes
@@ -0,0 +1,48 @@
1
+ """Template tags and filters for generating display IDs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+ from django import template
8
+
9
+ from ..encoding import encode_display_id
10
+
11
+ __all__ = [
12
+ "display_id",
13
+ ]
14
+
15
+ register = template.Library()
16
+
17
+
18
+ @register.filter(name="display_id")
19
+ def display_id(value: uuid.UUID | None, prefix: str) -> str:
20
+ """Encode a UUID as a display ID.
21
+
22
+ Usage:
23
+ {{ some_uuid|display_id:"inv" }}
24
+ {{ invoice.customer_id|display_id:"cust" }}
25
+
26
+ Args:
27
+ value: UUID to encode.
28
+ prefix: Display ID prefix (1-16 lowercase letters).
29
+
30
+ Returns:
31
+ Display ID string like "inv_2aUyqjCzEIiEcYMKj7TZtw", or empty string
32
+ if value is None.
33
+
34
+ Raises:
35
+ TemplateSyntaxError: If prefix is invalid or value is not a UUID.
36
+ """
37
+ if value is None:
38
+ return ""
39
+
40
+ if not isinstance(value, uuid.UUID):
41
+ raise template.TemplateSyntaxError(
42
+ f"display_id filter requires a UUID, got {type(value).__name__}"
43
+ )
44
+
45
+ try:
46
+ return encode_display_id(prefix, value)
47
+ except ValueError as e:
48
+ raise template.TemplateSyntaxError(str(e)) from e
@@ -133,7 +133,7 @@ class DisplayIDObjectMixin:
133
133
  qs = queryset if queryset is not None else self.get_queryset()
134
134
 
135
135
  try:
136
- return resolve_object(
136
+ return resolve_object( # type: ignore[no-any-return]
137
137
  model=self.model,
138
138
  value=str(value),
139
139
  strategies=self._get_strategies(),
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-display-ids
3
- Version: 0.2.0
3
+ Version: 0.3.1
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
- License: ISC
6
+ License: MIT
7
7
  Classifier: Development Status :: 4 - Beta
8
8
  Classifier: Framework :: Django
9
9
  Classifier: Framework :: Django :: 4.2
10
10
  Classifier: Framework :: Django :: 5.2
11
11
  Classifier: Framework :: Django :: 6.0
12
- Classifier: License :: OSI Approved :: ISC License (ISCL)
12
+ Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
@@ -26,10 +26,24 @@ Description-Content-Type: text/markdown
26
26
  [![PyPI](https://img.shields.io/pypi/v/django-display-ids)](https://pypi.org/project/django-display-ids/)
27
27
  [![Python](https://img.shields.io/pypi/pyversions/django-display-ids)](https://pypi.org/project/django-display-ids/)
28
28
  [![Django](https://img.shields.io/badge/django-4.2%20%7C%205.2%20%7C%206.0-blue)](https://pypi.org/project/django-display-ids/)
29
+ [![CI](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml/badge.svg)](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml)
30
+ [![codecov](https://codecov.io/gh/josephabrahams/django-display-ids/graph/badge.svg)](https://codecov.io/gh/josephabrahams/django-display-ids)
31
+ [![Docs](https://readthedocs.org/projects/django-display-ids/badge/?version=stable)](https://django-display-ids.readthedocs.io/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/josephabrahams/django-display-ids/blob/main/LICENSE)
29
33
 
30
34
  Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
31
35
 
32
- **Documentation**: [django-display-ids.readthedocs.io](https://django-display-ids.readthedocs.io/)
36
+ ## Why?
37
+
38
+ UUIDv7 (native in Python 3.14+) offers excellent database performance with time-ordered indexing. But they lack context — seeing `550e8400-e29b-41d4-a716-446655440000` in a URL or log doesn't tell you what kind of object it refers to.
39
+
40
+ Display IDs like `inv_2aUyqjCzEIiEcYMKj7TZtw` are more useful: the prefix identifies the object type at a glance, and they're compact and URL-safe. But storing display IDs in the database is far less efficient than native UUIDs.
41
+
42
+ Different consumers have different needs:
43
+ - **Humans** prefer slugs (`my-invoice`) or display IDs (`inv_xxx`)
44
+ - **APIs and integrations** work well with UUIDs
45
+
46
+ This library gives you the best of both worlds: accept any format in your URLs and API endpoints, then translate to an efficient UUID lookup in the database. Store UUIDs, expose whatever format your users need.
33
47
 
34
48
  ## Installation
35
49
 
@@ -37,10 +51,19 @@ Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema cha
37
51
  pip install django-display-ids
38
52
  ```
39
53
 
40
- No `INSTALLED_APPS` entry required.
54
+ Add to `INSTALLED_APPS`:
55
+
56
+ ```python
57
+ INSTALLED_APPS = [
58
+ # ...
59
+ "django_display_ids",
60
+ ]
61
+ ```
41
62
 
42
63
  ## Quick Start
43
64
 
65
+ **Django views:**
66
+
44
67
  ```python
45
68
  from django.views.generic import DetailView
46
69
  from django_display_ids import DisplayIDObjectMixin
@@ -48,53 +71,53 @@ from django_display_ids import DisplayIDObjectMixin
48
71
  class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
49
72
  model = Invoice
50
73
  lookup_param = "id"
51
- lookup_strategies = ("display_id", "uuid")
74
+ lookup_strategies = ("display_id", "uuid", "slug")
52
75
  display_id_prefix = "inv"
53
76
  ```
54
77
 
78
+ **Django REST Framework:**
79
+
55
80
  ```python
56
- # urls.py
57
- urlpatterns = [
58
- path("invoices/<str:id>/", InvoiceDetailView.as_view()),
59
- ]
81
+ from rest_framework.viewsets import ModelViewSet
82
+ from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
83
+
84
+ class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
85
+ queryset = Invoice.objects.all()
86
+ serializer_class = InvoiceSerializer
87
+ lookup_url_kwarg = "id"
88
+ lookup_strategies = ("display_id", "uuid", "slug")
89
+ display_id_prefix = "inv"
60
90
  ```
61
91
 
62
- Now your view accepts:
92
+ Now your views accept:
63
93
  - `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
64
94
  - `550e8400-e29b-41d4-a716-446655440000` (UUID)
95
+ - `my-invoice-slug` (slug)
96
+
97
+ **Templates:**
98
+
99
+ ```django
100
+ {% load display_ids %}
101
+
102
+ {{ invoice.display_id }} {# inv_2aUyqjCzEIiEcYMKj7TZtw #}
103
+ {{ order.customer_id|display_id:"cust" }} {# encode any UUID #}
104
+ ```
65
105
 
66
106
  ## Features
67
107
 
68
108
  - **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
69
109
  - **Framework support**: Django CBVs and Django REST Framework
110
+ - **Template filter**: Encode UUIDs as display IDs in templates
70
111
  - **Zero model changes required**: Works with any existing UUID field
71
112
  - **OpenAPI integration**: Automatic schema generation with drf-spectacular
72
113
 
73
- ## Development
74
-
75
- ```bash
76
- git clone https://github.com/josephabrahams/django-display-ids.git
77
- cd django-display-ids
78
- uv sync
79
- ```
114
+ ## Documentation
80
115
 
81
- Run tests:
116
+ Full documentation at [django-display-ids.readthedocs.io](https://django-display-ids.readthedocs.io/).
82
117
 
83
- ```bash
84
- uv run pytest
85
- ```
118
+ ## Contributing
86
119
 
87
- Run tests across Python and Django versions:
88
-
89
- ```bash
90
- uvx nox
91
- ```
92
-
93
- Lint and format:
94
-
95
- ```bash
96
- uvx pre-commit run --all-files
97
- ```
120
+ See the [contributing guide](https://django-display-ids.readthedocs.io/en/latest/contributing/).
98
121
 
99
122
  ## Related Projects
100
123
 
@@ -105,7 +128,3 @@ If you need ID generation and storage (custom model fields), consider:
105
128
  - **[django-charid-field](https://github.com/yunojuno/django-charid-field)** — CharField wrapper supporting cuid, ksuid, ulid
106
129
 
107
130
  **django-display-ids** works with existing UUID fields and handles resolution only — no migrations required.
108
-
109
- ## License
110
-
111
- ISC
@@ -0,0 +1,25 @@
1
+ django_display_ids/__init__.py,sha256=2qX0YC7Jwpoom5rLOhvOewR3GQEIbUhu5ZYwjfEoklo,3207
2
+ django_display_ids/admin.py,sha256=_voqWbr8AwPRC_uCTJWTcEhAhc7RZUgvs7DyVsutDuw,3046
3
+ django_display_ids/apps.py,sha256=UqblGiYNONOIEH-giEAuKp1YDgxl2yf0jS0ELMj1iig,315
4
+ django_display_ids/conf.py,sha256=Mg5ZIhTRSF5dzNO_iYndoF4_g5Umo-5oUw1vB76M7fw,1230
5
+ django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
6
+ django_display_ids/contrib/drf_spectacular/__init__.py,sha256=21n56CH7tp2lKq4EGW99KVEgVB9fJ5GnfllUW_KrIe8,4003
7
+ django_display_ids/contrib/rest_framework/__init__.py,sha256=Xun6zMhCZzQJZQ7ywvBYoKhTJIZEV84AntrbSWfBjYI,1567
8
+ django_display_ids/contrib/rest_framework/serializers.py,sha256=Jp-z7qHafxkGNYv30YC9rrqLt936SrhONJv3rqfQaC0,4049
9
+ django_display_ids/contrib/rest_framework/views.py,sha256=nDaze7MJwVqL0MrxQLOur3sPVrRXud_WT4ijGR02jDY,6087
10
+ django_display_ids/converters.py,sha256=9xP3vKW1keN1glJvwmZun0BaBRLMKio0aD7Coc888ms,3197
11
+ django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
12
+ django_display_ids/examples.py,sha256=gap5NNPTmE7B5uxiYKoMoK8G-OEtL1Ek0W039l6oJ9I,2689
13
+ django_display_ids/exceptions.py,sha256=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
14
+ django_display_ids/managers.py,sha256=PymcK4BZL6UsUOtoloHP34MCRNmvNHSKEcOImhZxGag,9779
15
+ django_display_ids/models.py,sha256=XI73N4bvxy1Pr2oHeTaJP3uq3huyCX67CFZ2T8mefsA,4317
16
+ django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ django_display_ids/resolver.py,sha256=ZlDVoxX0PmVf0MSwPyiNNwQVzdqJGDGE8fm2iyV7QjE,2848
18
+ django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
19
+ django_display_ids/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ django_display_ids/templatetags/display_ids.py,sha256=4KHE8r8mgSKb7LgIuXJaJB_3UGrzRZvTdLqSCYQtb5I,1157
21
+ django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
22
+ django_display_ids/views.py,sha256=-y_Zwo4QLU0lPRPjABpijsze5vsG0CBvJtrVwVtuLwM,5127
23
+ django_display_ids-0.3.1.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
24
+ django_display_ids-0.3.1.dist-info/METADATA,sha256=eKOjG80Rb_fK-_FQtUYR67fyWGZlN5SzgZuhjD4RGDA,5295
25
+ django_display_ids-0.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.27
2
+ Generator: uv 0.9.28
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,21 +0,0 @@
1
- django_display_ids/__init__.py,sha256=7_rl0cA0LWGIbhg5SbnWAZfKAWyXEpA5XzKDmPnoM8U,2650
2
- django_display_ids/admin.py,sha256=uRyPH3q5e9D5oMxs6PtCHq0syBXs--i1alRoe-EcIJo,2935
3
- django_display_ids/conf.py,sha256=qTsCzKeNBdJpEVeEkx2kFeWHBFa_NZwV_tpt-UTyRR0,1132
4
- django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
5
- django_display_ids/contrib/drf_spectacular/__init__.py,sha256=uPA1foQ8BjyHX3SS9Ta9yI7wp0y3sLdQ4EPOx4KIBlI,3770
6
- django_display_ids/contrib/rest_framework/__init__.py,sha256=Xun6zMhCZzQJZQ7ywvBYoKhTJIZEV84AntrbSWfBjYI,1567
7
- django_display_ids/contrib/rest_framework/serializers.py,sha256=zsYDmXVbra5gUkOnupwJd_vEQnLbgXVWNwehHje2GTs,3991
8
- django_display_ids/contrib/rest_framework/views.py,sha256=mu-twvdRbJedzSfLWVNn2BazFpAQzwRB5Eq3w2bxvvs,6056
9
- django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
10
- django_display_ids/examples.py,sha256=rrZgL-EoWz0mC32T-RNWCo45Y8_BXV_6r_5tRIz77gs,2677
11
- django_display_ids/exceptions.py,sha256=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
12
- django_display_ids/managers.py,sha256=EFvlQxsSFXeM8TVvV4NZKeKMC7QB3C0zYGgZ6bvSr4k,6884
13
- django_display_ids/models.py,sha256=_IXxaFlVw2MqobQUw4Cy4rB66LsTz6kiI5YpoSpnkCY,4156
14
- django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- django_display_ids/resolver.py,sha256=oCoA6jbGCFS8SMrkfD_oSSBQNrSxnxdooK5j933eA9Y,2494
16
- django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
17
- django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
18
- django_display_ids/views.py,sha256=sLsJm8Tpe3Qk1gOLcDzfpazxuaVqTCAdgVIXOONFnKQ,5096
19
- django_display_ids-0.2.0.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
20
- django_display_ids-0.2.0.dist-info/METADATA,sha256=cXxwTVjdSXtYv0ghEid7n-udEjrdZrZWhbEWSkvFr6M,3342
21
- django_display_ids-0.2.0.dist-info/RECORD,,