dj-hyperview 0.1.0a3__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.
- dj_hyperview-0.1.0a3/PKG-INFO +259 -0
- dj_hyperview-0.1.0a3/README.md +244 -0
- dj_hyperview-0.1.0a3/pyproject.toml +91 -0
- dj_hyperview-0.1.0a3/pyproject.toml.orig +70 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/__init__.py +61 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/apps.py +16 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/cache.py +592 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/checks.py +177 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/conf.py +95 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/__init__.py +1 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/__init__.py +1 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/_config.py +18 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/_invalidation.py +28 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/admin.py +303 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/apps.py +18 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/migrations/0001_initial.py +40 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/migrations/0002_field_validators.py +38 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/migrations/__init__.py +0 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/models.py +45 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/querysets.py +127 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/services.py +289 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/signals.py +123 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/sources.py +85 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/validators.py +52 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/engine.py +162 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/exceptions.py +78 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/http.py +88 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/loaders.py +152 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/middleware.py +107 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/resolver.py +291 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/sources/__init__.py +11 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/sources/base.py +64 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/sources/filesystem.py +62 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/templatetags/__init__.py +1 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/templatetags/dj_hyperview.py +24 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/validation.py +200 -0
- dj_hyperview-0.1.0a3/src/dj_hyperview/views.py +12 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dj-hyperview
|
|
3
|
+
Version: 0.1.0a3
|
|
4
|
+
Summary: Django infrastructure for server-driven Hyperview interfaces
|
|
5
|
+
Classifier: Framework :: Django
|
|
6
|
+
Classifier: Framework :: Django :: 5.2
|
|
7
|
+
Classifier: Framework :: Django :: 6.1
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Requires-Dist: django>=5.2,<6.2
|
|
12
|
+
Requires-Dist: lxml>=6.1.3,<7
|
|
13
|
+
Requires-Python: >=3.12, <3.15
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# dj-hyperview
|
|
17
|
+
|
|
18
|
+
Reusable Django infrastructure for serving Hyperview UI supplied by the host
|
|
19
|
+
project.
|
|
20
|
+
|
|
21
|
+
## Plan de desarrollo
|
|
22
|
+
|
|
23
|
+
Consultá el [plan de desarrollo](docs/development/README.md) para ver el roadmap,
|
|
24
|
+
la arquitectura, el trabajo completado, el estado actual y los criterios de release.
|
|
25
|
+
|
|
26
|
+
## Install for local development
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv sync --all-groups
|
|
30
|
+
uv run pytest
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package will resolve templates provided by a consumer from configured XML
|
|
34
|
+
directories or, when enabled, from an optional Django database application.
|
|
35
|
+
|
|
36
|
+
## Scope
|
|
37
|
+
|
|
38
|
+
`dj-hyperview` provides resolution, rendering, validation, caching, and Django
|
|
39
|
+
integration. It does **not** ship application screens, runtime XML/HXML files,
|
|
40
|
+
mobile components, Redis, or database/admin requirements. XML/HXML fixtures are
|
|
41
|
+
kept under `tests/` and excluded from the published package.
|
|
42
|
+
|
|
43
|
+
## HTTP contract
|
|
44
|
+
|
|
45
|
+
Responses default to Hyperview's vendor media type,
|
|
46
|
+
`application/vnd.hyperview+xml`, while accepting Django's explicit
|
|
47
|
+
`content_type`, status, charset, and headers arguments. Consumer projects supply
|
|
48
|
+
the markup directly or through their own templates:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from dj_hyperview import HyperviewResponse, HyperviewTemplateView
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def screen(request):
|
|
55
|
+
return HyperviewResponse("<view>Ready</view>", status=200)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DetailScreen(HyperviewTemplateView):
|
|
59
|
+
template_name = "mobile/detail.xml" # Provided by the consumer project.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Template responses remain unrendered until Django renders them, preserving the
|
|
63
|
+
standard `TemplateResponse` lifecycle.
|
|
64
|
+
|
|
65
|
+
## Request integration
|
|
66
|
+
|
|
67
|
+
Add the middleware to attach typed metadata without changing the response:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
MIDDLEWARE = [
|
|
71
|
+
"dj_hyperview.middleware.HyperviewMiddleware",
|
|
72
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
73
|
+
]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`request.hyperview` is truthy for the client's optional
|
|
77
|
+
`X-Hyperview-Version` header or an explicit Hyperview vendor media type in
|
|
78
|
+
`Accept`; its `version` is `None` when detection came only from negotiation.
|
|
79
|
+
Generic `application/xml` and wildcard requests are not classified as
|
|
80
|
+
Hyperview.
|
|
81
|
+
|
|
82
|
+
Mutable Hyperview forms can use Django's normal CSRF protection:
|
|
83
|
+
|
|
84
|
+
```django
|
|
85
|
+
{% load dj_hyperview %}
|
|
86
|
+
<form>
|
|
87
|
+
{% hv_csrf_token %}
|
|
88
|
+
</form>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The tag emits an XML-escaped hidden `csrfmiddlewaretoken` field. It calls
|
|
92
|
+
Django's standard token API and does not bypass `CsrfViewMiddleware`.
|
|
93
|
+
|
|
94
|
+
## Template sources
|
|
95
|
+
|
|
96
|
+
Sources are queried in declaration order; the first match wins. The built-in
|
|
97
|
+
filesystem source reads UTF-8 templates from the consumer's directories:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
HYPERVIEW = {
|
|
101
|
+
"TEMPLATE_DIRS": [BASE_DIR / "mobile_screens"],
|
|
102
|
+
"SOURCES": [
|
|
103
|
+
{"BACKEND": "dj_hyperview.sources.FileSystemSource"},
|
|
104
|
+
{
|
|
105
|
+
"BACKEND": "my_project.hyperview.TenantSource",
|
|
106
|
+
"OPTIONS": {"tenant_key": "slug"},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from dj_hyperview import render_template, resolve_template
|
|
114
|
+
|
|
115
|
+
screen = resolve_template("account/profile.xml")
|
|
116
|
+
markup = render_template("account/profile.xml", {"username": "Ada"})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Template names are relative POSIX paths. Absolute paths, empty or dot segments,
|
|
120
|
+
backslashes, NUL bytes, and filesystem symlink escapes are rejected. A source
|
|
121
|
+
returns `None` only for a miss; when every source misses, resolution raises
|
|
122
|
+
`TemplateNotFound`. Unicode remains case-preserving and unnormalized; combining
|
|
123
|
+
marks and format characters are accepted, while control and surrogate code
|
|
124
|
+
points are rejected.
|
|
125
|
+
|
|
126
|
+
### Optional database app
|
|
127
|
+
|
|
128
|
+
Install `"dj_hyperview.contrib.database"` in `INSTALLED_APPS` and run Django
|
|
129
|
+
migrations to make the `HyperviewTemplate` model available. Then enable the
|
|
130
|
+
database source where its precedence belongs:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
INSTALLED_APPS = [
|
|
134
|
+
# ...
|
|
135
|
+
"dj_hyperview.contrib.database",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
HYPERVIEW = {
|
|
139
|
+
"SOURCES": [
|
|
140
|
+
{"BACKEND": "dj_hyperview.contrib.database.sources.DatabaseSource"},
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The source uses Django's default manager and database router. Router-selected
|
|
146
|
+
sources are intentionally uncached because routing may vary by request or tenant.
|
|
147
|
+
Set `"OPTIONS": {"using": "replica"}` to pin a configured database alias and
|
|
148
|
+
enable generic raw-cache acceleration with an alias-specific identity.
|
|
149
|
+
Active exact-name rows are hits, including empty content; inactive or absent
|
|
150
|
+
rows are misses and resolution continues to the next source. Without cache, a
|
|
151
|
+
later lookup observes row content and revision changes. Explicit aliases retain
|
|
152
|
+
normal cache TTL and manual invalidation semantics.
|
|
153
|
+
|
|
154
|
+
The base package does not import the model or require a database table. Model
|
|
155
|
+
`full_clean()` checks canonical names and template safety; `save()` intentionally
|
|
156
|
+
follows Django's standard behavior and does not call `full_clean()` automatically.
|
|
157
|
+
Installed mutation signals still reject a noncanonical name before its SQL write.
|
|
158
|
+
When `django.contrib.admin` is installed, its standard model admin routes
|
|
159
|
+
create, edit, rename, and delete through the validated publication services.
|
|
160
|
+
Persisted-row edit and individual delete require exactly one hidden revision
|
|
161
|
+
token; missing, duplicate, or stale tokens fail safely. Add forms expose no token,
|
|
162
|
+
revision and timestamps stay read-only, and bulk delete remains signal-backed.
|
|
163
|
+
Omitting admin imports none of it.
|
|
164
|
+
|
|
165
|
+
Model save/delete plus controlled `QuerySet.delete()` and `QuerySet.update()`
|
|
166
|
+
schedule invalidation with Django's `transaction.on_commit()` on the mutation
|
|
167
|
+
database alias. Updates snapshot locked rows and validate their actual stored
|
|
168
|
+
names after SQL, so literal and expression renames invalidate both old and new
|
|
169
|
+
names. Rollbacks and rolled-back savepoints discard callbacks. A cache failure
|
|
170
|
+
remains observable after commit and therefore does not mean the database write
|
|
171
|
+
rolled back. Raw fixture saves and bulk APIs still require explicit invalidation
|
|
172
|
+
through `invalidate_templates`; automatic bulk hooks remain post-MVP.
|
|
173
|
+
Use the public database services to publish, rename, or delete validated
|
|
174
|
+
templates. Publication creates revision 1, increments updates once, and
|
|
175
|
+
accepts `expected_revision` for optimistic conflict detection; omitting it uses
|
|
176
|
+
last-write-wins under the selected database row lock. Its immutable result hides
|
|
177
|
+
the model and confirms persistence in the current transaction; an enclosing
|
|
178
|
+
transaction may still roll it back. SQLite cannot prove row-lock serialization,
|
|
179
|
+
so tests inject races.
|
|
180
|
+
Name uniqueness follows the database backend's collation: the package does not
|
|
181
|
+
case-fold names, and SQLite's default treats `screen.xml` and `Screen.xml` as
|
|
182
|
+
distinct.
|
|
183
|
+
|
|
184
|
+
## Cache contract
|
|
185
|
+
|
|
186
|
+
`TemplateCache` stores only serialized raw `ResolvedTemplate` data through a
|
|
187
|
+
configured Django cache alias; it never caches compiled templates. A lookup
|
|
188
|
+
returns `None` when no entry exists, `CACHE_MISS` for a cached source miss, and
|
|
189
|
+
returns a `CacheEntry` for content—even when that content is empty. Every hit or
|
|
190
|
+
miss is bound to its source, name, and revision. Backend failures use a stable
|
|
191
|
+
public error without exposing cached template data. Fixed-length
|
|
192
|
+
SHA-256 keys isolate each namespace. A non-empty `HYPERVIEW.CACHE` is opt-in;
|
|
193
|
+
omitting it or using `{}` never initializes a cache backend. Source identities hash
|
|
194
|
+
position plus effective backend options (including filesystem roots) without
|
|
195
|
+
exposing secrets; an unrepresentable custom configuration stays uncached.
|
|
196
|
+
Fingerprinting accepts a closed domain: exact built-in scalars, `dict`, `list`,
|
|
197
|
+
`tuple`, standard `pathlib` paths, and verified importable callables. Subclasses,
|
|
198
|
+
generic container implementations, ranges, mutable/buffer containers such as
|
|
199
|
+
`bytearray`, `memoryview`, and `array.array` stay uncached instead of risking an
|
|
200
|
+
identity that omits observable semantics.
|
|
201
|
+
Cached JSON is treated as untrusted: exact version/shape/type and lookup identity
|
|
202
|
+
must match, while duplicate keys or invalid aliases raise `SourceUnavailable`.
|
|
203
|
+
Checks and runtime accept an alias only when Django resolves its configured name
|
|
204
|
+
to a cache backend; leading underscores alone do not make an alias invalid.
|
|
205
|
+
|
|
206
|
+
Call `invalidate_templates("screens/home.xml")` after publishing raw content.
|
|
207
|
+
Invalidation rotates a shared, namespaced token for each canonical name, so all
|
|
208
|
+
sources' older hits and misses become invisible and a read that started earlier
|
|
209
|
+
cannot repopulate the new generation. Already-running renders may still finish
|
|
210
|
+
with their pinned snapshot. The call returns only after every requested token
|
|
211
|
+
rotation succeeds; any cache failure raises `SourceUnavailable`, independently
|
|
212
|
+
of the resolver's ordinary `bypass` policy. Multi-name rotation is not atomic,
|
|
213
|
+
so a partial failure is observable and callers may safely retry every name.
|
|
214
|
+
Every successfully claimed root or successor token leaves a shared,
|
|
215
|
+
non-expiring tombstone. Claim metadata binds each token to its root/successor
|
|
216
|
+
lifecycle, so repeated entropy cannot reuse an older generation even after raw
|
|
217
|
+
TTLs pass. Failed and concurrent candidates also remain claimed: correctness
|
|
218
|
+
costs roughly one small marker per generated candidate, reclaimed only with the
|
|
219
|
+
cache namespace/backend lifecycle.
|
|
220
|
+
Successors are domain-separated digests of the previous token plus fresh
|
|
221
|
+
entropy, not the entropy itself. Every raw content or miss write rechecks the
|
|
222
|
+
shared generation and removes a superseded exact key only when the backend
|
|
223
|
+
returns exactly `True`; `None`, `False`, and exceptions are ambiguous failures.
|
|
224
|
+
`bypass` may still return authoritative source data, but never reports stale
|
|
225
|
+
cache publication as successful or weakens its tombstone. If an operator evicts
|
|
226
|
+
a tombstone while retaining raw entries, generic caches cannot prove uniqueness;
|
|
227
|
+
cryptographic uniqueness is the fallback, not a durable transaction.
|
|
228
|
+
|
|
229
|
+
## Template engine
|
|
230
|
+
|
|
231
|
+
`render_template()` uses a dedicated Django template engine backed only by the
|
|
232
|
+
configured Hyperview sources. Root templates, `{% include %}`, and
|
|
233
|
+
`{% extends %}` therefore use the same canonical names and source precedence;
|
|
234
|
+
the host project's HTML template loaders are not modified. No compiled-template
|
|
235
|
+
cache is installed, so a new render sees newly published source content.
|
|
236
|
+
Templates returned by `get_template()` or `select_template()` preserve
|
|
237
|
+
Django's render signature and metadata while enforcing the same validation.
|
|
238
|
+
|
|
239
|
+
During one render, the first result—or miss—for each template name is pinned per
|
|
240
|
+
resolver identity. Nested engines sharing a resolver reuse its snapshot, while
|
|
241
|
+
different resolvers remain isolated even when they render the same name.
|
|
242
|
+
Repeated includes cannot mix revisions if a source changes concurrently, while
|
|
243
|
+
separate sync or async request contexts remain isolated. Dynamic names that have
|
|
244
|
+
not yet been resolved still observe source state at their first lookup because
|
|
245
|
+
the source protocol intentionally provides point lookups rather than a global
|
|
246
|
+
transaction.
|
|
247
|
+
|
|
248
|
+
When `HYPERVIEW["SOURCES"]` is configured, `HyperviewTemplateResponse` and
|
|
249
|
+
`HyperviewTemplateView` use this engine while preserving Django's lazy response,
|
|
250
|
+
status, header, context, and escaping behavior. An explicit `using=` continues
|
|
251
|
+
to select the consumer's standard Django template engine.
|
|
252
|
+
|
|
253
|
+
## HXML validation
|
|
254
|
+
|
|
255
|
+
Every public engine/response render path escapes context, rejects active
|
|
256
|
+
DTD/entities before compile without misclassifying comments or CDATA, and
|
|
257
|
+
enforces rendered XML schema, byte, depth, and node limits. Consumer XSD
|
|
258
|
+
includes/imports are denied to prevent network or traversal access. Failures
|
|
259
|
+
raise `TemplateValidationError`; no application schema or screen is included.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# dj-hyperview
|
|
2
|
+
|
|
3
|
+
Reusable Django infrastructure for serving Hyperview UI supplied by the host
|
|
4
|
+
project.
|
|
5
|
+
|
|
6
|
+
## Plan de desarrollo
|
|
7
|
+
|
|
8
|
+
Consultá el [plan de desarrollo](docs/development/README.md) para ver el roadmap,
|
|
9
|
+
la arquitectura, el trabajo completado, el estado actual y los criterios de release.
|
|
10
|
+
|
|
11
|
+
## Install for local development
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv sync --all-groups
|
|
15
|
+
uv run pytest
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The package will resolve templates provided by a consumer from configured XML
|
|
19
|
+
directories or, when enabled, from an optional Django database application.
|
|
20
|
+
|
|
21
|
+
## Scope
|
|
22
|
+
|
|
23
|
+
`dj-hyperview` provides resolution, rendering, validation, caching, and Django
|
|
24
|
+
integration. It does **not** ship application screens, runtime XML/HXML files,
|
|
25
|
+
mobile components, Redis, or database/admin requirements. XML/HXML fixtures are
|
|
26
|
+
kept under `tests/` and excluded from the published package.
|
|
27
|
+
|
|
28
|
+
## HTTP contract
|
|
29
|
+
|
|
30
|
+
Responses default to Hyperview's vendor media type,
|
|
31
|
+
`application/vnd.hyperview+xml`, while accepting Django's explicit
|
|
32
|
+
`content_type`, status, charset, and headers arguments. Consumer projects supply
|
|
33
|
+
the markup directly or through their own templates:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from dj_hyperview import HyperviewResponse, HyperviewTemplateView
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def screen(request):
|
|
40
|
+
return HyperviewResponse("<view>Ready</view>", status=200)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DetailScreen(HyperviewTemplateView):
|
|
44
|
+
template_name = "mobile/detail.xml" # Provided by the consumer project.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Template responses remain unrendered until Django renders them, preserving the
|
|
48
|
+
standard `TemplateResponse` lifecycle.
|
|
49
|
+
|
|
50
|
+
## Request integration
|
|
51
|
+
|
|
52
|
+
Add the middleware to attach typed metadata without changing the response:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
MIDDLEWARE = [
|
|
56
|
+
"dj_hyperview.middleware.HyperviewMiddleware",
|
|
57
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`request.hyperview` is truthy for the client's optional
|
|
62
|
+
`X-Hyperview-Version` header or an explicit Hyperview vendor media type in
|
|
63
|
+
`Accept`; its `version` is `None` when detection came only from negotiation.
|
|
64
|
+
Generic `application/xml` and wildcard requests are not classified as
|
|
65
|
+
Hyperview.
|
|
66
|
+
|
|
67
|
+
Mutable Hyperview forms can use Django's normal CSRF protection:
|
|
68
|
+
|
|
69
|
+
```django
|
|
70
|
+
{% load dj_hyperview %}
|
|
71
|
+
<form>
|
|
72
|
+
{% hv_csrf_token %}
|
|
73
|
+
</form>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The tag emits an XML-escaped hidden `csrfmiddlewaretoken` field. It calls
|
|
77
|
+
Django's standard token API and does not bypass `CsrfViewMiddleware`.
|
|
78
|
+
|
|
79
|
+
## Template sources
|
|
80
|
+
|
|
81
|
+
Sources are queried in declaration order; the first match wins. The built-in
|
|
82
|
+
filesystem source reads UTF-8 templates from the consumer's directories:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
HYPERVIEW = {
|
|
86
|
+
"TEMPLATE_DIRS": [BASE_DIR / "mobile_screens"],
|
|
87
|
+
"SOURCES": [
|
|
88
|
+
{"BACKEND": "dj_hyperview.sources.FileSystemSource"},
|
|
89
|
+
{
|
|
90
|
+
"BACKEND": "my_project.hyperview.TenantSource",
|
|
91
|
+
"OPTIONS": {"tenant_key": "slug"},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from dj_hyperview import render_template, resolve_template
|
|
99
|
+
|
|
100
|
+
screen = resolve_template("account/profile.xml")
|
|
101
|
+
markup = render_template("account/profile.xml", {"username": "Ada"})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Template names are relative POSIX paths. Absolute paths, empty or dot segments,
|
|
105
|
+
backslashes, NUL bytes, and filesystem symlink escapes are rejected. A source
|
|
106
|
+
returns `None` only for a miss; when every source misses, resolution raises
|
|
107
|
+
`TemplateNotFound`. Unicode remains case-preserving and unnormalized; combining
|
|
108
|
+
marks and format characters are accepted, while control and surrogate code
|
|
109
|
+
points are rejected.
|
|
110
|
+
|
|
111
|
+
### Optional database app
|
|
112
|
+
|
|
113
|
+
Install `"dj_hyperview.contrib.database"` in `INSTALLED_APPS` and run Django
|
|
114
|
+
migrations to make the `HyperviewTemplate` model available. Then enable the
|
|
115
|
+
database source where its precedence belongs:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
INSTALLED_APPS = [
|
|
119
|
+
# ...
|
|
120
|
+
"dj_hyperview.contrib.database",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
HYPERVIEW = {
|
|
124
|
+
"SOURCES": [
|
|
125
|
+
{"BACKEND": "dj_hyperview.contrib.database.sources.DatabaseSource"},
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The source uses Django's default manager and database router. Router-selected
|
|
131
|
+
sources are intentionally uncached because routing may vary by request or tenant.
|
|
132
|
+
Set `"OPTIONS": {"using": "replica"}` to pin a configured database alias and
|
|
133
|
+
enable generic raw-cache acceleration with an alias-specific identity.
|
|
134
|
+
Active exact-name rows are hits, including empty content; inactive or absent
|
|
135
|
+
rows are misses and resolution continues to the next source. Without cache, a
|
|
136
|
+
later lookup observes row content and revision changes. Explicit aliases retain
|
|
137
|
+
normal cache TTL and manual invalidation semantics.
|
|
138
|
+
|
|
139
|
+
The base package does not import the model or require a database table. Model
|
|
140
|
+
`full_clean()` checks canonical names and template safety; `save()` intentionally
|
|
141
|
+
follows Django's standard behavior and does not call `full_clean()` automatically.
|
|
142
|
+
Installed mutation signals still reject a noncanonical name before its SQL write.
|
|
143
|
+
When `django.contrib.admin` is installed, its standard model admin routes
|
|
144
|
+
create, edit, rename, and delete through the validated publication services.
|
|
145
|
+
Persisted-row edit and individual delete require exactly one hidden revision
|
|
146
|
+
token; missing, duplicate, or stale tokens fail safely. Add forms expose no token,
|
|
147
|
+
revision and timestamps stay read-only, and bulk delete remains signal-backed.
|
|
148
|
+
Omitting admin imports none of it.
|
|
149
|
+
|
|
150
|
+
Model save/delete plus controlled `QuerySet.delete()` and `QuerySet.update()`
|
|
151
|
+
schedule invalidation with Django's `transaction.on_commit()` on the mutation
|
|
152
|
+
database alias. Updates snapshot locked rows and validate their actual stored
|
|
153
|
+
names after SQL, so literal and expression renames invalidate both old and new
|
|
154
|
+
names. Rollbacks and rolled-back savepoints discard callbacks. A cache failure
|
|
155
|
+
remains observable after commit and therefore does not mean the database write
|
|
156
|
+
rolled back. Raw fixture saves and bulk APIs still require explicit invalidation
|
|
157
|
+
through `invalidate_templates`; automatic bulk hooks remain post-MVP.
|
|
158
|
+
Use the public database services to publish, rename, or delete validated
|
|
159
|
+
templates. Publication creates revision 1, increments updates once, and
|
|
160
|
+
accepts `expected_revision` for optimistic conflict detection; omitting it uses
|
|
161
|
+
last-write-wins under the selected database row lock. Its immutable result hides
|
|
162
|
+
the model and confirms persistence in the current transaction; an enclosing
|
|
163
|
+
transaction may still roll it back. SQLite cannot prove row-lock serialization,
|
|
164
|
+
so tests inject races.
|
|
165
|
+
Name uniqueness follows the database backend's collation: the package does not
|
|
166
|
+
case-fold names, and SQLite's default treats `screen.xml` and `Screen.xml` as
|
|
167
|
+
distinct.
|
|
168
|
+
|
|
169
|
+
## Cache contract
|
|
170
|
+
|
|
171
|
+
`TemplateCache` stores only serialized raw `ResolvedTemplate` data through a
|
|
172
|
+
configured Django cache alias; it never caches compiled templates. A lookup
|
|
173
|
+
returns `None` when no entry exists, `CACHE_MISS` for a cached source miss, and
|
|
174
|
+
returns a `CacheEntry` for content—even when that content is empty. Every hit or
|
|
175
|
+
miss is bound to its source, name, and revision. Backend failures use a stable
|
|
176
|
+
public error without exposing cached template data. Fixed-length
|
|
177
|
+
SHA-256 keys isolate each namespace. A non-empty `HYPERVIEW.CACHE` is opt-in;
|
|
178
|
+
omitting it or using `{}` never initializes a cache backend. Source identities hash
|
|
179
|
+
position plus effective backend options (including filesystem roots) without
|
|
180
|
+
exposing secrets; an unrepresentable custom configuration stays uncached.
|
|
181
|
+
Fingerprinting accepts a closed domain: exact built-in scalars, `dict`, `list`,
|
|
182
|
+
`tuple`, standard `pathlib` paths, and verified importable callables. Subclasses,
|
|
183
|
+
generic container implementations, ranges, mutable/buffer containers such as
|
|
184
|
+
`bytearray`, `memoryview`, and `array.array` stay uncached instead of risking an
|
|
185
|
+
identity that omits observable semantics.
|
|
186
|
+
Cached JSON is treated as untrusted: exact version/shape/type and lookup identity
|
|
187
|
+
must match, while duplicate keys or invalid aliases raise `SourceUnavailable`.
|
|
188
|
+
Checks and runtime accept an alias only when Django resolves its configured name
|
|
189
|
+
to a cache backend; leading underscores alone do not make an alias invalid.
|
|
190
|
+
|
|
191
|
+
Call `invalidate_templates("screens/home.xml")` after publishing raw content.
|
|
192
|
+
Invalidation rotates a shared, namespaced token for each canonical name, so all
|
|
193
|
+
sources' older hits and misses become invisible and a read that started earlier
|
|
194
|
+
cannot repopulate the new generation. Already-running renders may still finish
|
|
195
|
+
with their pinned snapshot. The call returns only after every requested token
|
|
196
|
+
rotation succeeds; any cache failure raises `SourceUnavailable`, independently
|
|
197
|
+
of the resolver's ordinary `bypass` policy. Multi-name rotation is not atomic,
|
|
198
|
+
so a partial failure is observable and callers may safely retry every name.
|
|
199
|
+
Every successfully claimed root or successor token leaves a shared,
|
|
200
|
+
non-expiring tombstone. Claim metadata binds each token to its root/successor
|
|
201
|
+
lifecycle, so repeated entropy cannot reuse an older generation even after raw
|
|
202
|
+
TTLs pass. Failed and concurrent candidates also remain claimed: correctness
|
|
203
|
+
costs roughly one small marker per generated candidate, reclaimed only with the
|
|
204
|
+
cache namespace/backend lifecycle.
|
|
205
|
+
Successors are domain-separated digests of the previous token plus fresh
|
|
206
|
+
entropy, not the entropy itself. Every raw content or miss write rechecks the
|
|
207
|
+
shared generation and removes a superseded exact key only when the backend
|
|
208
|
+
returns exactly `True`; `None`, `False`, and exceptions are ambiguous failures.
|
|
209
|
+
`bypass` may still return authoritative source data, but never reports stale
|
|
210
|
+
cache publication as successful or weakens its tombstone. If an operator evicts
|
|
211
|
+
a tombstone while retaining raw entries, generic caches cannot prove uniqueness;
|
|
212
|
+
cryptographic uniqueness is the fallback, not a durable transaction.
|
|
213
|
+
|
|
214
|
+
## Template engine
|
|
215
|
+
|
|
216
|
+
`render_template()` uses a dedicated Django template engine backed only by the
|
|
217
|
+
configured Hyperview sources. Root templates, `{% include %}`, and
|
|
218
|
+
`{% extends %}` therefore use the same canonical names and source precedence;
|
|
219
|
+
the host project's HTML template loaders are not modified. No compiled-template
|
|
220
|
+
cache is installed, so a new render sees newly published source content.
|
|
221
|
+
Templates returned by `get_template()` or `select_template()` preserve
|
|
222
|
+
Django's render signature and metadata while enforcing the same validation.
|
|
223
|
+
|
|
224
|
+
During one render, the first result—or miss—for each template name is pinned per
|
|
225
|
+
resolver identity. Nested engines sharing a resolver reuse its snapshot, while
|
|
226
|
+
different resolvers remain isolated even when they render the same name.
|
|
227
|
+
Repeated includes cannot mix revisions if a source changes concurrently, while
|
|
228
|
+
separate sync or async request contexts remain isolated. Dynamic names that have
|
|
229
|
+
not yet been resolved still observe source state at their first lookup because
|
|
230
|
+
the source protocol intentionally provides point lookups rather than a global
|
|
231
|
+
transaction.
|
|
232
|
+
|
|
233
|
+
When `HYPERVIEW["SOURCES"]` is configured, `HyperviewTemplateResponse` and
|
|
234
|
+
`HyperviewTemplateView` use this engine while preserving Django's lazy response,
|
|
235
|
+
status, header, context, and escaping behavior. An explicit `using=` continues
|
|
236
|
+
to select the consumer's standard Django template engine.
|
|
237
|
+
|
|
238
|
+
## HXML validation
|
|
239
|
+
|
|
240
|
+
Every public engine/response render path escapes context, rejects active
|
|
241
|
+
DTD/entities before compile without misclassifying comments or CDATA, and
|
|
242
|
+
enforces rendered XML schema, byte, depth, and node limits. Consumer XSD
|
|
243
|
+
includes/imports are denied to prevent network or traversal access. Failures
|
|
244
|
+
raise `TemplateValidationError`; no application schema or screen is included.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dj-hyperview"
|
|
3
|
+
version = "0.1.0a3"
|
|
4
|
+
description = "Django infrastructure for server-driven Hyperview interfaces"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12,<3.15"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Framework :: Django",
|
|
9
|
+
"Framework :: Django :: 5.2",
|
|
10
|
+
"Framework :: Django :: 6.1",
|
|
11
|
+
"Programming Language :: Python :: 3.12",
|
|
12
|
+
"Programming Language :: Python :: 3.13",
|
|
13
|
+
"Programming Language :: Python :: 3.14",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"Django>=5.2,<6.2",
|
|
17
|
+
"lxml>=6.1.3,<7",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.12.9,<0.13"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"coverage[toml]>=7.16,<8",
|
|
27
|
+
"packaging>=26.3,<27",
|
|
28
|
+
"pytest>=9.1,<10",
|
|
29
|
+
"pytest-cov>=7.1,<8",
|
|
30
|
+
"pytest-django>=4.14,<5",
|
|
31
|
+
"ruff>=0.16.6,<0.17",
|
|
32
|
+
"uv-build>=0.12.9,<0.13",
|
|
33
|
+
"zensical==0.0.59",
|
|
34
|
+
]
|
|
35
|
+
redis = ["redis==8.1.0"]
|
|
36
|
+
|
|
37
|
+
[tool.uv]
|
|
38
|
+
package = false
|
|
39
|
+
|
|
40
|
+
[tool.dj-hyperview]
|
|
41
|
+
dependency-audit-date = "2026-09-04"
|
|
42
|
+
supported-python = [
|
|
43
|
+
"3.12",
|
|
44
|
+
"3.13",
|
|
45
|
+
"3.14",
|
|
46
|
+
]
|
|
47
|
+
supported-django = [
|
|
48
|
+
"5.2.17",
|
|
49
|
+
"6.1.1",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
54
|
+
addopts = "--strict-config --strict-markers"
|
|
55
|
+
filterwarnings = [
|
|
56
|
+
"error::DeprecationWarning",
|
|
57
|
+
"error::PendingDeprecationWarning",
|
|
58
|
+
]
|
|
59
|
+
pythonpath = [
|
|
60
|
+
".",
|
|
61
|
+
"src",
|
|
62
|
+
]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
|
|
65
|
+
[tool.coverage.run]
|
|
66
|
+
branch = true
|
|
67
|
+
source = ["dj_hyperview"]
|
|
68
|
+
|
|
69
|
+
[tool.coverage.report]
|
|
70
|
+
fail_under = 95
|
|
71
|
+
show_missing = true
|
|
72
|
+
skip_covered = true
|
|
73
|
+
|
|
74
|
+
[tool.ruff]
|
|
75
|
+
target-version = "py312"
|
|
76
|
+
line-length = 88
|
|
77
|
+
src = [
|
|
78
|
+
"src",
|
|
79
|
+
"tests",
|
|
80
|
+
"tools",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
select = [
|
|
85
|
+
"E",
|
|
86
|
+
"F",
|
|
87
|
+
"I",
|
|
88
|
+
"UP",
|
|
89
|
+
"B",
|
|
90
|
+
"DJ",
|
|
91
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dj-hyperview"
|
|
3
|
+
version = "0.1.0a3"
|
|
4
|
+
description = "Django infrastructure for server-driven Hyperview interfaces"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12,<3.15"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Framework :: Django",
|
|
9
|
+
"Framework :: Django :: 5.2",
|
|
10
|
+
"Framework :: Django :: 6.1",
|
|
11
|
+
"Programming Language :: Python :: 3.12",
|
|
12
|
+
"Programming Language :: Python :: 3.13",
|
|
13
|
+
"Programming Language :: Python :: 3.14",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"Django>=5.2,<6.2",
|
|
17
|
+
"lxml>=6.1.3,<7",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.12.9,<0.13"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"coverage[toml]>=7.16,<8",
|
|
27
|
+
"packaging>=26.3,<27",
|
|
28
|
+
"pytest>=9.1,<10",
|
|
29
|
+
"pytest-cov>=7.1,<8",
|
|
30
|
+
"pytest-django>=4.14,<5",
|
|
31
|
+
"ruff>=0.16.6,<0.17",
|
|
32
|
+
"uv-build>=0.12.9,<0.13",
|
|
33
|
+
"zensical==0.0.59",
|
|
34
|
+
]
|
|
35
|
+
redis = ["redis==8.1.0"]
|
|
36
|
+
|
|
37
|
+
[tool.uv]
|
|
38
|
+
package = false
|
|
39
|
+
|
|
40
|
+
[tool.dj-hyperview]
|
|
41
|
+
dependency-audit-date = "2026-09-04"
|
|
42
|
+
supported-python = ["3.12", "3.13", "3.14"]
|
|
43
|
+
supported-django = ["5.2.17", "6.1.1"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
47
|
+
addopts = "--strict-config --strict-markers"
|
|
48
|
+
filterwarnings = [
|
|
49
|
+
"error::DeprecationWarning",
|
|
50
|
+
"error::PendingDeprecationWarning",
|
|
51
|
+
]
|
|
52
|
+
pythonpath = [".", "src"]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
|
|
55
|
+
[tool.coverage.run]
|
|
56
|
+
branch = true
|
|
57
|
+
source = ["dj_hyperview"]
|
|
58
|
+
|
|
59
|
+
[tool.coverage.report]
|
|
60
|
+
fail_under = 95
|
|
61
|
+
show_missing = true
|
|
62
|
+
skip_covered = true
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
target-version = "py312"
|
|
66
|
+
line-length = 88
|
|
67
|
+
src = ["src", "tests", "tools"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E", "F", "I", "UP", "B", "DJ"]
|