django-api-versioning 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- django_api_versioning/__init__.py +0 -0
- django_api_versioning/decorators.py +88 -0
- django_api_versioning/exceptions.py +18 -0
- django_api_versioning/registry.py +33 -0
- django_api_versioning/settings.py +77 -0
- django_api_versioning/urls.py +5 -0
- django_api_versioning-0.1.0.dist-info/LICENSE +21 -0
- django_api_versioning-0.1.0.dist-info/METADATA +261 -0
- django_api_versioning-0.1.0.dist-info/RECORD +17 -0
- django_api_versioning-0.1.0.dist-info/WHEEL +5 -0
- django_api_versioning-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_decorators.py +88 -0
- tests/test_exceptions.py +19 -0
- tests/test_registry.py +83 -0
- tests/test_settings.py +147 -0
- tests/test_urls.py +33 -0
File without changes
|
@@ -0,0 +1,88 @@
|
|
1
|
+
from functools import wraps
|
2
|
+
from typing import Callable, Optional, List
|
3
|
+
from .settings import api_settings as settings
|
4
|
+
from .registry import registry
|
5
|
+
from .exceptions import InvalidVersionError, VersionRangeError, VersionTypeError
|
6
|
+
|
7
|
+
|
8
|
+
def endpoint(
|
9
|
+
postfix: str,
|
10
|
+
version: Optional[int] = None,
|
11
|
+
backward: bool = True,
|
12
|
+
app_name: Optional[str] = None,
|
13
|
+
view_name: Optional[str] = None,
|
14
|
+
) -> Callable:
|
15
|
+
"""
|
16
|
+
Decorator to register API views with versioning support.
|
17
|
+
|
18
|
+
- Uses `API_MIN_VERSION` and `API_MAX_VERSION` from Django settings.
|
19
|
+
- Supports backward compatibility by registering multiple versions if needed.
|
20
|
+
- Ensures that no version lower than `API_MIN_VERSION` is registered.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
postfix (str): The endpoint suffix (e.g., "users" → "api/v1/users").
|
24
|
+
version (Optional[int]): The version of the API. Defaults to None (unversioned).
|
25
|
+
backward (bool): If True, registers routes for all versions from `API_MIN_VERSION` up to the current version, which is less than or equal to `API_MAX_VERSION`. Defaults to True.
|
26
|
+
app_name (Optional[str]): The app name to be prefixed to the route.
|
27
|
+
view_name (Optional[str]): The custom view name for Django.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
Callable: The decorated view function.
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
VersionTypeError: If the provided `version` is not an integer.
|
34
|
+
VersionRangeError: If `API_MIN_VERSION` or `API_MAX_VERSION` are not properly set.
|
35
|
+
"""
|
36
|
+
|
37
|
+
def decorator(func: Callable) -> Callable:
|
38
|
+
@wraps(func)
|
39
|
+
def view(*args, **kwargs):
|
40
|
+
return func(*args, **kwargs)
|
41
|
+
|
42
|
+
# Read API versioning settings
|
43
|
+
min_version: int = getattr(settings, "API_MIN_VERSION", 1)
|
44
|
+
max_version: int = getattr(settings, "API_MAX_VERSION", 1)
|
45
|
+
|
46
|
+
if not isinstance(min_version, int) or not isinstance(max_version, int):
|
47
|
+
raise VersionRangeError("API_MIN_VERSION and API_MAX_VERSION must be integers.")
|
48
|
+
|
49
|
+
if min_version > max_version:
|
50
|
+
raise VersionRangeError("API_MIN_VERSION cannot be greater than API_MAX_VERSION.")
|
51
|
+
|
52
|
+
if version is not None and not isinstance(version, int):
|
53
|
+
raise VersionTypeError("Version must be an integer or None.")
|
54
|
+
|
55
|
+
if version is not None and version > max_version:
|
56
|
+
raise InvalidVersionError(f"Version {version} is above the maximum allowed version {max_version}.")
|
57
|
+
|
58
|
+
app_name_part: str = f"{app_name}/" if app_name else ""
|
59
|
+
|
60
|
+
def _register_route(ver: Optional[int]) -> None:
|
61
|
+
"""Helper function to register a route in the registry."""
|
62
|
+
if ver is None:
|
63
|
+
base_path = "" # No version prefix
|
64
|
+
else:
|
65
|
+
base_path = settings.API_BASE_PATH.format(version=ver)
|
66
|
+
route = f"{base_path}{app_name_part}{postfix}"
|
67
|
+
registry.register(route, view, view_name)
|
68
|
+
|
69
|
+
def _get_valid_versions() -> List[Optional[int]]:
|
70
|
+
"""Returns a list of valid versions to register."""
|
71
|
+
if version is None:
|
72
|
+
# If no version is given, register only the unversioned route
|
73
|
+
return [None] # Just register the unversioned route
|
74
|
+
if version < min_version:
|
75
|
+
return [] # Ignore versions below min_version
|
76
|
+
if backward:
|
77
|
+
return list(range(min_version, version + 1)) # Register all versions up to the given version
|
78
|
+
return [version] # Only register the specified version when backward is False
|
79
|
+
|
80
|
+
# Register valid versions
|
81
|
+
valid_versions = _get_valid_versions()
|
82
|
+
|
83
|
+
for ver in valid_versions:
|
84
|
+
_register_route(ver)
|
85
|
+
|
86
|
+
return view
|
87
|
+
|
88
|
+
return decorator
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class VersioningError(Exception):
|
2
|
+
"""Raised when there is a general error in API versioning."""
|
3
|
+
pass
|
4
|
+
|
5
|
+
|
6
|
+
class InvalidVersionError(VersioningError):
|
7
|
+
"""Raised when an invalid API version is used."""
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
class VersionRangeError(VersioningError):
|
12
|
+
"""Raised when the version range is not valid (e.g., min_version > max_version)."""
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class VersionTypeError(VersioningError):
|
17
|
+
"""Raised when the version type is invalid (e.g., not an integer)."""
|
18
|
+
pass
|
@@ -0,0 +1,33 @@
|
|
1
|
+
from typing import Callable, List, Optional
|
2
|
+
from django.urls import path
|
3
|
+
from django.http import HttpRequest, HttpResponse
|
4
|
+
|
5
|
+
|
6
|
+
class UrlRegistry:
|
7
|
+
"""Registry for managing API versioning URLs."""
|
8
|
+
|
9
|
+
def __init__(self):
|
10
|
+
self.urlpatterns: List[path] = []
|
11
|
+
|
12
|
+
def register(self, route: str, view: Callable[[HttpRequest], HttpResponse], name: Optional[str] = None) -> None:
|
13
|
+
"""Register a new API endpoint."""
|
14
|
+
if self._is_duplicate(route):
|
15
|
+
self._remove_duplicate(route)
|
16
|
+
|
17
|
+
if not callable(view) and hasattr(view, "as_view"):
|
18
|
+
view = view.as_view()
|
19
|
+
|
20
|
+
path_dict = {"name": name} if name else {}
|
21
|
+
self.urlpatterns.append(path(route, view, **path_dict))
|
22
|
+
|
23
|
+
def _is_duplicate(self, full_path):
|
24
|
+
"""Check if the route is already registered."""
|
25
|
+
return any(str(pattern.pattern) == full_path for pattern in self.urlpatterns)
|
26
|
+
|
27
|
+
def _remove_duplicate(self, full_path):
|
28
|
+
"""Remove any existing route with the same path."""
|
29
|
+
self.urlpatterns = [p for p in self.urlpatterns if str(p.pattern) != full_path]
|
30
|
+
|
31
|
+
|
32
|
+
registry = UrlRegistry()
|
33
|
+
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import logging
|
2
|
+
from django.conf import settings
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import Optional
|
6
|
+
from .exceptions import VersionTypeError, VersionRangeError, VersioningError
|
7
|
+
|
8
|
+
# Initialize logger
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class APISettings:
|
13
|
+
|
14
|
+
API_BASE_PATH: str = "api/v{version}/"
|
15
|
+
API_MAX_VERSION: int = 1
|
16
|
+
API_MIN_VERSION: int = 1
|
17
|
+
ROOT_URLCONF: str = 'django_api_versioning.urls'
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def get_setting(name: str, default: Optional[any] = None) -> Optional[any]:
|
21
|
+
"""
|
22
|
+
Reads the setting from Django settings and provides a default if not found.
|
23
|
+
"""
|
24
|
+
return getattr(settings, name, default)
|
25
|
+
|
26
|
+
def __post_init__(self):
|
27
|
+
# Ensure API_BASE_PATH contains "{version}"
|
28
|
+
if "{version}" not in self.API_BASE_PATH:
|
29
|
+
raise VersioningError("API_BASE_PATH must contain '{version}' like 'api/v{version}/' to support API versioning.")
|
30
|
+
|
31
|
+
# Ensure that API_BASE_PATH ends with a "/"
|
32
|
+
if not self.API_BASE_PATH.endswith("/"):
|
33
|
+
logger.warning(
|
34
|
+
"API_BASE_PATH should end with a '/'. Adding '/' automatically."
|
35
|
+
)
|
36
|
+
self.API_BASE_PATH += "/"
|
37
|
+
|
38
|
+
# Validate version settings
|
39
|
+
self.validate_version_settings()
|
40
|
+
|
41
|
+
if not self.ROOT_URLCONF:
|
42
|
+
raise ImproperlyConfigured("ROOT_URLCONF is required in settings.")
|
43
|
+
|
44
|
+
def validate_version_settings(self) -> None:
|
45
|
+
"""
|
46
|
+
Validates that the API_MIN_VERSION and API_MAX_VERSION are integers
|
47
|
+
and that API_MIN_VERSION is not greater than API_MAX_VERSION.
|
48
|
+
"""
|
49
|
+
if not isinstance(self.API_MIN_VERSION, int):
|
50
|
+
raise VersionTypeError(f"API_MIN_VERSION must be an integer, got {type(self.API_MIN_VERSION).__name__}.")
|
51
|
+
|
52
|
+
if not isinstance(self.API_MAX_VERSION, int):
|
53
|
+
raise VersionTypeError(f"API_MAX_VERSION must be an integer, got {type(self.API_MAX_VERSION).__name__}.")
|
54
|
+
|
55
|
+
if self.API_MIN_VERSION > self.API_MAX_VERSION:
|
56
|
+
raise VersionRangeError("API_MIN_VERSION cannot be greater than API_MAX_VERSION.")
|
57
|
+
|
58
|
+
|
59
|
+
# Adding default settings if not defined in Django settings
|
60
|
+
default_settings = {
|
61
|
+
'API_BASE_PATH': "api/v{version}/",
|
62
|
+
'API_MAX_VERSION': 1,
|
63
|
+
'API_MIN_VERSION': 1,
|
64
|
+
'ROOT_URLCONF': 'django_api_versioning.urls',
|
65
|
+
}
|
66
|
+
|
67
|
+
# Override default settings with actual Django settings if they exist
|
68
|
+
for setting, default_value in default_settings.items():
|
69
|
+
setattr(settings, setting, getattr(settings, setting, default_value))
|
70
|
+
|
71
|
+
# Initialize APISettings from Django settings
|
72
|
+
api_settings = APISettings(
|
73
|
+
API_BASE_PATH=APISettings.get_setting("API_BASE_PATH", "api/v{version}/"),
|
74
|
+
API_MAX_VERSION=APISettings.get_setting("API_MAX_VERSION", 1),
|
75
|
+
API_MIN_VERSION=APISettings.get_setting("API_MIN_VERSION", 1),
|
76
|
+
ROOT_URLCONF=APISettings.get_setting("ROOT_URLCONF", 'django_api_versioning.urls')
|
77
|
+
)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) [year] [Full name]
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,261 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: django-api-versioning
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A powerful and flexible library for managing API versioning in Django projects.
|
5
|
+
Home-page: https://github.com/mojtaba-arvin/django-api-versioning
|
6
|
+
Author: Mojtaba Arvin
|
7
|
+
Author-email: ArvinDevDay@gmail.com
|
8
|
+
License: UNKNOWN
|
9
|
+
Platform: UNKNOWN
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
12
|
+
Classifier: Framework :: Django
|
13
|
+
Classifier: Framework :: Django :: 3.2
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
15
|
+
Classifier: Intended Audience :: Developers
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
17
|
+
Classifier: Operating System :: OS Independent
|
18
|
+
Requires-Python: >=3.9
|
19
|
+
Description-Content-Type: text/markdown
|
20
|
+
License-File: LICENSE
|
21
|
+
Requires-Dist: Django>=3.2
|
22
|
+
Provides-Extra: dev
|
23
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
24
|
+
Requires-Dist: flake8>=4.0.0; extra == "dev"
|
25
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
26
|
+
Requires-Dist: mypy>=0.900; extra == "dev"
|
27
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
28
|
+
Requires-Dist: pytest-django>=4.0; extra == "dev"
|
29
|
+
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
|
30
|
+
Requires-Dist: pre-commit>=2.0.0; extra == "dev"
|
31
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
32
|
+
Requires-Dist: build>=0.8.0; extra == "dev"
|
33
|
+
|
34
|
+
|
35
|
+
# Django API Versioning
|
36
|
+
|
37
|
+
[](https://badge.fury.io/py/django-api-versioning)
|
38
|
+
[](https://github.com/mojtaba-arvin/django-api-versioning/actions)
|
39
|
+
[](https://codecov.io/gh/mojtaba-arvin/django-api-versioning)
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
41
|
+
|
42
|
+
**Django API Versioning** is a powerful and flexible library for managing API versioning in Django projects. It allows you to easily define and manage different versions of your API endpoints using decorators, ensuring backward compatibility and clean code organization.
|
43
|
+
|
44
|
+
## Features
|
45
|
+
|
46
|
+
- **Easy Versioning**: Define API versions using simple decorators.
|
47
|
+
- **Backward Compatibility**: Automatically register routes for all versions up to the specified version.
|
48
|
+
- **Automatic Registration:** Views are **automatically** registered for each version specified, so there is no need to manually register each version in your `urls.py`.
|
49
|
+
- **Customizable Settings**: Configure API base path, minimum and maximum versions, and more.
|
50
|
+
- **Type Checking**: Full support for type hints and static type checking with `mypy`.
|
51
|
+
- **Testing Ready**: Includes comprehensive test suite and pre-commit hooks for code quality.
|
52
|
+
|
53
|
+
## Installation
|
54
|
+
|
55
|
+
You can install Django API Versioning via pip:
|
56
|
+
|
57
|
+
```bash
|
58
|
+
pip install django-api-versioning
|
59
|
+
```
|
60
|
+
|
61
|
+
## Quick Start
|
62
|
+
|
63
|
+
1. ### Add to Django Settings:
|
64
|
+
|
65
|
+
```python
|
66
|
+
INSTALLED_APPS = [
|
67
|
+
...
|
68
|
+
'django_api_versioning',
|
69
|
+
...
|
70
|
+
]
|
71
|
+
```
|
72
|
+
|
73
|
+
2. ### Define API Settings:
|
74
|
+
|
75
|
+
```python
|
76
|
+
|
77
|
+
API_BASE_PATH = "api/v{version}/"
|
78
|
+
API_MIN_VERSION = 1
|
79
|
+
API_MAX_VERSION = 3
|
80
|
+
```
|
81
|
+
|
82
|
+
3. ### Register API urls:
|
83
|
+
|
84
|
+
if you don't use any `ROOT_URLCONF` in settings you can use this:
|
85
|
+
|
86
|
+
```python
|
87
|
+
ROOT_URLCONF = 'django_api_versioning.urls'
|
88
|
+
```
|
89
|
+
|
90
|
+
or you have already have a `ROOT_URLCONF` in settings, you only need to import them into your root `urls.py`:
|
91
|
+
|
92
|
+
```python
|
93
|
+
from django.urls import path, include
|
94
|
+
from django_api_versioning.urls import urlpatterns as api_urlpatterns
|
95
|
+
|
96
|
+
urlpatterns = [
|
97
|
+
# other paths here
|
98
|
+
|
99
|
+
# use empty `route` param and use `API_BASE_PATH` in settings as prefix
|
100
|
+
path('', include(api_urlpatterns)),
|
101
|
+
]
|
102
|
+
|
103
|
+
```
|
104
|
+
|
105
|
+
3. ### Use the Decorator:
|
106
|
+
|
107
|
+
The `endpoint` decorator can be used in both function-based views (FBVs) and class-based views (CBVs). It's also fully compatible with `Django Rest Framework (DRF)`. The decorator allows you to define versioning for your API views and supports backward compatibility by default and you don't need to pass `backward=True` flag to the `endpoint` decorator.
|
108
|
+
|
109
|
+
|
110
|
+
#### Example for Function-Based Views (FBVs):
|
111
|
+
|
112
|
+
```python
|
113
|
+
from django_api_versioning.decorators import endpoint
|
114
|
+
from django.http import HttpResponse
|
115
|
+
|
116
|
+
@endpoint("users", version=2, app_name='account_app', view_name="users_list_api")
|
117
|
+
def users_view(request):
|
118
|
+
return HttpResponse("API Version 2 Users")
|
119
|
+
```
|
120
|
+
|
121
|
+
In this example, the `users_view` function is decorated with the endpoint decorator. This specifies that the view is accessible under version `2` of the API and **supports backward compatibility**. The `backward=True` flag as default ensures that users can also access the previous version (version `1`) at `/api/v1/account_app/users`.
|
122
|
+
|
123
|
+
#### Example for Class-Based Views (CBVs):
|
124
|
+
For class-based views, you can apply the decorator to methods such as `get`, `post`, or any other HTTP method you need to handle. Here’s an example:
|
125
|
+
|
126
|
+
```python
|
127
|
+
|
128
|
+
from django_api_versioning.decorators import endpoint
|
129
|
+
from django.http import JsonResponse
|
130
|
+
from django.views import View
|
131
|
+
|
132
|
+
@endpoint("users", version=2, app_name='account_app', view_name="users_list_api")
|
133
|
+
class UsersView(View):
|
134
|
+
|
135
|
+
def get(self, request):
|
136
|
+
return JsonResponse({"message": "API Version 2 Users"})
|
137
|
+
|
138
|
+
```
|
139
|
+
|
140
|
+
#### Integration with Django Rest Framework (DRF):
|
141
|
+
|
142
|
+
If you have already installed [Django Rest Framework](https://www.django-rest-framework.org/#installation), the `endpoint` decorator can be easily applied to APIView or viewsets. Here’s an example with a DRF APIView:
|
143
|
+
|
144
|
+
|
145
|
+
```python
|
146
|
+
from rest_framework.views import APIView
|
147
|
+
from rest_framework.response import Response
|
148
|
+
from django_api_versioning.decorators import endpoint
|
149
|
+
|
150
|
+
@endpoint("users", version=2, app_name='account_app', view_name="users_list_api")
|
151
|
+
class UsersAPIView(APIView):
|
152
|
+
|
153
|
+
def get(self, request):
|
154
|
+
return Response({"message": "API Version 2 Users"})
|
155
|
+
```
|
156
|
+
|
157
|
+
#### URL Generation Based on Versioning:
|
158
|
+
Once the decorator is applied, the URLs for your API will be generated based on the version specified in the decorator. For example, if the `API_MIN_VERSION` in your settings.py is set to `1` and the version in the decorator is set to `2`, the following URLs will be available:
|
159
|
+
|
160
|
+
* `/api/v1/account_app/users`
|
161
|
+
* `/api/v2/account_app/users`
|
162
|
+
|
163
|
+
The `API_MIN_VERSION` setting ensures that users can access the API using different versions, providing backward compatibility. You can adjust which versions are considered valid by modifying the `API_MIN_VERSION` and `version` numbers in the decorators.
|
164
|
+
|
165
|
+
#### Additional Configuration Options:
|
166
|
+
|
167
|
+
**Without `app_name`:** If you don't pass `app_name` in the decorator, like this:
|
168
|
+
```python
|
169
|
+
@endpoint("users", version=2, view_name="users_list_api")
|
170
|
+
```
|
171
|
+
|
172
|
+
The generated URLs will be:
|
173
|
+
|
174
|
+
* `/api/v1/users`
|
175
|
+
* `/api/v2/users`
|
176
|
+
|
177
|
+
|
178
|
+
**Without `version`:** If you don't pass `version` in the decorator, like this:
|
179
|
+
|
180
|
+
```python
|
181
|
+
@endpoint("users", view_name="users_list_api")
|
182
|
+
```
|
183
|
+
|
184
|
+
API versioning will be disabled (`API_BASE_PATH` as prefix will be removed) for that view. The only URL generated will be:
|
185
|
+
|
186
|
+
* `/users`
|
187
|
+
|
188
|
+
**Setting `backward=False`:** By default, the `backward` parameter is set to `True`, which ensures backward compatibility. If you explicitly set `backward=False`, like this:
|
189
|
+
|
190
|
+
```python
|
191
|
+
@endpoint("users", version=2, backward=False, view_name="users_list_api")
|
192
|
+
```
|
193
|
+
|
194
|
+
The generated URL will be only version 2:
|
195
|
+
|
196
|
+
* `api/v2/users`
|
197
|
+
|
198
|
+
4. Run the Server:
|
199
|
+
|
200
|
+
```bash
|
201
|
+
python manage.py runserver
|
202
|
+
```
|
203
|
+
|
204
|
+
## Notes
|
205
|
+
### 1. `API_BASE_PATH` in settings Must Include `{version}`:
|
206
|
+
The `API_BASE_PATH` should always include `{version}` to ensure proper API versioning. This is important for correctly mapping API routes to different versions.
|
207
|
+
|
208
|
+
### 2. Using `app_name` in the `endpoint` decorator:
|
209
|
+
It's recommended to fill in the `app_name` in the `endpoint` decorator to make the API URLs **more unique and organized**. This ensures that the routes are scoped under the correct app, avoiding potential conflicts and making them easier to manage.
|
210
|
+
|
211
|
+
### 3. Views with Version Less Than `API_MIN_VERSION` Are Automatically Ignored:
|
212
|
+
Any view whose `version` is less than the `API_MIN_VERSION` will be automatically ignored. This means clients will no longer have access to these older versions, **without the need to manually edit or remove code**. This is handled automatically by the package.
|
213
|
+
|
214
|
+
### 4. URLs for Versions Between `API_MIN_VERSION` <= `version` <= `API_MAX_VERSION`:
|
215
|
+
Endpoints that have versions within the range defined by `API_MIN_VERSION` <= `version` <= `API_MAX_VERSION` will always have a corresponding URL generated. This ensures that only valid versions will be accessible, providing flexibility in version management.
|
216
|
+
|
217
|
+
### `endpoint` Decorator Function Definition
|
218
|
+
|
219
|
+
The `endpoint` decorator is designed to register API views with versioning support in a Django application. It provides flexibility in managing versioned endpoints and ensures backward compatibility with previous versions of the API.
|
220
|
+
|
221
|
+
```python
|
222
|
+
def endpoint(
|
223
|
+
postfix: str,
|
224
|
+
version: Optional[int] = None,
|
225
|
+
backward: bool = True,
|
226
|
+
app_name: Optional[str] = None,
|
227
|
+
view_name: Optional[str] = None,
|
228
|
+
) -> Callable:
|
229
|
+
"""
|
230
|
+
Decorator to register API views with versioning support.
|
231
|
+
|
232
|
+
- Uses `API_MIN_VERSION` and `API_MAX_VERSION` from Django settings.
|
233
|
+
- Supports backward compatibility by registering multiple versions if needed.
|
234
|
+
- Ensures that no version lower than `API_MIN_VERSION` is registered.
|
235
|
+
|
236
|
+
Args:
|
237
|
+
postfix (str): The endpoint suffix (e.g., "users" → "api/v1/users").
|
238
|
+
version (Optional[int]): The version of the API. Defaults to None (unversioned).
|
239
|
+
backward (bool): If True, registers routes for all versions from `API_MIN_VERSION` up to the current version, which is less than or equal to `API_MAX_VERSION`. Defaults to True.
|
240
|
+
app_name (Optional[str]): The app name to be prefixed to the route.
|
241
|
+
view_name (Optional[str]): The custom view name for Django.
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
Callable: The decorated view function.
|
245
|
+
|
246
|
+
Raises:
|
247
|
+
VersionTypeError: If the provided `version` is not an integer.
|
248
|
+
VersionRangeError: If `API_MIN_VERSION` or `API_MAX_VERSION` are not properly set.
|
249
|
+
"""
|
250
|
+
```
|
251
|
+
|
252
|
+
|
253
|
+
## Contributing
|
254
|
+
|
255
|
+
Feel free to open an issue or submit a pull request with any improvements or bug fixes. We appreciate contributions to enhance this package!
|
256
|
+
|
257
|
+
## License
|
258
|
+
|
259
|
+
This package is open-source and available under the MIT license.
|
260
|
+
|
261
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
django_api_versioning/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
django_api_versioning/decorators.py,sha256=BC_7ZQwCDq3BjPdcNw-Efdh09kHv3uDjZEW1nGgcNog,3732
|
3
|
+
django_api_versioning/exceptions.py,sha256=MgCpaNBsD8laQoVIVK823_t1liQ82K_uqguuA60PxXQ,485
|
4
|
+
django_api_versioning/registry.py,sha256=FCRTHGyl995U1kLlxtIBp3lb62v-6AbSK9k3orzZxWA,1140
|
5
|
+
django_api_versioning/settings.py,sha256=rasiYFuKM0EUYqzrpIphHGPEYb209uz56pEKR59R8w4,3006
|
6
|
+
django_api_versioning/urls.py,sha256=B8UBYSXdyq_xTpWeN5zzH2SQzKiqZQMx3WFBrsu93X0,144
|
7
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
tests/test_decorators.py,sha256=G2G3PR_QAPNyJuaw7H9BMARSOK8C6GJlHRAL3KmTuCY,3587
|
9
|
+
tests/test_exceptions.py,sha256=fsyfA7ouIMqz6Emexqo_oB7oVEZlhqptNmwH8KX37s8,863
|
10
|
+
tests/test_registry.py,sha256=KQT6yWkfKZmcLShFkidHwfJfPi8yHN5i9xuzC3vqDso,2634
|
11
|
+
tests/test_settings.py,sha256=SjieOTnwHdU0E6A2-qLE3iXG5osY3qacbAzJJ7tqD-0,5213
|
12
|
+
tests/test_urls.py,sha256=DA8DbIEAYX812Re2PlTkL1OpkwCO2IZ1vW_QMh2d9nI,1207
|
13
|
+
django_api_versioning-0.1.0.dist-info/LICENSE,sha256=iDPJdze6sBlBBSoB-BIyT2iHfHDGUAaZG3nTFd6m2FQ,1070
|
14
|
+
django_api_versioning-0.1.0.dist-info/METADATA,sha256=B5McJrgZzySQghhKtqCvLMfBABI_TjgIFX8zHAnidS4,10436
|
15
|
+
django_api_versioning-0.1.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
16
|
+
django_api_versioning-0.1.0.dist-info/top_level.txt,sha256=F4n1zaE6P--9OytuMrvCD50vn7NvIVWkIl6ie9fsFck,28
|
17
|
+
django_api_versioning-0.1.0.dist-info/RECORD,,
|
tests/__init__.py
ADDED
File without changes
|
tests/test_decorators.py
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import patch
|
3
|
+
from django_api_versioning.settings import api_settings as settings
|
4
|
+
from django_api_versioning.registry import registry
|
5
|
+
from django_api_versioning.decorators import endpoint
|
6
|
+
from django_api_versioning.exceptions import InvalidVersionError, VersionTypeError, VersionRangeError
|
7
|
+
|
8
|
+
@pytest.fixture(autouse=True)
|
9
|
+
def clear_registered_routes():
|
10
|
+
"""Clear the registry before each test to ensure isolation."""
|
11
|
+
registry.urlpatterns.clear() # Clear the previous routes before the test
|
12
|
+
yield # Run the test
|
13
|
+
registry.urlpatterns.clear() # Clean up after the test to ensure isolation
|
14
|
+
|
15
|
+
@pytest.fixture
|
16
|
+
def mock_settings():
|
17
|
+
"""Fixture to mock settings for API versioning."""
|
18
|
+
with patch.object(settings, 'API_MIN_VERSION', 1), patch.object(settings, 'API_MAX_VERSION', 3):
|
19
|
+
yield settings
|
20
|
+
|
21
|
+
def test_version_below_minimum(mock_settings):
|
22
|
+
@endpoint("users", version=0)
|
23
|
+
def test_view():
|
24
|
+
pass
|
25
|
+
|
26
|
+
registered_routes = [str(p.pattern) for p in registry.urlpatterns]
|
27
|
+
assert "api/v0/users" not in registered_routes, f"Unexpected registered routes: {registered_routes}"
|
28
|
+
|
29
|
+
def test_version_above_maximum(mock_settings):
|
30
|
+
with pytest.raises(InvalidVersionError):
|
31
|
+
@endpoint("users", version=4)
|
32
|
+
def test_view():
|
33
|
+
pass
|
34
|
+
|
35
|
+
def test_invalid_version_type(mock_settings):
|
36
|
+
with pytest.raises(VersionTypeError):
|
37
|
+
@endpoint("users", version="invalid")
|
38
|
+
def test_view():
|
39
|
+
pass
|
40
|
+
|
41
|
+
def test_register_unversioned_route(mock_settings):
|
42
|
+
@endpoint("users")
|
43
|
+
def test_view():
|
44
|
+
pass
|
45
|
+
|
46
|
+
registered_routes = [str(p.pattern) for p in registry.urlpatterns]
|
47
|
+
# Assert that no versioned route (e.g., api/v1/users) is registered
|
48
|
+
for version in range(1, 4):
|
49
|
+
assert f"api/v{version}/users" not in registered_routes, f"Unexpected versioned route: api/v{version}/users"
|
50
|
+
# Assert that the unversioned route is registered
|
51
|
+
assert "users" in registered_routes, f"Missing unversioned route: {registered_routes}"
|
52
|
+
|
53
|
+
def test_backward_compatibility_enabled(mock_settings):
|
54
|
+
@endpoint("users", version=3)
|
55
|
+
def test_view():
|
56
|
+
pass
|
57
|
+
|
58
|
+
registered_routes = [str(p.pattern) for p in registry.urlpatterns]
|
59
|
+
# Assert that versions 1, 2, and 3 are registered for backward compatibility
|
60
|
+
for version in range(1, 4):
|
61
|
+
assert f"api/v{version}/users" in registered_routes, f"Missing route for v{version}: {registered_routes}"
|
62
|
+
|
63
|
+
def test_backward_compatibility_disabled(mock_settings):
|
64
|
+
@endpoint("users", version=2, backward=False)
|
65
|
+
def test_view():
|
66
|
+
pass
|
67
|
+
|
68
|
+
registered_routes = [str(p.pattern) for p in registry.urlpatterns]
|
69
|
+
# Assert that only version 2 is registered, and versions 1 and 3 are not
|
70
|
+
assert "api/v2/users" in registered_routes
|
71
|
+
assert "api/v1/users" not in registered_routes
|
72
|
+
assert "api/v3/users" not in registered_routes
|
73
|
+
|
74
|
+
def test_invalid_version_range(mock_settings):
|
75
|
+
# Set an invalid version range (min > max)
|
76
|
+
with patch.object(settings, 'API_MIN_VERSION', 4), patch.object(settings, 'API_MAX_VERSION', 3):
|
77
|
+
with pytest.raises(VersionRangeError):
|
78
|
+
@endpoint("users", version=2)
|
79
|
+
def test_view():
|
80
|
+
pass
|
81
|
+
|
82
|
+
def test_missing_api_version_settings():
|
83
|
+
# Remove API version settings to test for missing settings scenario
|
84
|
+
with patch.dict(settings.__dict__, {'API_MIN_VERSION': None, 'API_MAX_VERSION': None}):
|
85
|
+
with pytest.raises(VersionRangeError):
|
86
|
+
@endpoint("users", version=2)
|
87
|
+
def test_view():
|
88
|
+
pass
|
tests/test_exceptions.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
import pytest
|
2
|
+
from django_api_versioning.exceptions import VersioningError, InvalidVersionError, VersionRangeError, VersionTypeError
|
3
|
+
|
4
|
+
def test_versioning_error():
|
5
|
+
with pytest.raises(VersioningError, match="General versioning error"):
|
6
|
+
raise VersioningError("General versioning error")
|
7
|
+
|
8
|
+
|
9
|
+
def test_invalid_version_error():
|
10
|
+
with pytest.raises(InvalidVersionError, match="Invalid version specified"):
|
11
|
+
raise InvalidVersionError("Invalid version specified")
|
12
|
+
|
13
|
+
def test_version_range_error():
|
14
|
+
with pytest.raises(VersionRangeError, match="Invalid version range: min_version > max_version"):
|
15
|
+
raise VersionRangeError("Invalid version range: min_version > max_version")
|
16
|
+
|
17
|
+
def test_version_type_error():
|
18
|
+
with pytest.raises(VersionTypeError, match="Version must be an integer"):
|
19
|
+
raise VersionTypeError("Version must be an integer")
|
tests/test_registry.py
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
import pytest
|
2
|
+
from django_api_versioning.registry import registry
|
3
|
+
|
4
|
+
|
5
|
+
@pytest.fixture
|
6
|
+
def clear_registry():
|
7
|
+
"""Clears the registry before each test to ensure isolation."""
|
8
|
+
registry.urlpatterns.clear()
|
9
|
+
yield
|
10
|
+
registry.urlpatterns.clear()
|
11
|
+
|
12
|
+
|
13
|
+
def test_register_new_route(clear_registry):
|
14
|
+
"""Test that a new route is correctly registered."""
|
15
|
+
def sample_view():
|
16
|
+
pass
|
17
|
+
|
18
|
+
route = "api/v1/users"
|
19
|
+
registry.register(route, sample_view)
|
20
|
+
|
21
|
+
assert len(registry.urlpatterns) == 1
|
22
|
+
assert str(registry.urlpatterns[0].pattern) == route
|
23
|
+
|
24
|
+
|
25
|
+
def test_register_duplicate_route(clear_registry):
|
26
|
+
"""Test that a duplicate route is detected and removed."""
|
27
|
+
def sample_view():
|
28
|
+
pass
|
29
|
+
|
30
|
+
route = "api/v1/users"
|
31
|
+
registry.register(route, sample_view)
|
32
|
+
registry.register(route, sample_view) # Registering the same route again
|
33
|
+
|
34
|
+
assert len(registry.urlpatterns) == 1 # Only one instance of the route should be registered
|
35
|
+
assert str(registry.urlpatterns[0].pattern) == route
|
36
|
+
|
37
|
+
|
38
|
+
def test_register_route_with_name(clear_registry):
|
39
|
+
"""Test that a route is registered with a custom name."""
|
40
|
+
def sample_view():
|
41
|
+
pass
|
42
|
+
|
43
|
+
route = "api/v1/users"
|
44
|
+
route_name = "user-list"
|
45
|
+
registry.register(route, sample_view, name=route_name)
|
46
|
+
|
47
|
+
# Check if the route has the correct name
|
48
|
+
assert len(registry.urlpatterns) == 1
|
49
|
+
assert registry.urlpatterns[0].name == route_name
|
50
|
+
assert str(registry.urlpatterns[0].pattern) == route
|
51
|
+
|
52
|
+
|
53
|
+
def test_register_route_with_view_conversion(clear_registry):
|
54
|
+
"""Test that a view is correctly converted to an 'as_view' callable if needed."""
|
55
|
+
class SampleView:
|
56
|
+
def as_view(self):
|
57
|
+
return lambda: None # Simulate view conversion
|
58
|
+
|
59
|
+
route = "api/v1/items"
|
60
|
+
view = SampleView()
|
61
|
+
|
62
|
+
registry.register(route, view)
|
63
|
+
|
64
|
+
# Check if the route is registered with the converted view
|
65
|
+
assert len(registry.urlpatterns) == 1
|
66
|
+
assert callable(registry.urlpatterns[0].callback) # Should be callable as a view
|
67
|
+
assert str(registry.urlpatterns[0].pattern) == route
|
68
|
+
|
69
|
+
|
70
|
+
def test_duplicate_route_removal(clear_registry):
|
71
|
+
"""Test that duplicate routes are correctly removed."""
|
72
|
+
def sample_view():
|
73
|
+
pass
|
74
|
+
|
75
|
+
route1 = "api/v1/posts"
|
76
|
+
route2 = "api/v1/posts" # Duplicate route
|
77
|
+
|
78
|
+
registry.register(route1, sample_view)
|
79
|
+
registry.register(route2, sample_view) # Registering the same route again
|
80
|
+
|
81
|
+
# Check that only one route exists after attempting to register the duplicate
|
82
|
+
assert len(registry.urlpatterns) == 1
|
83
|
+
assert str(registry.urlpatterns[0].pattern) == route1
|
tests/test_settings.py
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
import pytest
|
2
|
+
from typing import Iterator
|
3
|
+
from django.conf import settings
|
4
|
+
from django.core.exceptions import ImproperlyConfigured
|
5
|
+
from django.test import override_settings
|
6
|
+
from django_api_versioning.settings import APISettings
|
7
|
+
from django_api_versioning.exceptions import VersioningError, VersionTypeError
|
8
|
+
|
9
|
+
@pytest.fixture(autouse=True)
|
10
|
+
def reset_settings() -> Iterator[None]:
|
11
|
+
"""
|
12
|
+
Automatically resets Django settings to their default values after each test.
|
13
|
+
This prevents state leakage between tests.
|
14
|
+
"""
|
15
|
+
settings.API_BASE_PATH = "api/v{version}/"
|
16
|
+
settings.API_MAX_VERSION = 1
|
17
|
+
settings.API_MIN_VERSION = 1
|
18
|
+
settings.ROOT_URLCONF = "django_api_versioning.urls"
|
19
|
+
yield # Run the test
|
20
|
+
# Reset settings after test execution
|
21
|
+
settings.API_BASE_PATH = "api/v{version}/"
|
22
|
+
settings.API_MAX_VERSION = 1
|
23
|
+
settings.API_MIN_VERSION = 1
|
24
|
+
settings.ROOT_URLCONF = "django_api_versioning.urls"
|
25
|
+
|
26
|
+
@override_settings(
|
27
|
+
API_BASE_PATH="api/v{version}/",
|
28
|
+
API_MAX_VERSION=1,
|
29
|
+
API_MIN_VERSION=1,
|
30
|
+
ROOT_URLCONF="django_api_versioning.urls",
|
31
|
+
)
|
32
|
+
def test_default_settings() -> None:
|
33
|
+
"""
|
34
|
+
Verifies that default settings are correctly applied when using APISettings.
|
35
|
+
"""
|
36
|
+
api_settings = APISettings(
|
37
|
+
API_BASE_PATH=settings.API_BASE_PATH,
|
38
|
+
API_MAX_VERSION=settings.API_MAX_VERSION,
|
39
|
+
API_MIN_VERSION=settings.API_MIN_VERSION,
|
40
|
+
ROOT_URLCONF=settings.ROOT_URLCONF,
|
41
|
+
)
|
42
|
+
|
43
|
+
assert api_settings.API_BASE_PATH == "api/v{version}/"
|
44
|
+
assert api_settings.API_MAX_VERSION == 1
|
45
|
+
assert api_settings.API_MIN_VERSION == 1
|
46
|
+
assert api_settings.ROOT_URLCONF == "django_api_versioning.urls"
|
47
|
+
|
48
|
+
@override_settings(
|
49
|
+
API_BASE_PATH="api/v{version}/custom/",
|
50
|
+
API_MAX_VERSION=2,
|
51
|
+
API_MIN_VERSION=1,
|
52
|
+
ROOT_URLCONF="my_custom_urls",
|
53
|
+
)
|
54
|
+
def test_custom_settings() -> None:
|
55
|
+
"""
|
56
|
+
Ensures that custom settings override the default values.
|
57
|
+
"""
|
58
|
+
api_settings = APISettings(
|
59
|
+
API_BASE_PATH=settings.API_BASE_PATH,
|
60
|
+
API_MAX_VERSION=settings.API_MAX_VERSION,
|
61
|
+
API_MIN_VERSION=settings.API_MIN_VERSION,
|
62
|
+
ROOT_URLCONF=settings.ROOT_URLCONF,
|
63
|
+
)
|
64
|
+
|
65
|
+
assert api_settings.API_BASE_PATH == "api/v{version}/custom/"
|
66
|
+
assert api_settings.API_MAX_VERSION == 2
|
67
|
+
assert api_settings.API_MIN_VERSION == 1
|
68
|
+
assert api_settings.ROOT_URLCONF == "my_custom_urls"
|
69
|
+
|
70
|
+
@override_settings(API_BASE_PATH="api/v1/")
|
71
|
+
def test_invalid_api_base_path() -> None:
|
72
|
+
"""
|
73
|
+
Ensures that API_BASE_PATH without '{version}' raises a ValueError.
|
74
|
+
"""
|
75
|
+
with pytest.raises(VersioningError, match="API_BASE_PATH must contain '{version}'"):
|
76
|
+
APISettings(
|
77
|
+
API_BASE_PATH=settings.API_BASE_PATH,
|
78
|
+
API_MAX_VERSION=1,
|
79
|
+
API_MIN_VERSION=1,
|
80
|
+
ROOT_URLCONF="django_api_versioning.urls",
|
81
|
+
)
|
82
|
+
|
83
|
+
@override_settings(API_MIN_VERSION=2, API_MAX_VERSION=1)
|
84
|
+
def test_invalid_version_range() -> None:
|
85
|
+
"""
|
86
|
+
Ensures that API_MIN_VERSION > API_MAX_VERSION raises a ValueError.
|
87
|
+
"""
|
88
|
+
with pytest.raises(VersioningError, match="API_MIN_VERSION cannot be greater than API_MAX_VERSION"):
|
89
|
+
APISettings(
|
90
|
+
API_BASE_PATH="api/v{version}/",
|
91
|
+
API_MAX_VERSION=settings.API_MAX_VERSION,
|
92
|
+
API_MIN_VERSION=settings.API_MIN_VERSION,
|
93
|
+
ROOT_URLCONF="django_api_versioning.urls",
|
94
|
+
)
|
95
|
+
|
96
|
+
@override_settings(API_MIN_VERSION="one")
|
97
|
+
def test_api_min_version_is_not_integer() -> None:
|
98
|
+
"""
|
99
|
+
Ensures that API_MIN_VERSION is validated as an integer.
|
100
|
+
"""
|
101
|
+
with pytest.raises(VersionTypeError, match="API_MIN_VERSION must be an integer"):
|
102
|
+
APISettings(
|
103
|
+
API_BASE_PATH="api/v{version}/",
|
104
|
+
API_MAX_VERSION=1,
|
105
|
+
API_MIN_VERSION=settings.API_MIN_VERSION,
|
106
|
+
ROOT_URLCONF="django_api_versioning.urls",
|
107
|
+
)
|
108
|
+
|
109
|
+
@override_settings(API_MAX_VERSION="two")
|
110
|
+
def test_api_max_version_is_not_integer() -> None:
|
111
|
+
"""
|
112
|
+
Ensures that API_MAX_VERSION is validated as an integer.
|
113
|
+
"""
|
114
|
+
with pytest.raises(VersionTypeError, match="API_MAX_VERSION must be an integer"):
|
115
|
+
APISettings(
|
116
|
+
API_BASE_PATH="api/v{version}/",
|
117
|
+
API_MAX_VERSION=settings.API_MAX_VERSION,
|
118
|
+
API_MIN_VERSION=1,
|
119
|
+
ROOT_URLCONF="django_api_versioning.urls",
|
120
|
+
)
|
121
|
+
|
122
|
+
@override_settings(API_BASE_PATH="api/v{version}")
|
123
|
+
def test_validate_version_path_format() -> None:
|
124
|
+
"""
|
125
|
+
Ensures that API_BASE_PATH automatically ends with '/' if not already present.
|
126
|
+
"""
|
127
|
+
api_settings = APISettings(
|
128
|
+
API_BASE_PATH=settings.API_BASE_PATH,
|
129
|
+
API_MAX_VERSION=1,
|
130
|
+
API_MIN_VERSION=1,
|
131
|
+
ROOT_URLCONF="django_api_versioning.urls",
|
132
|
+
)
|
133
|
+
|
134
|
+
assert api_settings.API_BASE_PATH == "api/v{version}/"
|
135
|
+
|
136
|
+
@override_settings(ROOT_URLCONF=None)
|
137
|
+
def test_missing_root_urlconf() -> None:
|
138
|
+
"""
|
139
|
+
Ensures that missing ROOT_URLCONF raises an ImproperlyConfigured exception.
|
140
|
+
"""
|
141
|
+
with pytest.raises(ImproperlyConfigured, match="ROOT_URLCONF is required"):
|
142
|
+
APISettings(
|
143
|
+
API_BASE_PATH="api/v{version}/",
|
144
|
+
API_MAX_VERSION=1,
|
145
|
+
API_MIN_VERSION=1,
|
146
|
+
ROOT_URLCONF=settings.ROOT_URLCONF, # None
|
147
|
+
)
|
tests/test_urls.py
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
import pytest
|
2
|
+
from django.urls import URLPattern, resolve
|
3
|
+
|
4
|
+
|
5
|
+
@pytest.fixture(scope="module", autouse=True)
|
6
|
+
def clear_urlpatterns():
|
7
|
+
"""Ensure urlpatterns is cleared before each test."""
|
8
|
+
from django_api_versioning.urls import urlpatterns
|
9
|
+
urlpatterns.clear() # Clear any existing patterns
|
10
|
+
yield
|
11
|
+
|
12
|
+
@pytest.fixture(scope="module")
|
13
|
+
def register_routes():
|
14
|
+
from django_api_versioning.decorators import endpoint
|
15
|
+
|
16
|
+
# Registering the accounts route with the endpoint decorator
|
17
|
+
@endpoint("accounts" , view_name="accounts_view")
|
18
|
+
def test_accounts():
|
19
|
+
pass
|
20
|
+
|
21
|
+
yield
|
22
|
+
|
23
|
+
def test_urlpatterns_type():
|
24
|
+
"""Test to check that urlpatterns is a list of URLPattern."""
|
25
|
+
from django_api_versioning.urls import urlpatterns
|
26
|
+
|
27
|
+
assert isinstance(urlpatterns, list), "urlpatterns should be a list."
|
28
|
+
assert all(isinstance(pattern, URLPattern) for pattern in urlpatterns), "All elements in urlpatterns should be of type URLPattern."
|
29
|
+
|
30
|
+
def test_registry_urlpatterns(register_routes):
|
31
|
+
"""Test to check that the 'accounts' route resolves correctly."""
|
32
|
+
match = resolve('/accounts')
|
33
|
+
assert match.view_name == 'accounts_view', f"Expected 'accounts_view', but got {match.view_name}"
|