django-datastar 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.
- django_datastar/__init__.py +13 -0
- django_datastar/apps.py +8 -0
- django_datastar/middleware.py +80 -0
- django_datastar/py.typed +0 -0
- django_datastar/static/django_datastar/datastar-csrf.js +154 -0
- django_datastar/templatetags/__init__.py +0 -0
- django_datastar/templatetags/django_datastar.py +44 -0
- django_datastar-0.1.0.dist-info/METADATA +170 -0
- django_datastar-0.1.0.dist-info/RECORD +11 -0
- django_datastar-0.1.0.dist-info/WHEEL +4 -0
- django_datastar-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .middleware import DatastarDetails
|
|
4
|
+
from .middleware import DatastarHttpRequest
|
|
5
|
+
from .middleware import DatastarMiddleware
|
|
6
|
+
from .middleware import is_datastar
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"DatastarDetails",
|
|
10
|
+
"DatastarHttpRequest",
|
|
11
|
+
"DatastarMiddleware",
|
|
12
|
+
"is_datastar",
|
|
13
|
+
]
|
django_datastar/apps.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from asgiref.sync import iscoroutinefunction
|
|
8
|
+
from asgiref.sync import markcoroutinefunction
|
|
9
|
+
from django.http import HttpRequest
|
|
10
|
+
from django.http.response import HttpResponseBase
|
|
11
|
+
|
|
12
|
+
_SyncGetResponse = Callable[[HttpRequest], HttpResponseBase]
|
|
13
|
+
_AsyncGetResponse = Callable[[HttpRequest], Awaitable[HttpResponseBase]]
|
|
14
|
+
_GetResponse = _SyncGetResponse | _AsyncGetResponse
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_datastar(request: HttpRequest) -> bool:
|
|
18
|
+
"""Return whether a request has Datastar's canonical marker header.
|
|
19
|
+
|
|
20
|
+
The marker is client-controlled and must not authorize a request or bypass
|
|
21
|
+
CSRF. Cacheable views that vary by this result should use
|
|
22
|
+
``@vary_on_headers("Datastar-Request")``.
|
|
23
|
+
"""
|
|
24
|
+
return request.headers.get("Datastar-Request") == "true"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DatastarDetails:
|
|
28
|
+
"""Datastar metadata attached to a Django request."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, request: HttpRequest) -> None:
|
|
31
|
+
self.request = request
|
|
32
|
+
|
|
33
|
+
def __bool__(self) -> bool:
|
|
34
|
+
return is_datastar(self.request)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DatastarHttpRequest(HttpRequest):
|
|
38
|
+
"""Typing contract for a request processed by :class:`DatastarMiddleware`.
|
|
39
|
+
|
|
40
|
+
Django continues to create the request object. Use this class as a view
|
|
41
|
+
annotation when the middleware is installed; do not use it for runtime
|
|
42
|
+
``isinstance`` checks.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
datastar: DatastarDetails
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DatastarMiddleware:
|
|
49
|
+
"""Attach Datastar request metadata in sync and async middleware chains."""
|
|
50
|
+
|
|
51
|
+
sync_capable = True
|
|
52
|
+
async_capable = True
|
|
53
|
+
|
|
54
|
+
def __init__(self, get_response: _GetResponse) -> None:
|
|
55
|
+
self.get_response = get_response
|
|
56
|
+
self._async_mode = iscoroutinefunction(get_response)
|
|
57
|
+
|
|
58
|
+
if self._async_mode:
|
|
59
|
+
markcoroutinefunction(self)
|
|
60
|
+
|
|
61
|
+
def __call__(
|
|
62
|
+
self,
|
|
63
|
+
request: HttpRequest,
|
|
64
|
+
) -> HttpResponseBase | Awaitable[HttpResponseBase]:
|
|
65
|
+
if self._async_mode:
|
|
66
|
+
return self.__acall__(request)
|
|
67
|
+
|
|
68
|
+
self._attach_details(request)
|
|
69
|
+
get_response = cast("_SyncGetResponse", self.get_response)
|
|
70
|
+
return get_response(request)
|
|
71
|
+
|
|
72
|
+
async def __acall__(self, request: HttpRequest) -> HttpResponseBase:
|
|
73
|
+
self._attach_details(request)
|
|
74
|
+
get_response = cast("_AsyncGetResponse", self.get_response)
|
|
75
|
+
return await get_response(request)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _attach_details(request: HttpRequest) -> None:
|
|
79
|
+
typed_request = cast("DatastarHttpRequest", request)
|
|
80
|
+
typed_request.datastar = DatastarDetails(request)
|
django_datastar/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const BRIDGE_INSTALLATION = Symbol.for("django-datastar.csrf-bridge");
|
|
2
|
+
const CSRF_HEADER = "X-CSRFToken";
|
|
3
|
+
const CSRF_META_SELECTOR = 'meta[name="datastar-csrf-token"]';
|
|
4
|
+
const DATASTAR_REQUEST_HEADER = "Datastar-Request";
|
|
5
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
|
6
|
+
const REQUEST_URL_GETTER = Object.getOwnPropertyDescriptor(
|
|
7
|
+
Request.prototype,
|
|
8
|
+
"url",
|
|
9
|
+
).get;
|
|
10
|
+
|
|
11
|
+
function installDatastarCsrfBridge() {
|
|
12
|
+
if (window[BRIDGE_INSTALLATION]) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const nativeFetch = window.fetch.bind(window);
|
|
17
|
+
window.fetch = createDatastarCsrfFetch(nativeFetch);
|
|
18
|
+
Object.defineProperty(window, BRIDGE_INSTALLATION, { value: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createDatastarCsrfFetch(nativeFetch) {
|
|
22
|
+
return (...args) => {
|
|
23
|
+
const [input, init] = args;
|
|
24
|
+
let csrfInit;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
csrfInit = csrfRequestInitFor(input, init);
|
|
28
|
+
} catch {
|
|
29
|
+
return nativeFetch(...args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!csrfInit) {
|
|
33
|
+
return nativeFetch(...args);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return nativeFetch(input, csrfInit);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function csrfRequestInitFor(input, init) {
|
|
41
|
+
if (hasAccessorRequestInitMember(init)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const request = effectiveRequest(input, init);
|
|
46
|
+
if (!requiresCsrfHeader(request)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const token = csrfTokenFromDom();
|
|
51
|
+
return token ? withCsrfHeader(init, request.headers, token) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function effectiveRequest(input, init) {
|
|
55
|
+
return {
|
|
56
|
+
input,
|
|
57
|
+
init,
|
|
58
|
+
headers: effectiveHeaders(input, init),
|
|
59
|
+
method: effectiveMethod(input, init),
|
|
60
|
+
url: effectiveUrl(input),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requiresCsrfHeader(request) {
|
|
65
|
+
return (
|
|
66
|
+
request.headers.get(DATASTAR_REQUEST_HEADER) === "true" &&
|
|
67
|
+
!request.headers.has(CSRF_HEADER) &&
|
|
68
|
+
!SAFE_METHODS.has(request.method) &&
|
|
69
|
+
request.url.origin === window.location.origin &&
|
|
70
|
+
hasCompatibleMode(request.input, request.init)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function withCsrfHeader(init, headers, token) {
|
|
75
|
+
headers.set(CSRF_HEADER, token);
|
|
76
|
+
const csrfInit = Object.create(init ?? null);
|
|
77
|
+
Object.defineProperties(csrfInit, {
|
|
78
|
+
headers: { enumerable: true, value: headers },
|
|
79
|
+
mode: { enumerable: true, value: "same-origin" },
|
|
80
|
+
});
|
|
81
|
+
return csrfInit;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function effectiveHeaders(input, init) {
|
|
85
|
+
if (init?.headers !== undefined) {
|
|
86
|
+
return new Headers(init.headers);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new Headers(isRequest(input) ? input.headers : undefined);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function effectiveMethod(input, init) {
|
|
93
|
+
const initMethod = init?.method;
|
|
94
|
+
const method =
|
|
95
|
+
initMethod !== undefined
|
|
96
|
+
? initMethod
|
|
97
|
+
: isRequest(input)
|
|
98
|
+
? input.method
|
|
99
|
+
: "GET";
|
|
100
|
+
return String(method).toUpperCase();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function effectiveUrl(input) {
|
|
104
|
+
const url = isRequest(input) ? input.url : input;
|
|
105
|
+
return new URL(url, document.baseURI);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasCompatibleMode(input, init) {
|
|
109
|
+
if (init?.mode !== undefined) {
|
|
110
|
+
return init.mode === "same-origin";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return !isRequest(input) || input.mode === "same-origin";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function csrfTokenFromDom() {
|
|
117
|
+
return document.querySelector(CSRF_META_SELECTOR)?.getAttribute("content") || "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hasAccessorRequestInitMember(init) {
|
|
121
|
+
if (init === undefined || init === null) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof init !== "object" && typeof init !== "function") {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let owner = init;
|
|
130
|
+
while (owner !== null && owner !== Object.prototype) {
|
|
131
|
+
const descriptors = Object.values(Object.getOwnPropertyDescriptors(owner));
|
|
132
|
+
if (
|
|
133
|
+
descriptors.some(
|
|
134
|
+
(descriptor) =>
|
|
135
|
+
descriptor.get !== undefined || descriptor.set !== undefined,
|
|
136
|
+
)
|
|
137
|
+
) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
owner = Object.getPrototypeOf(owner);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isRequest(input) {
|
|
146
|
+
try {
|
|
147
|
+
REQUEST_URL_GETTER.call(input);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
installDatastarCsrfBridge();
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django import template
|
|
4
|
+
from django.http import HttpRequest
|
|
5
|
+
from django.middleware.csrf import get_token
|
|
6
|
+
from django.templatetags.static import static
|
|
7
|
+
from django.utils.html import escape
|
|
8
|
+
from django.utils.html import format_html
|
|
9
|
+
from django.utils.safestring import SafeString
|
|
10
|
+
|
|
11
|
+
register = template.Library()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register.simple_tag(takes_context=True)
|
|
15
|
+
def datastar_csrf(
|
|
16
|
+
context: template.Context,
|
|
17
|
+
nonce: object | None = None,
|
|
18
|
+
) -> SafeString:
|
|
19
|
+
"""Render the masked CSRF token and Datastar fetch bridge module."""
|
|
20
|
+
request = getattr(context, "request", None)
|
|
21
|
+
if not isinstance(request, HttpRequest):
|
|
22
|
+
raise template.TemplateSyntaxError(
|
|
23
|
+
"{% datastar_csrf %} requires a request-aware template context."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
token = get_token(request)
|
|
27
|
+
meta = format_html(
|
|
28
|
+
'<meta name="datastar-csrf-token" content="{}" />',
|
|
29
|
+
token,
|
|
30
|
+
)
|
|
31
|
+
script_url = static("django_datastar/datastar-csrf.js")
|
|
32
|
+
script = _script_element(script_url, nonce)
|
|
33
|
+
return format_html("{}\n{}", meta, script)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _script_element(script_url: str, nonce: object | None) -> SafeString:
|
|
37
|
+
if nonce is None:
|
|
38
|
+
return format_html('<script type="module" src="{}"></script>', script_url)
|
|
39
|
+
|
|
40
|
+
return format_html(
|
|
41
|
+
'<script type="module" src="{}" nonce="{}"></script>',
|
|
42
|
+
script_url,
|
|
43
|
+
escape(nonce),
|
|
44
|
+
)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-datastar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Request metadata and opt-in CSRF integration for Django and Datastar
|
|
5
|
+
Project-URL: Documentation, https://django-datastar.readthedocs.io/
|
|
6
|
+
Project-URL: Repository, https://github.com/MarcusL11/django_datastar
|
|
7
|
+
Project-URL: Issues, https://github.com/MarcusL11/django_datastar/issues
|
|
8
|
+
Author-email: "Marcus A. Lee" <hello@marcusalee.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 5.2
|
|
14
|
+
Classifier: Framework :: Django :: 6.0
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: django<6.1,>=5.2
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: furo>=2024.8.6; extra == 'docs'
|
|
25
|
+
Requires-Dist: sphinx>=8.1; extra == 'docs'
|
|
26
|
+
Provides-Extra: tests
|
|
27
|
+
Requires-Dist: django-stubs>=5.2; extra == 'tests'
|
|
28
|
+
Requires-Dist: mypy>=1.15; extra == 'tests'
|
|
29
|
+
Requires-Dist: pytest-django>=4.10; extra == 'tests'
|
|
30
|
+
Requires-Dist: pytest>=8.3; extra == 'tests'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# django-datastar
|
|
34
|
+
|
|
35
|
+
`django-datastar` provides small, focused integration points between Django and
|
|
36
|
+
[Datastar](https://data-star.dev/):
|
|
37
|
+
|
|
38
|
+
- exact request classification through `Datastar-Request: true`;
|
|
39
|
+
- sync/async middleware-backed `request.datastar` metadata; and
|
|
40
|
+
- an opt-in bridge that supplies Django CSRF tokens to qualifying Datastar
|
|
41
|
+
backend-action requests.
|
|
42
|
+
|
|
43
|
+
It does not provide Datastar response or SSE APIs. Use
|
|
44
|
+
[`datastar-py`](https://github.com/starfederation/datastar/tree/main/sdk/python)
|
|
45
|
+
as a companion when those APIs are needed.
|
|
46
|
+
|
|
47
|
+
> The `Datastar-Request` header is client-controlled metadata. Never use it for
|
|
48
|
+
> authentication, authorization, permissions, or a CSRF bypass.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Install the package from PyPI:
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
python -m pip install django-datastar
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To install a development checkout instead, run `python -m pip install .` from
|
|
59
|
+
the repository root.
|
|
60
|
+
|
|
61
|
+
Add the middleware before Django's CSRF middleware:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
MIDDLEWARE = [
|
|
65
|
+
# ...
|
|
66
|
+
"django_datastar.middleware.DatastarMiddleware",
|
|
67
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
68
|
+
# ...
|
|
69
|
+
]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use either the helper or the attached details in a view:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from django.http import HttpResponse
|
|
76
|
+
from django_datastar import DatastarHttpRequest
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def update(request: DatastarHttpRequest) -> HttpResponse:
|
|
80
|
+
if request.datastar:
|
|
81
|
+
return HttpResponse("Datastar request")
|
|
82
|
+
return HttpResponse("ordinary request")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`DatastarHttpRequest` is an annotation for requests processed by the middleware;
|
|
86
|
+
Django still creates the actual request object.
|
|
87
|
+
|
|
88
|
+
When middleware-backed request details are not needed, classify a request directly:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from django.http import HttpRequest
|
|
92
|
+
from django.http import HttpResponse
|
|
93
|
+
from django_datastar import is_datastar
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def update(request: HttpRequest) -> HttpResponse:
|
|
97
|
+
if is_datastar(request):
|
|
98
|
+
return HttpResponse("Datastar request")
|
|
99
|
+
return HttpResponse("ordinary request")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Automatic CSRF headers
|
|
103
|
+
|
|
104
|
+
The bridge is optional. Add the app when you want its template tag and static
|
|
105
|
+
module:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
INSTALLED_APPS = [
|
|
109
|
+
# ...
|
|
110
|
+
"django_datastar",
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Load the tag before your chosen Datastar bundle:
|
|
115
|
+
|
|
116
|
+
```django
|
|
117
|
+
{% load django_datastar static %}
|
|
118
|
+
|
|
119
|
+
{% datastar_csrf %}
|
|
120
|
+
<script type="module" src="{% static 'js/datastar.js' %}"></script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For nonce-based Content Security Policies:
|
|
124
|
+
|
|
125
|
+
```django
|
|
126
|
+
{% datastar_csrf nonce=request.csp_nonce %}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The tag calls Django's CSRF token machinery and emits a masked token in the DOM,
|
|
130
|
+
so it works with `CSRF_COOKIE_HTTPONLY=True`. The external module reads that token
|
|
131
|
+
at request time and injects `X-CSRFToken` only when all of these conditions hold:
|
|
132
|
+
|
|
133
|
+
- the effective method is unsafe;
|
|
134
|
+
- `Datastar-Request` is exactly `true`;
|
|
135
|
+
- the effective target is same-origin;
|
|
136
|
+
- the request mode is compatible with `same-origin`;
|
|
137
|
+
- no CSRF header was supplied explicitly; and
|
|
138
|
+
- a DOM token is present.
|
|
139
|
+
|
|
140
|
+
Django's `CsrfViewMiddleware` remains solely responsible for validation. The
|
|
141
|
+
bridge assumes Datastar resolves `window.fetch` at request time; see the
|
|
142
|
+
[compatibility documentation](https://django-datastar.readthedocs.io/en/latest/compatibility.html)
|
|
143
|
+
before upgrading Datastar.
|
|
144
|
+
|
|
145
|
+
## Documentation
|
|
146
|
+
|
|
147
|
+
Build the documentation locally with:
|
|
148
|
+
|
|
149
|
+
```console
|
|
150
|
+
python -m pip install ".[docs]"
|
|
151
|
+
sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```console
|
|
157
|
+
uv sync --group dev
|
|
158
|
+
uv run pytest
|
|
159
|
+
node --test tests/test_datastar_csrf.mjs
|
|
160
|
+
uv run ruff check .
|
|
161
|
+
uv run ruff format --check .
|
|
162
|
+
uv run mypy
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Node is contributor and CI tooling only. It is not a runtime dependency for
|
|
166
|
+
Django applications.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
django_datastar/__init__.py,sha256=QbtE3iNVEFRlzsgHTYKgTz5Evu54T9Qval0bFhEfl7E,309
|
|
2
|
+
django_datastar/apps.py,sha256=dbFIfNS4ZE83SmfWJHvW23TjbVX3vbyXYfM5Ry1J8sg,177
|
|
3
|
+
django_datastar/middleware.py,sha256=iFqFL4ht_DSuUlopnC8R6A6h-SEI5bxygiZph9v170g,2603
|
|
4
|
+
django_datastar/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
django_datastar/static/django_datastar/datastar-csrf.js,sha256=Q_JAxGJTena94Dtl0FIN9rrueJBlkcuQI6XKUClsX1U,3755
|
|
6
|
+
django_datastar/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
django_datastar/templatetags/django_datastar.py,sha256=tpWwrDRCZYf-zjvvulVoeC44Hi1EtjEs0KSn-cC9BhA,1398
|
|
8
|
+
django_datastar-0.1.0.dist-info/METADATA,sha256=BhIeJ9cJRGS_4YDfNJd1wVC2vaD3SHBvDjQpPZ3Ar9A,4943
|
|
9
|
+
django_datastar-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
10
|
+
django_datastar-0.1.0.dist-info/licenses/LICENSE,sha256=STFRmHA7eloSbM0gNFGVzABChKCG84hCoJhQPzXuoKI,1070
|
|
11
|
+
django_datastar-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcus A. Lee
|
|
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.
|