drf-query-method 0.1.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.
@@ -0,0 +1,20 @@
1
+ """Support for the HTTP QUERY method (RFC 10008) in Django REST framework.
2
+
3
+ This is a stop-gap until DRF supports ``QUERY`` itself
4
+ (encode/django-rest-framework#10091). It mirrors the names used there, so
5
+ moving to DRF's own support means removing this package rather than
6
+ rewriting views.
7
+
8
+ There are two ways to use it:
9
+
10
+ * Opt-in classes with the same names as DRF's, for example
11
+ ``drf_query_method.filters.SearchFilter``, imported in place of DRF's.
12
+ * :func:`patch`, which applies the same behaviour to DRF itself, and to
13
+ django-filter when it is installed.
14
+ """
15
+
16
+ from drf_query_method.methods import add_query_method
17
+ from drf_query_method.params import filter_params
18
+ from drf_query_method.patching import patch, unpatch
19
+
20
+ __all__ = ["add_query_method", "filter_params", "patch", "unpatch"]
@@ -0,0 +1,36 @@
1
+ """django-filter's DRF backend, reading QUERY request bodies.
2
+
3
+ Importing this module requires django-filter.
4
+ """
5
+
6
+ from django.http import QueryDict
7
+ from django_filters import rest_framework
8
+
9
+ from drf_query_method.params import filter_params
10
+
11
+
12
+ def as_filter_data(params):
13
+ """Stringify a QUERY body's scalar values, as a query string would deliver them.
14
+
15
+ django-filter's widgets expect strings. JSON numbers and booleans become
16
+ ``"5"`` and ``"True"``, which its fields parse as usual, rather than
17
+ reaching widgets that call ``.split(",")`` on them.
18
+ """
19
+ data = QueryDict(mutable=True)
20
+ for key, values in params.lists():
21
+ data.setlist(key, [value if value is None or isinstance(value, str) else str(value) for value in values])
22
+ return data
23
+
24
+
25
+ def get_filterset_kwargs(self, request, queryset, view, original):
26
+ kwargs = original(self, request, queryset, view)
27
+ if request.method == "QUERY":
28
+ kwargs["data"] = as_filter_data(filter_params(request))
29
+ return kwargs
30
+
31
+
32
+ class DjangoFilterBackend(rest_framework.DjangoFilterBackend):
33
+ def get_filterset_kwargs(self, request, queryset, view):
34
+ return get_filterset_kwargs(
35
+ self, request, queryset, view, rest_framework.DjangoFilterBackend.get_filterset_kwargs
36
+ )
@@ -0,0 +1,38 @@
1
+ """DRF's ``SearchFilter`` and ``OrderingFilter``, reading QUERY request bodies.
2
+
3
+ ``get_ordering`` is adapted from Django REST framework
4
+ (Copyright Encode OSS Ltd, BSD 3-Clause; see LICENSE).
5
+ """
6
+
7
+ from rest_framework import filters
8
+ from rest_framework.fields import CharField
9
+
10
+ from drf_query_method.params import filter_params
11
+
12
+
13
+ def get_search_terms(self, request):
14
+ """Search terms come from ``?search=...``, or a QUERY request's body, and may be whitespace delimited."""
15
+ value = filter_params(request).get(self.search_param, "")
16
+ field = CharField(trim_whitespace=False, allow_blank=True)
17
+ cleaned_value = field.run_validation(value)
18
+ return filters.search_smart_split(cleaned_value)
19
+
20
+
21
+ def get_ordering(self, request, queryset, view):
22
+ """Ordering comes from a comma delimited ``?ordering=...``, or a QUERY request's body."""
23
+ params = filter_params(request).get(self.ordering_param)
24
+ if params and isinstance(params, str):
25
+ fields = [param.strip() for param in params.split(",")]
26
+ ordering = self.remove_invalid_fields(queryset, fields, view, request)
27
+ if ordering:
28
+ return ordering
29
+
30
+ return self.get_default_ordering(view)
31
+
32
+
33
+ class SearchFilter(filters.SearchFilter):
34
+ get_search_terms = get_search_terms
35
+
36
+
37
+ class OrderingFilter(filters.OrderingFilter):
38
+ get_ordering = get_ordering
@@ -0,0 +1,16 @@
1
+ """Make Django accept the QUERY method on versions before 6.2."""
2
+
3
+ from django.views.generic import View
4
+
5
+
6
+ def add_query_method() -> None:
7
+ """Add ``query`` to Django's ``View.http_method_names`` if it is missing.
8
+
9
+ Django 6.2 lists ``query`` itself, so this does nothing there. On earlier
10
+ versions it lets any view, including DRF's, dispatch ``QUERY`` requests to
11
+ a ``query()`` handler. Earlier versions of Django's CSRF middleware treat
12
+ ``QUERY`` as unsafe, so session-authenticated ``QUERY`` requests need a
13
+ CSRF token until Django 6.2.
14
+ """
15
+ if "query" not in View.http_method_names:
16
+ View.http_method_names = [*View.http_method_names, "query"]
@@ -0,0 +1,48 @@
1
+ """The parameters filter backends should read for a request."""
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from django.http import QueryDict
6
+ from django.utils.datastructures import MultiValueDict
7
+ from rest_framework.exceptions import ParseError
8
+
9
+
10
+ def merge_filter_params(request) -> QueryDict:
11
+ """Return ``request.query_params`` with a QUERY request's body merged over it.
12
+
13
+ For any method other than ``QUERY`` this is ``request.query_params``
14
+ itself. A ``QUERY`` request carries its query in the request body, so the
15
+ parsed body is merged over the URL query parameters, and the body wins
16
+ for any key present in both. List values in a JSON body become multiple
17
+ values, as repeated query-string parameters do.
18
+
19
+ Raises ``ParseError`` (400) when the body is not an object of parameters,
20
+ such as a JSON array.
21
+ """
22
+ if request.method != "QUERY":
23
+ return request.query_params
24
+
25
+ body = request.data
26
+ if not isinstance(body, Mapping):
27
+ raise ParseError(f"QUERY request body must be an object of parameters, not {type(body).__name__}.")
28
+
29
+ if not isinstance(body, MultiValueDict):
30
+ body = MultiValueDict({key: value if isinstance(value, list) else [value] for key, value in body.items()})
31
+
32
+ params = request.query_params.copy()
33
+ for key, values in body.lists():
34
+ params.setlist(key, values)
35
+ return params
36
+
37
+
38
+ def filter_params(request) -> QueryDict:
39
+ """The parameters filter backends should read for ``request``.
40
+
41
+ Uses ``request.filter_params`` when DRF provides it, natively or through
42
+ :func:`drf_query_method.patch`, and computes it otherwise. Custom filter
43
+ backends should read their parameters through this rather than
44
+ ``request.query_params``, so that they support ``QUERY`` requests.
45
+ """
46
+ if hasattr(type(request), "filter_params"):
47
+ return request.filter_params
48
+ return merge_filter_params(request)
@@ -0,0 +1,107 @@
1
+ """Apply QUERY support to DRF itself, and to django-filter when installed.
2
+
3
+ :func:`patch` makes DRF behave as if encode/django-rest-framework#10091 were
4
+ merged, so existing views, routers and settings pick up ``QUERY`` without
5
+ changing imports. Call it once at startup, for example from an
6
+ ``AppConfig.ready()``. It does nothing to DRF once DRF supports ``QUERY``
7
+ itself.
8
+ """
9
+
10
+ import importlib.util
11
+
12
+ from drf_query_method.methods import add_query_method
13
+
14
+ _MISSING = object()
15
+ _applied: list[tuple[object, str, object]] = []
16
+
17
+
18
+ def _set(owner, name: str, value) -> None:
19
+ _applied.append((owner, name, owner.__dict__.get(name, _MISSING)))
20
+ setattr(owner, name, value)
21
+
22
+
23
+ def _patch_drf() -> None:
24
+ from rest_framework import decorators, filters, permissions, routers, test, viewsets
25
+ from rest_framework.request import Request
26
+ from rest_framework.schemas.generators import EndpointEnumerator
27
+
28
+ from drf_query_method import filters as query_filters
29
+ from drf_query_method import permissions as query_permissions
30
+ from drf_query_method import test as query_test
31
+ from drf_query_method.params import merge_filter_params
32
+ from drf_query_method.routers import get_routes_keeping_query_actions, with_query_route
33
+ from drf_query_method.viewsets import drop_unmapped_query
34
+
35
+ _set(Request, "filter_params", property(merge_filter_params))
36
+ _set(filters.SearchFilter, "get_search_terms", query_filters.get_search_terms)
37
+ _set(filters.OrderingFilter, "get_ordering", query_filters.get_ordering)
38
+
39
+ _set(permissions, "SAFE_METHODS", query_permissions.SAFE_METHODS)
40
+ for permission_class in (permissions.DjangoModelPermissions, permissions.DjangoObjectPermissions):
41
+ _set(permission_class, "perms_map", {**permission_class.perms_map, "QUERY": []})
42
+
43
+ _set(routers.SimpleRouter, "routes", with_query_route(routers.SimpleRouter.routes))
44
+ simple_get_routes = routers.SimpleRouter.get_routes
45
+ _set(
46
+ routers.SimpleRouter,
47
+ "get_routes",
48
+ lambda router, viewset: get_routes_keeping_query_actions(router, viewset, simple_get_routes),
49
+ )
50
+
51
+ def dispatch(self, request, *args, **kwargs):
52
+ drop_unmapped_query(self)
53
+ return super(viewsets.ViewSetMixin, self).dispatch(request, *args, **kwargs)
54
+
55
+ _set(viewsets.ViewSetMixin, "dispatch", dispatch)
56
+ _set(decorators.MethodMapper, "query", lambda self, func: self._map("query", func))
57
+ _set(test.APIRequestFactory, "query", query_test.factory_query)
58
+ _set(test.APIClient, "query", query_test.client_query)
59
+
60
+ get_allowed_methods = EndpointEnumerator.get_allowed_methods
61
+ _set(
62
+ EndpointEnumerator,
63
+ "get_allowed_methods",
64
+ lambda self, callback: [method for method in get_allowed_methods(self, callback) if method != "QUERY"],
65
+ )
66
+
67
+
68
+ def _patch_django_filter() -> None:
69
+ from django_filters.rest_framework import DjangoFilterBackend
70
+
71
+ from drf_query_method.django_filters import get_filterset_kwargs
72
+
73
+ original = DjangoFilterBackend.get_filterset_kwargs
74
+ _set(
75
+ DjangoFilterBackend,
76
+ "get_filterset_kwargs",
77
+ lambda self, request, queryset, view: get_filterset_kwargs(self, request, queryset, view, original),
78
+ )
79
+
80
+
81
+ def patch() -> None:
82
+ """Add QUERY support to Django, DRF and django-filter in place.
83
+
84
+ Safe to call more than once. On Django versions before 6.2 this also
85
+ calls :func:`~drf_query_method.add_query_method`, which :func:`unpatch`
86
+ does not undo.
87
+ """
88
+ if _applied:
89
+ return
90
+
91
+ from rest_framework.request import Request
92
+
93
+ add_query_method()
94
+ if not hasattr(Request, "filter_params"):
95
+ _patch_drf()
96
+ if importlib.util.find_spec("django_filters") is not None:
97
+ _patch_django_filter()
98
+
99
+
100
+ def unpatch() -> None:
101
+ """Undo :func:`patch`, mainly for tests."""
102
+ while _applied:
103
+ owner, name, original = _applied.pop()
104
+ if original is _MISSING:
105
+ delattr(owner, name)
106
+ else:
107
+ setattr(owner, name, original)
@@ -0,0 +1,45 @@
1
+ """DRF permission classes that treat QUERY as a safe method.
2
+
3
+ ``has_object_permission`` is adapted from Django REST framework
4
+ (Copyright Encode OSS Ltd, BSD 3-Clause; see LICENSE).
5
+ """
6
+
7
+ from django.http import Http404
8
+ from rest_framework import permissions
9
+
10
+ SAFE_METHODS = ("GET", "HEAD", "OPTIONS", "QUERY")
11
+
12
+
13
+ class IsAuthenticatedOrReadOnly(permissions.IsAuthenticatedOrReadOnly):
14
+ def has_permission(self, request, view):
15
+ return bool(request.method in SAFE_METHODS or (request.user and request.user.is_authenticated))
16
+
17
+
18
+ class DjangoModelPermissions(permissions.DjangoModelPermissions):
19
+ perms_map = {**permissions.DjangoModelPermissions.perms_map, "QUERY": []}
20
+
21
+
22
+ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
23
+ authenticated_users_only = False
24
+
25
+
26
+ class DjangoObjectPermissions(permissions.DjangoObjectPermissions):
27
+ perms_map = {**permissions.DjangoObjectPermissions.perms_map, "QUERY": []}
28
+
29
+ def has_object_permission(self, request, view, obj):
30
+ queryset = self._queryset(view)
31
+ model_cls = queryset.model
32
+ user = request.user
33
+
34
+ perms = self.get_required_object_permissions(request.method, model_cls)
35
+ if user.has_perms(perms, obj):
36
+ return True
37
+
38
+ # A user without read permission sees a 404 rather than a 403.
39
+ if request.method in SAFE_METHODS:
40
+ raise Http404
41
+
42
+ read_perms = self.get_required_object_permissions("GET", model_cls)
43
+ if not user.has_perms(read_perms, obj):
44
+ raise Http404
45
+ return False
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561
@@ -0,0 +1,61 @@
1
+ """DRF routers that route QUERY to a viewset's ``query()`` on the list route."""
2
+
3
+ import copy
4
+
5
+ from rest_framework import routers
6
+
7
+
8
+ def _is_list_route(route) -> bool:
9
+ return isinstance(route, routers.Route) and route.mapping.get("get") == "list"
10
+
11
+
12
+ def with_query_route(routes):
13
+ """Map QUERY to ``query`` on the list route of ``routes``."""
14
+ return [
15
+ route._replace(mapping={**route.mapping, "query": "query"}) if _is_list_route(route) else route
16
+ for route in routes
17
+ ]
18
+
19
+
20
+ def without_query_route(routes):
21
+ """Remove the QUERY mapping from every route in ``routes``."""
22
+ return [
23
+ route._replace(mapping={method: action for method, action in route.mapping.items() if action != "query"})
24
+ if isinstance(route, routers.Route)
25
+ else route
26
+ for route in routes
27
+ ]
28
+
29
+
30
+ def has_query_action(viewset) -> bool:
31
+ """Whether the viewset already has an ``@action`` named ``query``."""
32
+ return any(action.__name__ == "query" for action in viewset.get_extra_actions())
33
+
34
+
35
+ def get_routes_keeping_query_actions(router, viewset, get_routes):
36
+ """Call ``get_routes``, leaving QUERY unrouted when the viewset has an ``@action`` named ``query``.
37
+
38
+ That action keeps its own route, and DRF would otherwise reject it for
39
+ clashing with the built-in ``query`` route.
40
+ """
41
+ if not has_query_action(viewset):
42
+ return get_routes(router, viewset)
43
+
44
+ router_without_query = copy.copy(router)
45
+ router_without_query.routes = without_query_route(router.routes)
46
+ return get_routes(router_without_query, viewset)
47
+
48
+
49
+ class QueryRouterMixin:
50
+ routes = with_query_route(routers.SimpleRouter.routes)
51
+
52
+ def get_routes(self, viewset):
53
+ return get_routes_keeping_query_actions(self, viewset, super(QueryRouterMixin, type(self)).get_routes) # ty: ignore[unresolved-attribute]
54
+
55
+
56
+ class SimpleRouter(QueryRouterMixin, routers.SimpleRouter):
57
+ pass
58
+
59
+
60
+ class DefaultRouter(QueryRouterMixin, routers.DefaultRouter):
61
+ pass
@@ -0,0 +1,41 @@
1
+ """DRF's test request factory and client, with a ``.query()`` method."""
2
+
3
+ from rest_framework import test
4
+
5
+
6
+ def factory_query(self, path, data=None, format=None, content_type=None, **extra):
7
+ """Build a QUERY request, encoding ``data`` as ``.post()`` does."""
8
+ data, content_type = self._encode_data(data, format, content_type)
9
+ return self.generic("QUERY", path, data, content_type, **extra)
10
+
11
+
12
+ def client_query(self, path, data=None, format=None, content_type=None, follow=False, **extra):
13
+ """Send a QUERY request, encoding ``data`` as ``.post()`` does."""
14
+ response = factory_query(self, path, data=data, format=format, content_type=content_type, **extra)
15
+ if follow:
16
+ response = self._handle_redirects(response, data=data, format=format, content_type=content_type, **extra)
17
+ return response
18
+
19
+
20
+ class APIRequestFactory(test.APIRequestFactory):
21
+ query = factory_query
22
+
23
+
24
+ class APIClient(test.APIClient):
25
+ query = client_query
26
+
27
+
28
+ class APISimpleTestCase(test.APISimpleTestCase):
29
+ client_class = APIClient
30
+
31
+
32
+ class APITransactionTestCase(test.APITransactionTestCase):
33
+ client_class = APIClient
34
+
35
+
36
+ class APITestCase(test.APITestCase):
37
+ client_class = APIClient
38
+
39
+
40
+ class APILiveServerTestCase(test.APILiveServerTestCase):
41
+ client_class = APIClient
@@ -0,0 +1,36 @@
1
+ """DRF viewsets that only answer QUERY on routes that map it."""
2
+
3
+ from rest_framework import viewsets
4
+
5
+
6
+ def drop_unmapped_query(view) -> None:
7
+ """Stop a viewset answering QUERY on a route whose actions don't map it.
8
+
9
+ A viewset's ``query()`` method has the same name as the QUERY handler, so
10
+ without this every route of a viewset that defines ``query()`` would
11
+ answer QUERY, including detail routes.
12
+ """
13
+ if "query" not in (getattr(view, "action_map", None) or {}):
14
+ view.http_method_names = [method for method in view.http_method_names if method != "query"]
15
+
16
+
17
+ class QueryViewSetMixin:
18
+ def dispatch(self, request, *args, **kwargs):
19
+ drop_unmapped_query(self)
20
+ return super().dispatch(request, *args, **kwargs) # ty: ignore[unresolved-attribute]
21
+
22
+
23
+ class ViewSet(QueryViewSetMixin, viewsets.ViewSet):
24
+ pass
25
+
26
+
27
+ class GenericViewSet(QueryViewSetMixin, viewsets.GenericViewSet):
28
+ pass
29
+
30
+
31
+ class ReadOnlyModelViewSet(QueryViewSetMixin, viewsets.ReadOnlyModelViewSet):
32
+ pass
33
+
34
+
35
+ class ModelViewSet(QueryViewSetMixin, viewsets.ModelViewSet):
36
+ pass
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.5
2
+ Name: drf-query-method
3
+ Version: 0.1.0
4
+ Summary: HTTP QUERY method (RFC 10008) support for Django REST framework.
5
+ Project-URL: bugs, https://github.com/BenA-SA/drf-query-method/issues
6
+ Project-URL: changelog, https://github.com/BenA-SA/drf-query-method/releases
7
+ Project-URL: documentation, https://BenA-SA.github.io/drf-query-method/
8
+ Project-URL: homepage, https://github.com/BenA-SA/drf-query-method
9
+ Author-email: Ben Atkinson <ben@streamlinedanalytics.co.uk>
10
+ Maintainer-email: Ben Atkinson <ben@streamlinedanalytics.co.uk>
11
+ License-Expression: BSD-3-Clause
12
+ License-File: LICENSE
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Django :: 4.2
17
+ Classifier: Framework :: Django :: 5.0
18
+ Classifier: Framework :: Django :: 5.1
19
+ Classifier: Framework :: Django :: 5.2
20
+ Classifier: Framework :: Django :: 6.0
21
+ Classifier: Framework :: Django :: 6.1
22
+ Classifier: Intended Audience :: Developers
23
+ Classifier: Operating System :: OS Independent
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Programming Language :: Python :: 3.14
30
+ Classifier: Topic :: Internet :: WWW/HTTP
31
+ Classifier: Typing :: Typed
32
+ Requires-Python: >=3.10
33
+ Requires-Dist: django>=4.2
34
+ Requires-Dist: djangorestframework>=3.16
35
+ Provides-Extra: django-filter
36
+ Requires-Dist: django-filter; extra == 'django-filter'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # drf-query-method
40
+
41
+ [![PyPI version](https://img.shields.io/pypi/v/drf-query-method.svg)](https://pypi.org/project/drf-query-method/)
42
+
43
+ Support for the HTTP `QUERY` method ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008.html)) in Django REST framework.
44
+
45
+ > [!NOTE]
46
+ > **This is a stop-gap.** Support for `QUERY` has been proposed for Django REST framework itself in
47
+ > [encode/django-rest-framework#10091](https://github.com/encode/django-rest-framework/pull/10091).
48
+ > If it is merged, use DRF's built-in support instead. This package exists so you can use `QUERY` in the
49
+ > meantime, and on Django versions before 6.2. It mirrors the names in that PR (`query()` handlers and
50
+ > `request.filter_params`), so switching over should mean removing this package rather than rewriting your views.
51
+
52
+ `QUERY` is safe and idempotent like `GET`, but carries a request body like `POST`. It suits searches whose
53
+ parameters are too long or too structured for a URL.
54
+
55
+ * [GitHub](https://github.com/BenA-SA/drf-query-method/) | [PyPI](https://pypi.org/project/drf-query-method/) | [Documentation](https://BenA-SA.github.io/drf-query-method/)
56
+ * Created by [Ben Atkinson](https://streamlinedanalytics.co.uk) | GitHub [@BenA-SA](https://github.com/BenA-SA) | PyPI [@Ben-SA](https://pypi.org/user/Ben-SA/)
57
+ * BSD 3-Clause License
58
+
59
+ ## Usage
60
+
61
+ Define a `query()` handler, then call `patch()` once at startup:
62
+
63
+ ```python
64
+ import drf_query_method
65
+
66
+ drf_query_method.patch()
67
+
68
+
69
+ class NoteViewSet(ModelViewSet):
70
+ ...
71
+
72
+ def query(self, request, *args, **kwargs):
73
+ return self.list(request, *args, **kwargs)
74
+ ```
75
+
76
+ Routers then send `QUERY /notes/` to `query()`, and `SearchFilter`, `OrderingFilter` and django-filter's
77
+ `DjangoFilterBackend` read their parameters from the request body. If you'd rather not patch DRF, import drop-in
78
+ classes with the same names from `drf_query_method` instead. See the [documentation](https://BenA-SA.github.io/drf-query-method/usage/)
79
+ for both approaches, and for sending `QUERY` requests from JavaScript and other clients.
80
+
81
+ The package is in early development and not yet released.
82
+
83
+ ## Compatibility
84
+
85
+ * Python 3.10+
86
+ * Django 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1. Django 6.2 adds `QUERY` natively; on earlier versions this package adds it.
87
+ * Django REST framework 3.16+
88
+ * django-filter, optionally
89
+
90
+ ## Installation
91
+
92
+ ```bash
93
+ pip install drf-query-method
94
+ ```
95
+
96
+ ## Contributing
97
+
98
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and release instructions.
@@ -0,0 +1,15 @@
1
+ drf_query_method/__init__.py,sha256=qXQXHtGtBCCGmyWF7Z1BkGJuGDzuKpM-htSYLEVeYPY,795
2
+ drf_query_method/django_filters.py,sha256=BKiQeay-uWMmbSnoVp-XsegNLaKdmi15Ttl4NO5-klw,1285
3
+ drf_query_method/filters.py,sha256=TBsBbOMMkZkKzqdk326Q5zl7zW0hVO5E-O4aQSqzvbs,1350
4
+ drf_query_method/methods.py,sha256=xM_xg0CkXqRMhrGFrZjVLzdPPsQlnsFR8Mec9yy8Z9M,684
5
+ drf_query_method/params.py,sha256=X1JB9idk7hkP6s6map_IVvCVpkM7yfigktqaFUxObnQ,1911
6
+ drf_query_method/patching.py,sha256=tCmSDbWYBaYlj9njuY6wNWM1A1uVWIgMXFYLGgz8N34,4006
7
+ drf_query_method/permissions.py,sha256=AS1fv8pP9afi38OnRmJoyr0Y-AIzwgKZjALzmgLH1VM,1552
8
+ drf_query_method/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
9
+ drf_query_method/routers.py,sha256=LICu8HKhVDA_aj0bh5ghAvBP40xkjDSPRRqpQliPv98,1991
10
+ drf_query_method/test.py,sha256=jTGzfZUA0jS-GszkL-l_NfHIqBTkq0fq_irwZ8jCvfM,1286
11
+ drf_query_method/viewsets.py,sha256=fRtTk8wW34MEHysLjW4uXjbGwd6M91CHiuvNwd7QoRI,1095
12
+ drf_query_method-0.1.0.dist-info/METADATA,sha256=73xCRhf0_GC04-JIImO-onu1K0ND_VSF2nIrGFpJ_IU,4116
13
+ drf_query_method-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
14
+ drf_query_method-0.1.0.dist-info/licenses/LICENSE,sha256=oBYuNHwE3Dt5IZYkSGjRcgTMS0X3CfC_-ZhlcLUqXI0,3181
15
+ drf_query_method-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,61 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Ben Atkinson
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ ---
31
+
32
+ Parts of drf_query_method/filters.py and drf_query_method/permissions.py are
33
+ adapted from Django REST framework, which carries the following licence:
34
+
35
+ Copyright © 2011-present, Encode OSS Ltd (https://www.encode.io/).
36
+ All rights reserved.
37
+
38
+ Redistribution and use in source and binary forms, with or without
39
+ modification, are permitted provided that the following conditions are met:
40
+
41
+ * Redistributions of source code must retain the above copyright notice, this
42
+ list of conditions and the following disclaimer.
43
+
44
+ * Redistributions in binary form must reproduce the above copyright notice,
45
+ this list of conditions and the following disclaimer in the documentation
46
+ and/or other materials provided with the distribution.
47
+
48
+ * Neither the name of the copyright holder nor the names of its
49
+ contributors may be used to endorse or promote products derived from
50
+ this software without specific prior written permission.
51
+
52
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
53
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
54
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
55
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
56
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
57
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
58
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
59
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
60
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
61
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.