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.
- django_display_ids/__init__.py +11 -0
- django_display_ids/contrib/drf_spectacular/__init__.py +93 -0
- django_display_ids/contrib/rest_framework/__init__.py +42 -1
- django_display_ids/contrib/rest_framework/serializers.py +116 -0
- django_display_ids/examples.py +88 -0
- django_display_ids-0.2.0.dist-info/METADATA +111 -0
- {django_display_ids-0.1.3.dist-info → django_display_ids-0.2.0.dist-info}/RECORD +8 -5
- {django_display_ids-0.1.3.dist-info → django_display_ids-0.2.0.dist-info}/WHEEL +1 -1
- django_display_ids-0.1.3.dist-info/METADATA +0 -335
django_display_ids/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
27
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
28
|
+
[](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=
|
|
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/
|
|
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.
|
|
17
|
-
django_display_ids-0.
|
|
18
|
-
django_display_ids-0.
|
|
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,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
|