django-display-ids 0.1.2__py3-none-any.whl → 0.1.4__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.
@@ -30,6 +30,12 @@ from .encoding import (
30
30
  encode_display_id,
31
31
  encode_uuid,
32
32
  )
33
+ from .examples import (
34
+ example_display_id,
35
+ example_display_id_for_prefix,
36
+ example_uuid,
37
+ example_uuid_for_prefix,
38
+ )
33
39
  from .exceptions import (
34
40
  AmbiguousIdentifierError,
35
41
  InvalidIdentifierError,
@@ -50,6 +56,11 @@ __all__ = [ # noqa: RUF022 - keep categorized order for readability
50
56
  "decode_uuid",
51
57
  "encode_display_id",
52
58
  "decode_display_id",
59
+ # Examples (for OpenAPI schemas, documentation)
60
+ "example_uuid",
61
+ "example_display_id",
62
+ "example_uuid_for_prefix", # alias
63
+ "example_display_id_for_prefix", # alias
53
64
  # Core resolver
54
65
  "resolve_object",
55
66
  # Errors
@@ -0,0 +1,93 @@
1
+ """drf-spectacular extension for DisplayIDField.
2
+
3
+ This extension auto-registers when drf-spectacular is installed, providing
4
+ proper OpenAPI schema generation for DisplayIDField.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ try:
10
+ from drf_spectacular.extensions import OpenApiSerializerFieldExtension
11
+ except ImportError:
12
+ # drf-spectacular not installed, skip extension registration
13
+ pass
14
+ else:
15
+ from django_display_ids.encoding import ENCODED_UUID_LENGTH, encode_uuid
16
+ from django_display_ids.examples import example_uuid_for_prefix
17
+
18
+ class DisplayIDFieldExtension(OpenApiSerializerFieldExtension):
19
+ """OpenAPI schema extension for DisplayIDField.
20
+
21
+ Generates schema with correct prefix example based on the field's
22
+ configuration or the model's display_id_prefix.
23
+ """
24
+
25
+ target_class = (
26
+ "django_display_ids.contrib.rest_framework.serializers.DisplayIDField"
27
+ )
28
+ match_subclasses = True
29
+
30
+ def _get_model_from_view(self, auto_schema):
31
+ """Try to get model from the view's queryset."""
32
+ if auto_schema is None:
33
+ return None
34
+ view = getattr(auto_schema, "view", None)
35
+ if view is None:
36
+ return None
37
+ # Try get_queryset first
38
+ if hasattr(view, "get_queryset"):
39
+ try:
40
+ queryset = view.get_queryset()
41
+ if hasattr(queryset, "model"):
42
+ return queryset.model
43
+ except Exception:
44
+ pass
45
+ # Try queryset attribute
46
+ queryset = getattr(view, "queryset", None)
47
+ if queryset is not None and hasattr(queryset, "model"):
48
+ return queryset.model
49
+ return None
50
+
51
+ def map_serializer_field(self, auto_schema, direction):
52
+ """Generate OpenAPI schema for DisplayIDField."""
53
+ # Get prefix from field override or try to get from model
54
+ prefix = self.target._prefix_override
55
+
56
+ if prefix is None:
57
+ parent = self.target.parent
58
+ if parent is not None:
59
+ # Try serializer's display_id_prefix attribute first
60
+ prefix = getattr(parent, "display_id_prefix", None)
61
+
62
+ # Then try Meta.model.display_id_prefix
63
+ if prefix is None:
64
+ meta = getattr(parent, "Meta", None)
65
+ model = getattr(meta, "model", None) if meta else None
66
+ if model is not None:
67
+ prefix = getattr(model, "display_id_prefix", None)
68
+
69
+ # Try to get prefix from view's queryset model
70
+ if prefix is None:
71
+ model = self._get_model_from_view(auto_schema)
72
+ if model is not None:
73
+ prefix = getattr(model, "display_id_prefix", None)
74
+
75
+ # Build schema
76
+ if prefix:
77
+ example_uuid = example_uuid_for_prefix(prefix)
78
+ example_encoded = encode_uuid(example_uuid)
79
+ example = f"{prefix}_{example_encoded}"
80
+ description = f"Human-readable identifier with '{prefix}_' prefix"
81
+ else:
82
+ example_uuid = example_uuid_for_prefix("type")
83
+ example_encoded = encode_uuid(example_uuid)
84
+ example = f"type_{example_encoded}"
85
+ description = "Human-readable identifier with type prefix"
86
+
87
+ return {
88
+ "type": "string",
89
+ "description": description,
90
+ "example": example,
91
+ "pattern": f"^[a-z]{{1,16}}_[0-9A-Za-z]{{{ENCODED_UUID_LENGTH}}}$",
92
+ "readOnly": True,
93
+ }
@@ -1,7 +1,48 @@
1
1
  """Django REST Framework integration for django-display-ids."""
2
2
 
3
+ import contextlib
4
+
5
+ from .serializers import DisplayIDField
3
6
  from .views import DisplayIDLookupMixin
4
7
 
5
- __all__ = [
8
+ # Register drf-spectacular extension if available
9
+ # The extension auto-registers when the module is imported
10
+ with contextlib.suppress(ImportError):
11
+ from django_display_ids.contrib import (
12
+ drf_spectacular as _drf_spectacular, # noqa: F401
13
+ )
14
+
15
+ # OpenAPI parameter descriptions for consistent documentation
16
+ ID_PARAM_DESCRIPTION = "Identifier: display_id (prefix_xxx) or UUID"
17
+ ID_PARAM_DESCRIPTION_WITH_SLUG = "Identifier: display_id (prefix_xxx), UUID, or slug"
18
+
19
+
20
+ def id_param_description(prefix: str, *, with_slug: bool = False) -> str:
21
+ """Generate ID parameter description with the actual prefix.
22
+
23
+ Args:
24
+ prefix: The display_id prefix (e.g., "user", "app").
25
+ with_slug: Include slug as an identifier option.
26
+
27
+ Returns:
28
+ Description string for OpenAPI parameter.
29
+
30
+ Example:
31
+ >>> id_param_description("user")
32
+ 'Identifier: display_id (user_xxx) or UUID'
33
+
34
+ >>> id_param_description("app", with_slug=True)
35
+ 'Identifier: display_id (app_xxx), UUID, or slug'
36
+ """
37
+ if with_slug:
38
+ return f"Identifier: display_id ({prefix}_xxx), UUID, or slug"
39
+ return f"Identifier: display_id ({prefix}_xxx) or UUID"
40
+
41
+
42
+ __all__ = [ # noqa: RUF022 - keep logical order for readability
43
+ "DisplayIDField",
6
44
  "DisplayIDLookupMixin",
45
+ "ID_PARAM_DESCRIPTION",
46
+ "ID_PARAM_DESCRIPTION_WITH_SLUG",
47
+ "id_param_description",
7
48
  ]
@@ -0,0 +1,116 @@
1
+ """Django REST Framework serializer fields for display IDs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from rest_framework import serializers
8
+
9
+ from django_display_ids.conf import get_setting
10
+ from django_display_ids.encoding import PREFIX_PATTERN, encode_display_id
11
+
12
+ if TYPE_CHECKING:
13
+ from django.db import models
14
+
15
+ __all__ = [
16
+ "DisplayIDField",
17
+ ]
18
+
19
+
20
+ class DisplayIDField(serializers.SerializerMethodField):
21
+ """Serializer field that returns the display_id from a model.
22
+
23
+ Automatically generates OpenAPI schema with the correct prefix example
24
+ when drf-spectacular is installed.
25
+
26
+ The field reads `display_id_prefix` from the model to determine the prefix.
27
+ If the model has no prefix, the field returns None and can be excluded
28
+ from serialization.
29
+
30
+ Example:
31
+ class UserSerializer(serializers.Serializer):
32
+ id = serializers.UUIDField(source="uid", read_only=True)
33
+ display_id = DisplayIDField()
34
+
35
+ # Output: {"id": "...", "display_id": "user_2nBm7K8xYq1pLwZj"}
36
+
37
+ Example with custom prefix (overrides model's prefix):
38
+ class UserSerializer(serializers.Serializer):
39
+ display_id = DisplayIDField(prefix="usr")
40
+
41
+ Attributes:
42
+ prefix: Optional prefix override. If not set, uses model's
43
+ display_id_prefix attribute.
44
+ """
45
+
46
+ def __init__(self, prefix: str | None = None, **kwargs: Any) -> None:
47
+ """Initialize the field.
48
+
49
+ Args:
50
+ prefix: Optional prefix override. If not set, uses model's
51
+ display_id_prefix attribute.
52
+ **kwargs: Additional arguments passed to SerializerMethodField.
53
+
54
+ Raises:
55
+ ValueError: If prefix is invalid (must be 1-16 lowercase letters).
56
+ """
57
+ if prefix is not None and not PREFIX_PATTERN.match(prefix):
58
+ raise ValueError(f"prefix must be 1-16 lowercase letters, got: {prefix!r}")
59
+ self._prefix_override = prefix
60
+ kwargs["read_only"] = True
61
+ super().__init__(**kwargs)
62
+
63
+ def get_prefix(self, obj: models.Model) -> str | None:
64
+ """Get the prefix for the display ID.
65
+
66
+ Args:
67
+ obj: The model instance.
68
+
69
+ Returns:
70
+ The prefix string or None if not available.
71
+ """
72
+ if self._prefix_override is not None:
73
+ return self._prefix_override
74
+ return getattr(obj, "display_id_prefix", None)
75
+
76
+ def to_representation(self, obj: models.Model) -> str:
77
+ """Return the display_id from the model.
78
+
79
+ Args:
80
+ obj: The model instance.
81
+
82
+ Returns:
83
+ The display_id string.
84
+
85
+ Raises:
86
+ ValueError: If no prefix is available (neither on field nor model).
87
+ """
88
+ prefix = self.get_prefix(obj)
89
+ if prefix is None:
90
+ raise ValueError(
91
+ f"DisplayIDField requires a prefix. Either set prefix= on the "
92
+ f"field or add display_id_prefix to {obj.__class__.__name__}."
93
+ )
94
+
95
+ # If using prefix override, generate display_id with that prefix
96
+ if self._prefix_override is not None:
97
+ # Get uuid_field name from model, then fall back to settings
98
+ uuid_field_name = getattr(obj, "uuid_field", None)
99
+ if uuid_field_name is None:
100
+ uuid_field_name = get_setting("UUID_FIELD")
101
+ uuid_value = getattr(obj, uuid_field_name, None)
102
+ if uuid_value is None:
103
+ raise ValueError(
104
+ f"Cannot generate display_id: {obj.__class__.__name__} "
105
+ f"has no '{uuid_field_name}' field."
106
+ )
107
+ return encode_display_id(prefix, uuid_value)
108
+
109
+ # Use the model's display_id property
110
+ if hasattr(obj, "display_id"):
111
+ return obj.display_id
112
+
113
+ raise ValueError(
114
+ f"Cannot generate display_id: {obj.__class__.__name__} "
115
+ f"has no display_id property."
116
+ )
@@ -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,
@@ -0,0 +1,88 @@
1
+ """Deterministic example generation for display IDs.
2
+
3
+ These functions generate consistent example UUIDs and display IDs based on
4
+ a prefix or model, useful for OpenAPI schema examples and documentation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import uuid
11
+ from typing import TYPE_CHECKING
12
+
13
+ from .encoding import encode_uuid
14
+
15
+ if TYPE_CHECKING:
16
+ from django.db.models import Model
17
+
18
+ __all__ = [
19
+ "example_display_id",
20
+ "example_uuid",
21
+ ]
22
+
23
+
24
+ def _get_prefix(prefix_or_model: str | type[Model]) -> str:
25
+ """Extract prefix from string or model class."""
26
+ if isinstance(prefix_or_model, str):
27
+ return prefix_or_model
28
+ # It's a model class
29
+ prefix = getattr(prefix_or_model, "display_id_prefix", None)
30
+ if prefix is None:
31
+ raise ValueError(f"Model {prefix_or_model.__name__} has no display_id_prefix")
32
+ return prefix
33
+
34
+
35
+ def example_uuid(prefix_or_model: str | type[Model]) -> uuid.UUID:
36
+ """Generate a deterministic UUID from a prefix or model.
37
+
38
+ Uses SHA-256 hash of the prefix to generate a consistent UUID.
39
+ This ensures the same prefix always produces the same example UUID.
40
+
41
+ Args:
42
+ prefix_or_model: Either a display ID prefix string (e.g., "app")
43
+ or a model class with a display_id_prefix attribute.
44
+
45
+ Returns:
46
+ A deterministic UUID based on the prefix.
47
+
48
+ Example:
49
+ >>> example_uuid("app")
50
+ UUID('a172cedc-ae47-474b-615c-54d510a5d84a')
51
+
52
+ >>> example_uuid(App) # Model with display_id_prefix = "app"
53
+ UUID('a172cedc-ae47-474b-615c-54d510a5d84a')
54
+ """
55
+ prefix = _get_prefix(prefix_or_model)
56
+ hash_bytes = hashlib.sha256(prefix.encode()).digest()[:16]
57
+ return uuid.UUID(bytes=hash_bytes)
58
+
59
+
60
+ def example_display_id(prefix_or_model: str | type[Model]) -> str:
61
+ """Generate a deterministic display ID example from a prefix or model.
62
+
63
+ Combines the prefix with a base62-encoded UUID derived from
64
+ the prefix itself.
65
+
66
+ Args:
67
+ prefix_or_model: Either a display ID prefix string (e.g., "app")
68
+ or a model class with a display_id_prefix attribute.
69
+
70
+ Returns:
71
+ A complete display ID example (e.g., "app_4ueEO5Nz4X7u9qc3FVHokM").
72
+
73
+ Example:
74
+ >>> example_display_id("app")
75
+ 'app_4ueEO5Nz4X7u9qc3FVHokM'
76
+
77
+ >>> example_display_id(App) # Model with display_id_prefix = "app"
78
+ 'app_4ueEO5Nz4X7u9qc3FVHokM'
79
+ """
80
+ prefix = _get_prefix(prefix_or_model)
81
+ ex_uuid = example_uuid(prefix)
82
+ encoded = encode_uuid(ex_uuid)
83
+ return f"{prefix}_{encoded}"
84
+
85
+
86
+ # Aliases for backwards compatibility
87
+ example_uuid_for_prefix = example_uuid
88
+ example_display_id_for_prefix = example_display_id
@@ -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.4
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
@@ -120,6 +122,93 @@ class InvoiceView(DisplayIDLookupMixin, APIView):
120
122
  return Response({"id": str(invoice.id)})
121
123
  ```
122
124
 
125
+ #### Serializer Field
126
+
127
+ Include `display_id` in your API responses:
128
+
129
+ ```python
130
+ from rest_framework import serializers
131
+ from django_display_ids.contrib.rest_framework import DisplayIDField
132
+
133
+ class InvoiceSerializer(serializers.Serializer):
134
+ id = serializers.UUIDField(read_only=True)
135
+ display_id = DisplayIDField()
136
+ name = serializers.CharField()
137
+
138
+ # Output: {"id": "...", "display_id": "inv_2aUyqjCzEIiEcYMKj7TZtw", ...}
139
+ ```
140
+
141
+ The field reads `display_id_prefix` from the model. You can override it:
142
+
143
+ ```python
144
+ display_id = DisplayIDField(prefix="inv") # Use custom prefix
145
+ ```
146
+
147
+ Prefix must be 1-16 lowercase letters. Invalid prefixes raise `ValueError` at initialization.
148
+
149
+ **OpenAPI/drf-spectacular**: When drf-spectacular is installed, the field automatically generates proper schema with prefix-specific examples (e.g., `inv_2aUyqjCzEIiEcYMKj7TZtw`). The prefix is resolved from (in order): field's `prefix=` argument, serializer's `Meta.model.display_id_prefix`, or the view's queryset model.
150
+
151
+ #### OpenAPI Parameter Descriptions
152
+
153
+ For consistent API documentation, use the provided description helpers:
154
+
155
+ ```python
156
+ from django_display_ids.contrib.rest_framework import id_param_description
157
+ from drf_spectacular.utils import extend_schema, OpenApiParameter
158
+ from drf_spectacular.types import OpenApiTypes
159
+
160
+ @extend_schema(
161
+ parameters=[
162
+ OpenApiParameter(
163
+ "id",
164
+ OpenApiTypes.STR,
165
+ OpenApiParameter.PATH,
166
+ description=id_param_description("inv"),
167
+ # -> "Identifier: display_id (inv_xxx) or UUID"
168
+ )
169
+ ],
170
+ )
171
+ class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
172
+ ...
173
+ ```
174
+
175
+ For endpoints that also accept slugs:
176
+
177
+ ```python
178
+ description=id_param_description("app", with_slug=True)
179
+ # -> "Identifier: display_id (app_xxx), UUID, or slug"
180
+ ```
181
+
182
+ Generic constants are also available:
183
+
184
+ ```python
185
+ from django_display_ids.contrib.rest_framework import (
186
+ ID_PARAM_DESCRIPTION, # "Identifier: display_id (prefix_xxx) or UUID"
187
+ ID_PARAM_DESCRIPTION_WITH_SLUG, # "Identifier: display_id (prefix_xxx), UUID, or slug"
188
+ )
189
+ ```
190
+
191
+ #### Deterministic Examples for OpenAPI
192
+
193
+ Generate consistent example UUIDs and display IDs for OpenAPI schemas:
194
+
195
+ ```python
196
+ from django_display_ids import example_uuid, example_display_id
197
+
198
+ # Generate deterministic UUID for a prefix
199
+ example_uuid("inv")
200
+ # -> UUID('a172cedc-ae47-474b-615c-54d510a5d84a')
201
+
202
+ # Generate deterministic display ID
203
+ example_display_id("inv")
204
+ # -> "inv_4ueEO5Nz4X7u9qc3FVHokM"
205
+
206
+ # Also works with model classes
207
+ example_uuid(Invoice) # Uses Invoice.display_id_prefix
208
+ ```
209
+
210
+ The same prefix always produces the same example, ensuring consistent documentation across regenerations.
211
+
123
212
  ### Model Mixin
124
213
 
125
214
  Add a `display_id` property to your models:
@@ -249,11 +338,13 @@ The `display_id` strategy requires a prefix. If no prefix is configured, the str
249
338
  |-----------|---------|-------------|
250
339
  | `lookup_param` / `lookup_url_kwarg` | `"pk"` | URL parameter name |
251
340
  | `lookup_strategies` | from settings | Strategies to try |
252
- | `display_id_prefix` | `None` | Expected prefix |
341
+ | `display_id_prefix` | from model | Expected prefix (falls back to model's `display_id_prefix`) |
253
342
  | `uuid_field` | `"id"` | UUID field name on model |
254
343
  | `slug_field` | `"slug"` | Slug field name on model |
255
344
 
256
- ### Django Settings
345
+ ### Django Settings (Optional)
346
+
347
+ All settings have sensible defaults. Only add this if you need to override them:
257
348
 
258
349
  ```python
259
350
  # settings.py
@@ -298,10 +389,16 @@ Run tests:
298
389
  uv run pytest
299
390
  ```
300
391
 
392
+ Run tests with coverage:
393
+
394
+ ```bash
395
+ uv run pytest --cov=src/django_display_ids
396
+ ```
397
+
301
398
  Run tests across Python and Django versions:
302
399
 
303
400
  ```bash
304
- uvx nox -p
401
+ uvx nox
305
402
  ```
306
403
 
307
404
  Lint and format:
@@ -0,0 +1,21 @@
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.1.4.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
20
+ django_display_ids-0.1.4.dist-info/METADATA,sha256=46ob0brsZSBW7QFrmISYvNkD7mQGu3NkIXRe3ueae6g,12356
21
+ django_display_ids-0.1.4.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- django_display_ids/__init__.py,sha256=Iy-JKsXmO4IfF-F9HU4MnGPHLWiixBxXp4ACU2rWrWQ,2334
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/rest_framework/__init__.py,sha256=hBk-6m01T66J5bar3GKwApktOnHhDtK-gUpjghlKoJo,148
6
- django_display_ids/contrib/rest_framework/views.py,sha256=SuUDs4FIyEf8MI2-gTPly8RgL_OshvvRLHGUKXWFexc,5187
7
- django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
8
- django_display_ids/exceptions.py,sha256=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
9
- django_display_ids/managers.py,sha256=EFvlQxsSFXeM8TVvV4NZKeKMC7QB3C0zYGgZ6bvSr4k,6884
10
- django_display_ids/models.py,sha256=bTuD6kMkuoMDH8P9wTgIvTktEnw1VAEFq-NuiTkP_K0,3891
11
- django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- django_display_ids/resolver.py,sha256=oCoA6jbGCFS8SMrkfD_oSSBQNrSxnxdooK5j933eA9Y,2494
13
- django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
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,,