django-display-ids 0.1.3__py3-none-any.whl → 0.2.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.
@@ -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
+ )
@@ -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
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.3
2
+ Name: django-display-ids
3
+ Version: 0.2.0
4
+ Summary: Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
5
+ Keywords: django,stripe,uuid,base62,prefixed-id,drf,shortuuid,nanoid,ulid
6
+ License: ISC
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Framework :: Django
9
+ Classifier: Framework :: Django :: 4.2
10
+ Classifier: Framework :: Django :: 5.2
11
+ Classifier: Framework :: Django :: 6.0
12
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: django>=4.2
19
+ Requires-Python: >=3.12
20
+ Project-URL: Documentation, https://django-display-ids.readthedocs.io/
21
+ Project-URL: Repository, https://github.com/josephabrahams/django-display-ids
22
+ Description-Content-Type: text/markdown
23
+
24
+ # django-display-ids
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/django-display-ids)](https://pypi.org/project/django-display-ids/)
27
+ [![Python](https://img.shields.io/pypi/pyversions/django-display-ids)](https://pypi.org/project/django-display-ids/)
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
+
30
+ Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
31
+
32
+ **Documentation**: [django-display-ids.readthedocs.io](https://django-display-ids.readthedocs.io/)
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install django-display-ids
38
+ ```
39
+
40
+ No `INSTALLED_APPS` entry required.
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ from django.views.generic import DetailView
46
+ from django_display_ids import DisplayIDObjectMixin
47
+
48
+ class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
49
+ model = Invoice
50
+ lookup_param = "id"
51
+ lookup_strategies = ("display_id", "uuid")
52
+ display_id_prefix = "inv"
53
+ ```
54
+
55
+ ```python
56
+ # urls.py
57
+ urlpatterns = [
58
+ path("invoices/<str:id>/", InvoiceDetailView.as_view()),
59
+ ]
60
+ ```
61
+
62
+ Now your view accepts:
63
+ - `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
64
+ - `550e8400-e29b-41d4-a716-446655440000` (UUID)
65
+
66
+ ## Features
67
+
68
+ - **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
69
+ - **Framework support**: Django CBVs and Django REST Framework
70
+ - **Zero model changes required**: Works with any existing UUID field
71
+ - **OpenAPI integration**: Automatic schema generation with drf-spectacular
72
+
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
+ ```
80
+
81
+ Run tests:
82
+
83
+ ```bash
84
+ uv run pytest
85
+ ```
86
+
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
+ ```
98
+
99
+ ## Related Projects
100
+
101
+ If you need ID generation and storage (custom model fields), consider:
102
+
103
+ - **[django-prefix-id](https://github.com/jaddison/django-prefix-id)** — PrefixIDField that generates and stores base62-encoded UUIDs
104
+ - **[django-spicy-id](https://github.com/mik3y/django-spicy-id)** — Drop-in AutoField replacement
105
+ - **[django-charid-field](https://github.com/yunojuno/django-charid-field)** — CharField wrapper supporting cuid, ksuid, ulid
106
+
107
+ **django-display-ids** works with existing UUID fields and handles resolution only — no migrations required.
108
+
109
+ ## License
110
+
111
+ ISC
@@ -1,10 +1,13 @@
1
- django_display_ids/__init__.py,sha256=Iy-JKsXmO4IfF-F9HU4MnGPHLWiixBxXp4ACU2rWrWQ,2334
1
+ django_display_ids/__init__.py,sha256=7_rl0cA0LWGIbhg5SbnWAZfKAWyXEpA5XzKDmPnoM8U,2650
2
2
  django_display_ids/admin.py,sha256=uRyPH3q5e9D5oMxs6PtCHq0syBXs--i1alRoe-EcIJo,2935
3
3
  django_display_ids/conf.py,sha256=qTsCzKeNBdJpEVeEkx2kFeWHBFa_NZwV_tpt-UTyRR0,1132
4
4
  django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
5
- django_display_ids/contrib/rest_framework/__init__.py,sha256=hBk-6m01T66J5bar3GKwApktOnHhDtK-gUpjghlKoJo,148
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
6
8
  django_display_ids/contrib/rest_framework/views.py,sha256=mu-twvdRbJedzSfLWVNn2BazFpAQzwRB5Eq3w2bxvvs,6056
7
9
  django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
10
+ django_display_ids/examples.py,sha256=rrZgL-EoWz0mC32T-RNWCo45Y8_BXV_6r_5tRIz77gs,2677
8
11
  django_display_ids/exceptions.py,sha256=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
9
12
  django_display_ids/managers.py,sha256=EFvlQxsSFXeM8TVvV4NZKeKMC7QB3C0zYGgZ6bvSr4k,6884
10
13
  django_display_ids/models.py,sha256=_IXxaFlVw2MqobQUw4Cy4rB66LsTz6kiI5YpoSpnkCY,4156
@@ -13,6 +16,6 @@ django_display_ids/resolver.py,sha256=oCoA6jbGCFS8SMrkfD_oSSBQNrSxnxdooK5j933eA9
13
16
  django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
14
17
  django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
15
18
  django_display_ids/views.py,sha256=sLsJm8Tpe3Qk1gOLcDzfpazxuaVqTCAdgVIXOONFnKQ,5096
16
- django_display_ids-0.1.3.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
17
- django_display_ids-0.1.3.dist-info/METADATA,sha256=AjMU5ctNag1QVDINfx4gggW0VWfYkkIEE1brlokAUqo,9649
18
- django_display_ids-0.1.3.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.27
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,335 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: django-display-ids
3
- Version: 0.1.3
4
- Summary: Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
5
- Keywords: django,stripe,uuid,base62,prefixed-id,drf,shortuuid,nanoid,ulid
6
- License: ISC
7
- Classifier: Development Status :: 4 - Beta
8
- Classifier: Framework :: Django
9
- Classifier: Framework :: Django :: 4.2
10
- Classifier: Framework :: Django :: 5.2
11
- Classifier: Framework :: Django :: 6.0
12
- Classifier: License :: OSI Approved :: ISC License (ISCL)
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Programming Language :: Python :: 3.14
17
- Classifier: Typing :: Typed
18
- Requires-Dist: django>=4.2
19
- Requires-Python: >=3.12
20
- Project-URL: Homepage, https://joseph.is/django-display-ids
21
- Description-Content-Type: text/markdown
22
-
23
- # django-display-ids
24
-
25
- Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
26
-
27
- Display IDs are human-friendly identifiers like `inv_2aUyqjCzEIiEcYMKj7TZtw` — a short prefix indicating the object type, followed by a base62-encoded UUID. This format, popularized by Stripe, makes IDs recognizable at a glance while remaining URL-safe and compact.
28
-
29
- This library focuses on **lookup only** — it works with your existing UUID fields and requires no migrations or schema changes.
30
-
31
- ## Installation
32
-
33
- ```bash
34
- pip install django-display-ids
35
- ```
36
-
37
- No `INSTALLED_APPS` entry required — just import and use.
38
-
39
- ## Quick Start
40
-
41
- ```python
42
- from django.views.generic import DetailView
43
- from django_display_ids import DisplayIDObjectMixin
44
-
45
- class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
46
- model = Invoice
47
- lookup_param = "id"
48
- lookup_strategies = ("display_id", "uuid")
49
- display_id_prefix = "inv"
50
- ```
51
-
52
- ```python
53
- # urls.py
54
- urlpatterns = [
55
- path("invoices/<str:id>/", InvoiceDetailView.as_view()),
56
- ]
57
- ```
58
-
59
- Now your view accepts both formats:
60
- - `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
61
- - `550e8400-e29b-41d4-a716-446655440000` (UUID)
62
-
63
- ## Features
64
-
65
- - **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
66
- - **Framework support**: Django CBVs and Django REST Framework
67
- - **Zero model changes required**: Works with any existing UUID field
68
- - **Stateless**: Pure lookup, no database writes
69
-
70
- ## Usage
71
-
72
- ### Django Class-Based Views
73
-
74
- ```python
75
- from django.views.generic import DetailView, UpdateView, DeleteView
76
- from django_display_ids import DisplayIDObjectMixin
77
-
78
- class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
79
- model = Invoice
80
- lookup_param = "id"
81
- lookup_strategies = ("display_id", "uuid")
82
- display_id_prefix = "inv"
83
-
84
- # Works with any view that uses get_object()
85
- class InvoiceUpdateView(DisplayIDObjectMixin, UpdateView):
86
- model = Invoice
87
- lookup_param = "id"
88
- display_id_prefix = "inv"
89
- ```
90
-
91
- ### Django REST Framework
92
-
93
- ```python
94
- from rest_framework.viewsets import ModelViewSet
95
- from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
96
-
97
- class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
98
- queryset = Invoice.objects.all()
99
- serializer_class = InvoiceSerializer
100
- lookup_url_kwarg = "id"
101
- lookup_strategies = ("display_id", "uuid")
102
- display_id_prefix = "inv"
103
- ```
104
-
105
- Or with APIView:
106
-
107
- ```python
108
- from rest_framework.views import APIView
109
- from rest_framework.response import Response
110
- from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
111
-
112
- class InvoiceView(DisplayIDLookupMixin, APIView):
113
- lookup_url_kwarg = "id"
114
- lookup_strategies = ("display_id", "uuid")
115
- display_id_prefix = "inv"
116
-
117
- def get_queryset(self):
118
- return Invoice.objects.all()
119
-
120
- def get(self, request, *args, **kwargs):
121
- invoice = self.get_object()
122
- return Response({"id": str(invoice.id)})
123
- ```
124
-
125
- ### Model Mixin
126
-
127
- Add a `display_id` property to your models:
128
-
129
- ```python
130
- import uuid
131
- from django.db import models
132
- from django_display_ids import DisplayIDMixin
133
-
134
- class Invoice(DisplayIDMixin, models.Model):
135
- display_id_prefix = "inv"
136
- id = models.UUIDField(primary_key=True, default=uuid.uuid4)
137
-
138
- invoice = Invoice.objects.first()
139
- invoice.display_id # -> "inv_2aUyqjCzEIiEcYMKj7TZtw"
140
- ```
141
-
142
- ### Model Manager
143
-
144
- ```python
145
- from django_display_ids import DisplayIDMixin, DisplayIDManager
146
-
147
- class Invoice(DisplayIDMixin, models.Model):
148
- display_id_prefix = "inv"
149
- objects = DisplayIDManager()
150
- id = models.UUIDField(primary_key=True, default=uuid.uuid4)
151
-
152
- # Get by display ID
153
- invoice = Invoice.objects.get_by_display_id("inv_2aUyqjCzEIiEcYMKj7TZtw")
154
-
155
- # Get by any identifier type
156
- invoice = Invoice.objects.get_by_identifier("inv_2aUyqjCzEIiEcYMKj7TZtw")
157
- invoice = Invoice.objects.get_by_identifier("550e8400-e29b-41d4-a716-446655440000")
158
-
159
- # Works with filtered querysets
160
- invoice = Invoice.objects.filter(active=True).get_by_identifier("inv_xxx")
161
- ```
162
-
163
- ### Django Admin
164
-
165
- Enable searching by display ID or raw UUID in the admin:
166
-
167
- ```python
168
- from django.contrib import admin
169
- from django_display_ids import DisplayIDSearchMixin
170
-
171
- @admin.register(Invoice)
172
- class InvoiceAdmin(DisplayIDSearchMixin, admin.ModelAdmin):
173
- list_display = ["id", "display_id", "name", "created"]
174
- search_fields = ["name"] # display_id/UUID search is automatic
175
- ```
176
-
177
- Now you can search by either format in the admin search box:
178
- - `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
179
- - `550e8400-e29b-41d4-a716-446655440000` (raw UUID from logs)
180
-
181
- The mixin automatically detects the UUID field from your model's `uuid_field`
182
- attribute (if using `DisplayIDMixin`), or defaults to `id`. Override with:
183
-
184
- ```python
185
- class InvoiceAdmin(DisplayIDSearchMixin, admin.ModelAdmin):
186
- uuid_field = "uid" # custom UUID field name
187
- ```
188
-
189
- ### Encoding and Decoding
190
-
191
- ```python
192
- import uuid
193
- from django_display_ids import encode_display_id, decode_display_id
194
-
195
- # Create a display ID from a UUID
196
- invoice_id = uuid.uuid4()
197
- display_id = encode_display_id("inv", invoice_id)
198
- # -> "inv_2aUyqjCzEIiEcYMKj7TZtw"
199
-
200
- # Decode back to prefix and UUID
201
- prefix, decoded_uuid = decode_display_id(display_id)
202
- ```
203
-
204
- ### Direct Resolution
205
-
206
- ```python
207
- from django_display_ids import resolve_object
208
-
209
- invoice = resolve_object(
210
- model=Invoice,
211
- value="inv_2aUyqjCzEIiEcYMKj7TZtw",
212
- strategies=("display_id", "uuid", "slug"),
213
- prefix="inv",
214
- )
215
- ```
216
-
217
- ## Identifier Formats
218
-
219
- | Format | Example | Description |
220
- |--------|---------|-------------|
221
- | Display ID | `inv_2aUyqjCzEIiEcYMKj7TZtw` | Prefix + base62-encoded UUID |
222
- | UUID | `550e8400-e29b-41d4-a716-446655440000` | Standard UUID (v4/v7) |
223
- | Slug | `my-invoice-slug` | Human-readable identifier |
224
-
225
- Display ID format:
226
- - Prefix: 1-16 lowercase letters
227
- - Separator: underscore
228
- - Encoded UUID: 22 base62 characters (fixed length)
229
-
230
- ## Lookup Strategies
231
-
232
- Strategies are tried in order. The first successful match is returned.
233
-
234
- | Strategy | Description |
235
- |----------|-------------|
236
- | `display_id` | Decode display ID, lookup by UUID field |
237
- | `uuid` | Parse as UUID, lookup by UUID field |
238
- | `slug` | Lookup by slug field |
239
-
240
- Default: `("display_id", "uuid")`
241
-
242
- The slug strategy is a catch-all, so it should always be last.
243
-
244
- The `display_id` strategy requires a prefix. If no prefix is configured, the strategy is skipped.
245
-
246
- ## Configuration
247
-
248
- ### View/Mixin Attributes
249
-
250
- | Attribute | Default | Description |
251
- |-----------|---------|-------------|
252
- | `lookup_param` / `lookup_url_kwarg` | `"pk"` | URL parameter name |
253
- | `lookup_strategies` | from settings | Strategies to try |
254
- | `display_id_prefix` | from model | Expected prefix (falls back to model's `display_id_prefix`) |
255
- | `uuid_field` | `"id"` | UUID field name on model |
256
- | `slug_field` | `"slug"` | Slug field name on model |
257
-
258
- ### Django Settings (Optional)
259
-
260
- All settings have sensible defaults. Only add this if you need to override them:
261
-
262
- ```python
263
- # settings.py
264
- DISPLAY_IDS = {
265
- "UUID_FIELD": "id", # default
266
- "SLUG_FIELD": "slug", # default
267
- "STRATEGIES": ("display_id", "uuid"), # default
268
- }
269
- ```
270
-
271
- ## Error Handling
272
-
273
- | Exception | When Raised |
274
- |-----------|-------------|
275
- | `InvalidIdentifierError` | Identifier cannot be parsed |
276
- | `UnknownPrefixError` | Display ID prefix doesn't match expected |
277
- | `ObjectNotFoundError` | No matching database record |
278
-
279
- In views, errors are converted to HTTP responses:
280
- - Django CBV: `Http404`
281
- - DRF: `NotFound` (404) or `ParseError` (400)
282
-
283
- ## Requirements
284
-
285
- - Python 3.12+
286
- - Django 4.2+
287
- - Django REST Framework 3.14+ (optional)
288
-
289
- ## Development
290
-
291
- Clone the repository and install dependencies:
292
-
293
- ```bash
294
- git clone https://github.com/josephabrahams/django-display-ids.git
295
- cd django-display-ids
296
- uv sync
297
- ```
298
-
299
- Run tests:
300
-
301
- ```bash
302
- uv run pytest
303
- ```
304
-
305
- Run tests with coverage:
306
-
307
- ```bash
308
- uv run pytest --cov=src/django_display_ids
309
- ```
310
-
311
- Run tests across Python and Django versions:
312
-
313
- ```bash
314
- uvx nox
315
- ```
316
-
317
- Lint and format:
318
-
319
- ```bash
320
- uvx pre-commit run --all-files
321
- ```
322
-
323
- ## Related Projects
324
-
325
- If you need ID generation and storage (custom model fields), consider these alternatives:
326
-
327
- - **[django-prefix-id](https://github.com/jaddison/django-prefix-id)** — PrefixIDField that generates and stores base62-encoded UUIDs
328
- - **[django-spicy-id](https://github.com/mik3y/django-spicy-id)** — Drop-in AutoField replacement that displays numeric IDs as prefixed strings
329
- - **[django-charid-field](https://github.com/yunojuno/django-charid-field)** — CharField wrapper supporting cuid, ksuid, ulid, and other generators
330
-
331
- **django-display-ids** takes a different approach: it works with your existing UUID fields and handles resolution only. No migrations, no schema changes — just add the mixin to your views.
332
-
333
- ## License
334
-
335
- ISC