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.
Files changed (37) hide show
  1. dj_hyperview-0.1.0a3/PKG-INFO +259 -0
  2. dj_hyperview-0.1.0a3/README.md +244 -0
  3. dj_hyperview-0.1.0a3/pyproject.toml +91 -0
  4. dj_hyperview-0.1.0a3/pyproject.toml.orig +70 -0
  5. dj_hyperview-0.1.0a3/src/dj_hyperview/__init__.py +61 -0
  6. dj_hyperview-0.1.0a3/src/dj_hyperview/apps.py +16 -0
  7. dj_hyperview-0.1.0a3/src/dj_hyperview/cache.py +592 -0
  8. dj_hyperview-0.1.0a3/src/dj_hyperview/checks.py +177 -0
  9. dj_hyperview-0.1.0a3/src/dj_hyperview/conf.py +95 -0
  10. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/__init__.py +1 -0
  11. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/__init__.py +1 -0
  12. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/_config.py +18 -0
  13. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/_invalidation.py +28 -0
  14. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/admin.py +303 -0
  15. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/apps.py +18 -0
  16. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/migrations/0001_initial.py +40 -0
  17. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/migrations/0002_field_validators.py +38 -0
  18. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/migrations/__init__.py +0 -0
  19. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/models.py +45 -0
  20. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/querysets.py +127 -0
  21. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/services.py +289 -0
  22. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/signals.py +123 -0
  23. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/sources.py +85 -0
  24. dj_hyperview-0.1.0a3/src/dj_hyperview/contrib/database/validators.py +52 -0
  25. dj_hyperview-0.1.0a3/src/dj_hyperview/engine.py +162 -0
  26. dj_hyperview-0.1.0a3/src/dj_hyperview/exceptions.py +78 -0
  27. dj_hyperview-0.1.0a3/src/dj_hyperview/http.py +88 -0
  28. dj_hyperview-0.1.0a3/src/dj_hyperview/loaders.py +152 -0
  29. dj_hyperview-0.1.0a3/src/dj_hyperview/middleware.py +107 -0
  30. dj_hyperview-0.1.0a3/src/dj_hyperview/resolver.py +291 -0
  31. dj_hyperview-0.1.0a3/src/dj_hyperview/sources/__init__.py +11 -0
  32. dj_hyperview-0.1.0a3/src/dj_hyperview/sources/base.py +64 -0
  33. dj_hyperview-0.1.0a3/src/dj_hyperview/sources/filesystem.py +62 -0
  34. dj_hyperview-0.1.0a3/src/dj_hyperview/templatetags/__init__.py +1 -0
  35. dj_hyperview-0.1.0a3/src/dj_hyperview/templatetags/dj_hyperview.py +24 -0
  36. dj_hyperview-0.1.0a3/src/dj_hyperview/validation.py +200 -0
  37. 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"]