django-display-ids 0.1.4__tar.gz → 0.3.0__tar.gz

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.
Files changed (27) hide show
  1. django_display_ids-0.3.0/PKG-INFO +130 -0
  2. django_display_ids-0.3.0/README.md +107 -0
  3. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/pyproject.toml +28 -6
  4. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/__init__.py +22 -4
  5. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/admin.py +8 -6
  6. django_display_ids-0.3.0/src/django_display_ids/apps.py +11 -0
  7. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/conf.py +5 -2
  8. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/drf_spectacular/__init__.py +10 -3
  9. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/rest_framework/serializers.py +4 -3
  10. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/rest_framework/views.py +1 -1
  11. django_display_ids-0.3.0/src/django_display_ids/converters.py +111 -0
  12. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/examples.py +1 -1
  13. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/managers.py +88 -6
  14. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/models.py +3 -3
  15. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/resolver.py +2 -2
  16. django_display_ids-0.3.0/src/django_display_ids/templatetags/__init__.py +0 -0
  17. django_display_ids-0.3.0/src/django_display_ids/templatetags/display_ids.py +48 -0
  18. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/views.py +1 -1
  19. django_display_ids-0.1.4/PKG-INFO +0 -422
  20. django_display_ids-0.1.4/README.md +0 -400
  21. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/__init__.py +0 -0
  22. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/rest_framework/__init__.py +0 -0
  23. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/encoding.py +0 -0
  24. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/exceptions.py +0 -0
  25. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/py.typed +0 -0
  26. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/strategies.py +0 -0
  27. {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/typing.py +0 -0
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.3
2
+ Name: django-display-ids
3
+ Version: 0.3.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: MIT
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 :: MIT License
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
+ [![CI](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml/badge.svg)](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml)
30
+ [![codecov](https://codecov.io/gh/josephabrahams/django-display-ids/graph/badge.svg)](https://codecov.io/gh/josephabrahams/django-display-ids)
31
+ [![Docs](https://readthedocs.org/projects/django-display-ids/badge/?version=stable)](https://django-display-ids.readthedocs.io/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/josephabrahams/django-display-ids/blob/main/LICENSE)
33
+
34
+ Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
35
+
36
+ ## Why?
37
+
38
+ UUIDv7 (native in Python 3.14+) offers excellent database performance with time-ordered indexing. But they lack context — seeing `550e8400-e29b-41d4-a716-446655440000` in a URL or log doesn't tell you what kind of object it refers to.
39
+
40
+ Display IDs like `inv_2aUyqjCzEIiEcYMKj7TZtw` are more useful: the prefix identifies the object type at a glance, and they're compact and URL-safe. But storing display IDs in the database is far less efficient than native UUIDs.
41
+
42
+ Different consumers have different needs:
43
+ - **Humans** prefer slugs (`my-invoice`) or display IDs (`inv_xxx`)
44
+ - **APIs and integrations** work well with UUIDs
45
+
46
+ This library gives you the best of both worlds: accept any format in your URLs and API endpoints, then translate to an efficient UUID lookup in the database. Store UUIDs, expose whatever format your users need.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install django-display-ids
52
+ ```
53
+
54
+ Add to `INSTALLED_APPS`:
55
+
56
+ ```python
57
+ INSTALLED_APPS = [
58
+ # ...
59
+ "django_display_ids",
60
+ ]
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ **Django views:**
66
+
67
+ ```python
68
+ from django.views.generic import DetailView
69
+ from django_display_ids import DisplayIDObjectMixin
70
+
71
+ class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
72
+ model = Invoice
73
+ lookup_param = "id"
74
+ lookup_strategies = ("display_id", "uuid", "slug")
75
+ display_id_prefix = "inv"
76
+ ```
77
+
78
+ **Django REST Framework:**
79
+
80
+ ```python
81
+ from rest_framework.viewsets import ModelViewSet
82
+ from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
83
+
84
+ class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
85
+ queryset = Invoice.objects.all()
86
+ serializer_class = InvoiceSerializer
87
+ lookup_url_kwarg = "id"
88
+ lookup_strategies = ("display_id", "uuid", "slug")
89
+ display_id_prefix = "inv"
90
+ ```
91
+
92
+ Now your views accept:
93
+ - `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
94
+ - `550e8400-e29b-41d4-a716-446655440000` (UUID)
95
+ - `my-invoice-slug` (slug)
96
+
97
+ **Templates:**
98
+
99
+ ```django
100
+ {% load display_ids %}
101
+
102
+ {{ invoice.display_id }} {# inv_2aUyqjCzEIiEcYMKj7TZtw #}
103
+ {{ order.customer_id|display_id:"cust" }} {# encode any UUID #}
104
+ ```
105
+
106
+ ## Features
107
+
108
+ - **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
109
+ - **Framework support**: Django CBVs and Django REST Framework
110
+ - **Template filter**: Encode UUIDs as display IDs in templates
111
+ - **Zero model changes required**: Works with any existing UUID field
112
+ - **OpenAPI integration**: Automatic schema generation with drf-spectacular
113
+
114
+ ## Documentation
115
+
116
+ Full documentation at [django-display-ids.readthedocs.io](https://django-display-ids.readthedocs.io/).
117
+
118
+ ## Contributing
119
+
120
+ See the [contributing guide](https://django-display-ids.readthedocs.io/en/latest/contributing.html).
121
+
122
+ ## Related Projects
123
+
124
+ If you need ID generation and storage (custom model fields), consider:
125
+
126
+ - **[django-prefix-id](https://github.com/jaddison/django-prefix-id)** — PrefixIDField that generates and stores base62-encoded UUIDs
127
+ - **[django-spicy-id](https://github.com/mik3y/django-spicy-id)** — Drop-in AutoField replacement
128
+ - **[django-charid-field](https://github.com/yunojuno/django-charid-field)** — CharField wrapper supporting cuid, ksuid, ulid
129
+
130
+ **django-display-ids** works with existing UUID fields and handles resolution only — no migrations required.
@@ -0,0 +1,107 @@
1
+ # django-display-ids
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/django-display-ids)](https://pypi.org/project/django-display-ids/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/django-display-ids)](https://pypi.org/project/django-display-ids/)
5
+ [![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/)
6
+ [![CI](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml/badge.svg)](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml)
7
+ [![codecov](https://codecov.io/gh/josephabrahams/django-display-ids/graph/badge.svg)](https://codecov.io/gh/josephabrahams/django-display-ids)
8
+ [![Docs](https://readthedocs.org/projects/django-display-ids/badge/?version=stable)](https://django-display-ids.readthedocs.io/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/josephabrahams/django-display-ids/blob/main/LICENSE)
10
+
11
+ Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
12
+
13
+ ## Why?
14
+
15
+ UUIDv7 (native in Python 3.14+) offers excellent database performance with time-ordered indexing. But they lack context — seeing `550e8400-e29b-41d4-a716-446655440000` in a URL or log doesn't tell you what kind of object it refers to.
16
+
17
+ Display IDs like `inv_2aUyqjCzEIiEcYMKj7TZtw` are more useful: the prefix identifies the object type at a glance, and they're compact and URL-safe. But storing display IDs in the database is far less efficient than native UUIDs.
18
+
19
+ Different consumers have different needs:
20
+ - **Humans** prefer slugs (`my-invoice`) or display IDs (`inv_xxx`)
21
+ - **APIs and integrations** work well with UUIDs
22
+
23
+ This library gives you the best of both worlds: accept any format in your URLs and API endpoints, then translate to an efficient UUID lookup in the database. Store UUIDs, expose whatever format your users need.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install django-display-ids
29
+ ```
30
+
31
+ Add to `INSTALLED_APPS`:
32
+
33
+ ```python
34
+ INSTALLED_APPS = [
35
+ # ...
36
+ "django_display_ids",
37
+ ]
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ **Django views:**
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", "slug")
52
+ display_id_prefix = "inv"
53
+ ```
54
+
55
+ **Django REST Framework:**
56
+
57
+ ```python
58
+ from rest_framework.viewsets import ModelViewSet
59
+ from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
60
+
61
+ class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
62
+ queryset = Invoice.objects.all()
63
+ serializer_class = InvoiceSerializer
64
+ lookup_url_kwarg = "id"
65
+ lookup_strategies = ("display_id", "uuid", "slug")
66
+ display_id_prefix = "inv"
67
+ ```
68
+
69
+ Now your views accept:
70
+ - `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
71
+ - `550e8400-e29b-41d4-a716-446655440000` (UUID)
72
+ - `my-invoice-slug` (slug)
73
+
74
+ **Templates:**
75
+
76
+ ```django
77
+ {% load display_ids %}
78
+
79
+ {{ invoice.display_id }} {# inv_2aUyqjCzEIiEcYMKj7TZtw #}
80
+ {{ order.customer_id|display_id:"cust" }} {# encode any UUID #}
81
+ ```
82
+
83
+ ## Features
84
+
85
+ - **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
86
+ - **Framework support**: Django CBVs and Django REST Framework
87
+ - **Template filter**: Encode UUIDs as display IDs in templates
88
+ - **Zero model changes required**: Works with any existing UUID field
89
+ - **OpenAPI integration**: Automatic schema generation with drf-spectacular
90
+
91
+ ## Documentation
92
+
93
+ Full documentation at [django-display-ids.readthedocs.io](https://django-display-ids.readthedocs.io/).
94
+
95
+ ## Contributing
96
+
97
+ See the [contributing guide](https://django-display-ids.readthedocs.io/en/latest/contributing.html).
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.
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "django-display-ids"
3
- version = "0.1.4"
3
+ version = "0.3.0"
4
4
  description = "Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
- license = { text = "ISC" }
7
+ license = { text = "MIT" }
8
8
  keywords = [
9
9
  "django",
10
10
  "stripe",
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Framework :: Django :: 4.2",
23
23
  "Framework :: Django :: 5.2",
24
24
  "Framework :: Django :: 6.0",
25
- "License :: OSI Approved :: ISC License (ISCL)",
25
+ "License :: OSI Approved :: MIT License",
26
26
  "Programming Language :: Python :: 3",
27
27
  "Programming Language :: Python :: 3.12",
28
28
  "Programming Language :: Python :: 3.13",
@@ -34,7 +34,8 @@ dependencies = [
34
34
  ]
35
35
 
36
36
  [project.urls]
37
- Homepage = "https://joseph.is/django-display-ids"
37
+ Documentation = "https://django-display-ids.readthedocs.io/"
38
+ Repository = "https://github.com/josephabrahams/django-display-ids"
38
39
 
39
40
  [dependency-groups]
40
41
  dev = [
@@ -47,14 +48,32 @@ dev = [
47
48
  "nox>=2024.0",
48
49
  "ruff>=0.8",
49
50
  "pre-commit>=4.0",
51
+ "mypy>=1.13",
52
+ "django-stubs>=5.1",
53
+ "djangorestframework-stubs>=3.15",
54
+ "sphinx-autobuild",
55
+ ]
56
+ docs = [
57
+ "sphinx>=7.0",
58
+ "furo",
59
+ "sphinx-copybutton",
50
60
  ]
51
61
 
52
62
  [build-system]
53
63
  requires = ["uv_build>=0.9.26,<0.10.0"]
54
64
  build-backend = "uv_build"
55
65
 
56
- [tool.uv]
57
- default-groups = ["dev"]
66
+ [tool.django-stubs]
67
+ django_settings_module = "tests.settings"
68
+
69
+ [tool.mypy]
70
+ python_version = "3.12"
71
+ plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"]
72
+ strict = true
73
+
74
+ [[tool.mypy.overrides]]
75
+ module = "tests.*"
76
+ disable_error_code = ["no-untyped-def"]
58
77
 
59
78
  [tool.pytest.ini_options]
60
79
  DJANGO_SETTINGS_MODULE = "tests.settings"
@@ -93,3 +112,6 @@ known-first-party = ["django_display_ids"]
93
112
  [tool.ruff.format]
94
113
  quote-style = "double"
95
114
  indent-style = "space"
115
+
116
+ [tool.uv]
117
+ default-groups = ["dev", "docs"]
@@ -23,7 +23,10 @@ Example:
23
23
  display_id_prefix = "inv"
24
24
  """
25
25
 
26
+ from typing import Any
27
+
26
28
  from .admin import DisplayIDSearchMixin
29
+ from .converters import DisplayIDConverter, DisplayIDOrUUIDConverter, UUIDConverter
27
30
  from .encoding import (
28
31
  decode_display_id,
29
32
  decode_uuid,
@@ -45,12 +48,29 @@ from .exceptions import (
45
48
  UnknownPrefixError,
46
49
  )
47
50
  from .managers import DisplayIDManager, DisplayIDQuerySet
48
- from .models import DisplayIDMixin, get_model_for_prefix
49
51
  from .resolver import resolve_object
50
52
  from .typing import DEFAULT_STRATEGIES, StrategyName
51
53
  from .views import DisplayIDObjectMixin
52
54
 
55
+
56
+ def __getattr__(name: str) -> Any:
57
+ """Lazy import for model-related items to avoid app registry issues."""
58
+ if name == "DisplayIDMixin":
59
+ from .models import DisplayIDMixin
60
+
61
+ return DisplayIDMixin
62
+ if name == "get_model_for_prefix":
63
+ from .models import get_model_for_prefix
64
+
65
+ return get_model_for_prefix
66
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
67
+
68
+
53
69
  __all__ = [ # noqa: RUF022 - keep categorized order for readability
70
+ # URL converters
71
+ "DisplayIDConverter",
72
+ "UUIDConverter",
73
+ "DisplayIDOrUUIDConverter",
54
74
  # Encoding
55
75
  "encode_uuid",
56
76
  "decode_uuid",
@@ -85,10 +105,8 @@ __all__ = [ # noqa: RUF022 - keep categorized order for readability
85
105
  "DEFAULT_STRATEGIES",
86
106
  ]
87
107
 
88
- __version__ = "0.1.1"
89
-
90
108
 
91
- def get_drf_mixin():
109
+ def get_drf_mixin() -> type:
92
110
  """Lazily import the DRF mixin to avoid hard dependency.
93
111
 
94
112
  Returns:
@@ -4,12 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import contextlib
6
6
  import uuid
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from .encoding import decode_display_id
10
10
 
11
11
  if TYPE_CHECKING:
12
- from django.db.models import QuerySet
12
+ from django.db.models import Model, QuerySet
13
13
  from django.http import HttpRequest
14
14
 
15
15
  __all__ = ["DisplayIDSearchMixin"]
@@ -39,13 +39,15 @@ class DisplayIDSearchMixin:
39
39
  """
40
40
 
41
41
  uuid_field: str | None = None
42
+ model: type[Model]
42
43
 
43
44
  def _get_uuid_field(self) -> str:
44
45
  """Get the UUID field name to search."""
45
46
  if self.uuid_field is not None:
46
47
  return self.uuid_field
47
48
  # Try to get from model's uuid_field attribute
48
- return getattr(self.model, "uuid_field", None) or "id"
49
+ uuid_field: str | None = getattr(self.model, "uuid_field", None)
50
+ return uuid_field or "id"
49
51
 
50
52
  def _try_parse_uuid(self, value: str) -> uuid.UUID | None:
51
53
  """Try to parse a string as a UUID."""
@@ -57,16 +59,16 @@ class DisplayIDSearchMixin:
57
59
  def get_search_results(
58
60
  self,
59
61
  request: HttpRequest,
60
- queryset: QuerySet,
62
+ queryset: QuerySet[Any],
61
63
  search_term: str,
62
- ) -> tuple[QuerySet, bool]:
64
+ ) -> tuple[QuerySet[Any], bool]:
63
65
  """Extend search to handle display IDs and raw UUIDs.
64
66
 
65
67
  Tries to match the search term as:
66
68
  1. A display ID (prefix_base62uuid) if it contains an underscore
67
69
  2. A raw UUID if it looks like a UUID format
68
70
  """
69
- queryset, use_distinct = super().get_search_results(
71
+ queryset, use_distinct = super().get_search_results( # type: ignore[misc]
70
72
  request, queryset, search_term
71
73
  )
72
74
 
@@ -0,0 +1,11 @@
1
+ """Django app configuration for django-display-ids."""
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class DjangoDisplayIdsConfig(AppConfig):
7
+ """App configuration for django-display-ids."""
8
+
9
+ name = "django_display_ids"
10
+ verbose_name = "Django Display IDs"
11
+ default_auto_field = "django.db.models.BigAutoField"
@@ -45,5 +45,8 @@ def get_setting(name: str) -> str | tuple[StrategyName, ...]:
45
45
  if name not in DEFAULTS:
46
46
  raise KeyError(f"Unknown setting: {name}")
47
47
 
48
- user_settings = getattr(settings, "DISPLAY_IDS", {})
49
- return user_settings.get(name, DEFAULTS[name])
48
+ user_settings: dict[str, str | tuple[str, ...]] = getattr(
49
+ settings, "DISPLAY_IDS", {}
50
+ )
51
+ result = user_settings.get(name, DEFAULTS[name])
52
+ return result # type: ignore[return-value]
@@ -6,16 +6,21 @@ proper OpenAPI schema generation for DisplayIDField.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ from typing import TYPE_CHECKING, Any
10
+
9
11
  try:
10
12
  from drf_spectacular.extensions import OpenApiSerializerFieldExtension
11
13
  except ImportError:
12
14
  # drf-spectacular not installed, skip extension registration
13
15
  pass
14
16
  else:
17
+ if TYPE_CHECKING:
18
+ from drf_spectacular.openapi import AutoSchema
19
+
15
20
  from django_display_ids.encoding import ENCODED_UUID_LENGTH, encode_uuid
16
21
  from django_display_ids.examples import example_uuid_for_prefix
17
22
 
18
- class DisplayIDFieldExtension(OpenApiSerializerFieldExtension):
23
+ class DisplayIDFieldExtension(OpenApiSerializerFieldExtension): # type: ignore[no-untyped-call]
19
24
  """OpenAPI schema extension for DisplayIDField.
20
25
 
21
26
  Generates schema with correct prefix example based on the field's
@@ -27,7 +32,7 @@ else:
27
32
  )
28
33
  match_subclasses = True
29
34
 
30
- def _get_model_from_view(self, auto_schema):
35
+ def _get_model_from_view(self, auto_schema: AutoSchema | None) -> Any:
31
36
  """Try to get model from the view's queryset."""
32
37
  if auto_schema is None:
33
38
  return None
@@ -48,7 +53,9 @@ else:
48
53
  return queryset.model
49
54
  return None
50
55
 
51
- def map_serializer_field(self, auto_schema, direction):
56
+ def map_serializer_field(
57
+ self, auto_schema: AutoSchema, direction: str
58
+ ) -> dict[str, Any]:
52
59
  """Generate OpenAPI schema for DisplayIDField."""
53
60
  # Get prefix from field override or try to get from model
54
61
  prefix = self.target._prefix_override
@@ -95,9 +95,9 @@ class DisplayIDField(serializers.SerializerMethodField):
95
95
  # If using prefix override, generate display_id with that prefix
96
96
  if self._prefix_override is not None:
97
97
  # Get uuid_field name from model, then fall back to settings
98
- uuid_field_name = getattr(obj, "uuid_field", None)
98
+ uuid_field_name: str | None = getattr(obj, "uuid_field", None)
99
99
  if uuid_field_name is None:
100
- uuid_field_name = get_setting("UUID_FIELD")
100
+ uuid_field_name = str(get_setting("UUID_FIELD"))
101
101
  uuid_value = getattr(obj, uuid_field_name, None)
102
102
  if uuid_value is None:
103
103
  raise ValueError(
@@ -108,7 +108,8 @@ class DisplayIDField(serializers.SerializerMethodField):
108
108
 
109
109
  # Use the model's display_id property
110
110
  if hasattr(obj, "display_id"):
111
- return obj.display_id
111
+ display_id: str = obj.display_id
112
+ return display_id
112
113
 
113
114
  raise ValueError(
114
115
  f"Cannot generate display_id: {obj.__class__.__name__} "
@@ -174,4 +174,4 @@ class DisplayIDLookupMixin:
174
174
  # Check object-level permissions
175
175
  self.check_object_permissions(self.request, obj)
176
176
 
177
- return obj
177
+ return obj # type: ignore[no-any-return]
@@ -0,0 +1,111 @@
1
+ """Django URL path converters for display IDs and UUIDs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "DisplayIDConverter",
7
+ "DisplayIDOrUUIDConverter",
8
+ "UUIDConverter",
9
+ ]
10
+
11
+
12
+ class DisplayIDConverter:
13
+ """Path converter for display IDs.
14
+
15
+ Matches the format: {prefix}_{base62} where prefix is 1-16 lowercase
16
+ letters and base62 is exactly 22 alphanumeric characters.
17
+
18
+ Example:
19
+ from django.urls import path, register_converter
20
+ from django_display_ids.converters import DisplayIDConverter
21
+
22
+ register_converter(DisplayIDConverter, "display_id")
23
+
24
+ urlpatterns = [
25
+ path("invoices/<display_id:id>/", InvoiceDetailView.as_view()),
26
+ ]
27
+ """
28
+
29
+ regex = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
30
+
31
+ def to_python(self, value: str) -> str:
32
+ """Convert the URL value to a Python object."""
33
+ return value
34
+
35
+ def to_url(self, value: str) -> str:
36
+ """Convert a Python object to a URL string."""
37
+ return value
38
+
39
+
40
+ class UUIDConverter:
41
+ """Path converter for UUIDs.
42
+
43
+ Matches UUIDs in both hyphenated and unhyphenated formats:
44
+ - 550e8400-e29b-41d4-a716-446655440000 (hyphenated)
45
+ - 550e8400e29b41d4a716446655440000 (unhyphenated)
46
+
47
+ Example:
48
+ from django.urls import path, register_converter
49
+ from django_display_ids.converters import UUIDConverter
50
+
51
+ register_converter(UUIDConverter, "uuid")
52
+
53
+ urlpatterns = [
54
+ path("invoices/<uuid:id>/", InvoiceDetailView.as_view()),
55
+ ]
56
+
57
+ Note:
58
+ Django's built-in UUIDConverter only accepts hyphenated UUIDs.
59
+ This converter is more permissive.
60
+ """
61
+
62
+ # Hyphenated: 8-4-4-4-12 hex chars with hyphens
63
+ # Unhyphenated: 32 hex chars
64
+ # Note: Parentheses group the alternatives so ^ and $ anchor correctly
65
+ regex = (
66
+ r"(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32})"
67
+ )
68
+
69
+ def to_python(self, value: str) -> str:
70
+ """Convert the URL value to a Python object."""
71
+ return value
72
+
73
+ def to_url(self, value: str) -> str:
74
+ """Convert a Python object to a URL string."""
75
+ return value
76
+
77
+
78
+ class DisplayIDOrUUIDConverter:
79
+ """Path converter for display IDs or UUIDs.
80
+
81
+ Matches either format:
82
+ - Display ID: {prefix}_{base62}
83
+ - UUID: hyphenated or unhyphenated
84
+
85
+ Example:
86
+ from django.urls import path, register_converter
87
+ from django_display_ids.converters import DisplayIDOrUUIDConverter
88
+
89
+ register_converter(DisplayIDOrUUIDConverter, "display_id_or_uuid")
90
+
91
+ urlpatterns = [
92
+ path("invoices/<display_id_or_uuid:id>/", InvoiceDetailView.as_view()),
93
+ ]
94
+ """
95
+
96
+ # Note: Parentheses group the alternatives so ^ and $ anchor correctly
97
+ regex = (
98
+ r"(?:"
99
+ r"[a-z]{1,16}_[0-9A-Za-z]{22}"
100
+ r"|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
101
+ r"|[0-9a-f]{32}"
102
+ r")"
103
+ )
104
+
105
+ def to_python(self, value: str) -> str:
106
+ """Convert the URL value to a Python object."""
107
+ return value
108
+
109
+ def to_url(self, value: str) -> str:
110
+ """Convert a Python object to a URL string."""
111
+ return value
@@ -26,7 +26,7 @@ def _get_prefix(prefix_or_model: str | type[Model]) -> str:
26
26
  if isinstance(prefix_or_model, str):
27
27
  return prefix_or_model
28
28
  # It's a model class
29
- prefix = getattr(prefix_or_model, "display_id_prefix", None)
29
+ prefix: str | None = getattr(prefix_or_model, "display_id_prefix", None)
30
30
  if prefix is None:
31
31
  raise ValueError(f"Model {prefix_or_model.__name__} has no display_id_prefix")
32
32
  return prefix