Complete independent projection runtime
This commit is contained in:
parent
1134c2d375
commit
f1fabaf0ca
38 changed files with 4907 additions and 87 deletions
|
|
@ -3,15 +3,28 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
from typing import Protocol, cast
|
||||
|
||||
from .errors import DocForgeError
|
||||
from .manual_projection import build_manual_projection_package, build_manual_render_plan
|
||||
from .models import ProjectSnapshot, RenderView
|
||||
from .projection_contract import (
|
||||
ManualRenderPlanV1,
|
||||
ProjectionPackageV1,
|
||||
ProjectionRenderResult,
|
||||
)
|
||||
from .projection_fragments import (
|
||||
FragmentKey,
|
||||
FragmentRecord,
|
||||
ProjectionFragmentCache,
|
||||
fragment_semantic_hash,
|
||||
)
|
||||
from .projection_worker import render_projection_in_worker
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -22,6 +35,7 @@ class PreparedRender:
|
|||
renderer: str
|
||||
renderer_version: str
|
||||
template_hash: str
|
||||
projection_receipt: dict[str, object] | None = None
|
||||
|
||||
|
||||
class Renderer(Protocol):
|
||||
|
|
@ -46,10 +60,13 @@ class GenericHtmlRenderer:
|
|||
renderer_id = "generic_html"
|
||||
contract_version = "1"
|
||||
|
||||
def __init__(self) -> None:
|
||||
page_component_version = "manual.page@1"
|
||||
|
||||
def __init__(self, *, incremental: bool = True) -> None:
|
||||
self.renderer_version = (
|
||||
f"{self.contract_version}+markdown-it-py-{version('markdown-it-py')}"
|
||||
)
|
||||
self.incremental = incremental
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
|
|
@ -98,19 +115,24 @@ class GenericHtmlRenderer:
|
|||
json.dumps(identity_payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
).hexdigest()
|
||||
plan = build_manual_render_plan(snapshot, view, changeset_hash=changeset_hash)
|
||||
package = build_manual_projection_package(
|
||||
full_package = build_manual_projection_package(
|
||||
plan,
|
||||
template_bytes,
|
||||
renderer_id=self.renderer_id,
|
||||
renderer_version=self.renderer_version,
|
||||
max_output_bytes=snapshot.descriptor.limits.max_render_bytes,
|
||||
)
|
||||
from docforge_renderers.manual import ManualHtmlRenderer
|
||||
|
||||
result = ManualHtmlRenderer(self.renderer_version).render(
|
||||
package,
|
||||
render_identity=render_identity,
|
||||
)
|
||||
if self.incremental:
|
||||
result = self._incremental_result(
|
||||
snapshot,
|
||||
plan,
|
||||
template_bytes,
|
||||
full_package=full_package,
|
||||
render_identity=render_identity,
|
||||
)
|
||||
else:
|
||||
result = render_projection_in_worker(full_package)
|
||||
if len(result.artifacts) != 1:
|
||||
raise DocForgeError(
|
||||
"invalid_projection",
|
||||
|
|
@ -124,21 +146,128 @@ class GenericHtmlRenderer:
|
|||
renderer=self.renderer_id,
|
||||
renderer_version=self.renderer_version,
|
||||
template_hash=template_hash,
|
||||
projection_receipt=result.receipt.as_dict(),
|
||||
)
|
||||
|
||||
def _incremental_result(
|
||||
self,
|
||||
snapshot: ProjectSnapshot,
|
||||
plan: ManualRenderPlanV1,
|
||||
template_bytes: bytes,
|
||||
*,
|
||||
full_package: ProjectionPackageV1,
|
||||
render_identity: str,
|
||||
) -> ProjectionRenderResult:
|
||||
cache = ProjectionFragmentCache(
|
||||
snapshot.descriptor.root,
|
||||
snapshot.descriptor.cache_root,
|
||||
)
|
||||
pages = cast(list[object], plan.document["pages"])
|
||||
records: list[FragmentRecord | None] = []
|
||||
missing: list[tuple[int, FragmentKey]] = []
|
||||
for value in pages:
|
||||
page = cast(dict[str, object], value)
|
||||
key = FragmentKey.create(
|
||||
projection_kind="manual",
|
||||
renderer_id=self.renderer_id,
|
||||
renderer_version=self.renderer_version,
|
||||
component_version=self.page_component_version,
|
||||
semantic_input_hash=fragment_semantic_hash(page),
|
||||
)
|
||||
record = cache.get(key)
|
||||
if record is None:
|
||||
missing.append((len(records), key))
|
||||
records.append(record)
|
||||
if not records:
|
||||
return render_projection_in_worker(full_package)
|
||||
full_result: ProjectionRenderResult | None = None
|
||||
new_records: list[FragmentRecord] = []
|
||||
if missing:
|
||||
full_result = render_projection_in_worker(full_package)
|
||||
fragments = self._extract_page_fragments(
|
||||
pages,
|
||||
full_result.artifacts[0].content,
|
||||
)
|
||||
try:
|
||||
for position, key in missing:
|
||||
record = FragmentRecord.create(key, fragments[position])
|
||||
records[position] = record
|
||||
new_records.append(record)
|
||||
except DocForgeError:
|
||||
return full_result
|
||||
complete_records = [record for record in records if record is not None]
|
||||
if len(complete_records) != len(records):
|
||||
return full_result or render_projection_in_worker(full_package)
|
||||
try:
|
||||
incremental_package = build_manual_projection_package(
|
||||
plan,
|
||||
template_bytes,
|
||||
fragment_records=[record.as_dict() for record in complete_records],
|
||||
renderer_id=self.renderer_id,
|
||||
renderer_version=self.renderer_version,
|
||||
max_output_bytes=snapshot.descriptor.limits.max_render_bytes,
|
||||
render_identity=render_identity,
|
||||
)
|
||||
incremental_result = render_projection_in_worker(incremental_package)
|
||||
except DocForgeError:
|
||||
cache.prune(())
|
||||
return full_result or render_projection_in_worker(full_package)
|
||||
if full_result is None:
|
||||
cache.prune(tuple(record.key for record in complete_records))
|
||||
return incremental_result
|
||||
if tuple(
|
||||
(artifact.artifact_id, artifact.media_type, artifact.content)
|
||||
for artifact in incremental_result.artifacts
|
||||
) != tuple(
|
||||
(artifact.artifact_id, artifact.media_type, artifact.content)
|
||||
for artifact in full_result.artifacts
|
||||
):
|
||||
cache.prune(())
|
||||
return full_result
|
||||
for record in new_records:
|
||||
cache.put(record.key, record.content)
|
||||
cache.prune(tuple(record.key for record in complete_records))
|
||||
return incremental_result
|
||||
|
||||
@staticmethod
|
||||
def _extract_page_fragments(
|
||||
pages: list[object],
|
||||
output: bytes,
|
||||
) -> list[bytes]:
|
||||
"""Extract exact deterministic page sections from one trusted full artifact."""
|
||||
|
||||
fragments: list[bytes] = []
|
||||
cursor = 0
|
||||
closing = b"</section>"
|
||||
for value in pages:
|
||||
page = cast(dict[str, object], value)
|
||||
node_id = cast(str, page["node_id"])
|
||||
marker = f'<section id="node-{html.escape(node_id, quote=True)}">'.encode()
|
||||
start = output.find(marker, cursor)
|
||||
end = output.find(closing, start + len(marker)) if start >= 0 else -1
|
||||
if start < 0 or end < 0:
|
||||
raise DocForgeError(
|
||||
"invalid_projection",
|
||||
"Full manual artifact does not contain its planned page fragments",
|
||||
)
|
||||
end += len(closing)
|
||||
fragments.append(output[start:end])
|
||||
cursor = end
|
||||
return fragments
|
||||
|
||||
|
||||
_RENDERERS: dict[str, type[GenericHtmlRenderer]] = {
|
||||
GenericHtmlRenderer.renderer_id: GenericHtmlRenderer
|
||||
}
|
||||
|
||||
|
||||
def renderer_for(view: RenderView) -> Renderer:
|
||||
def renderer_for(view: RenderView, *, incremental: bool = True) -> Renderer:
|
||||
factory = _RENDERERS.get(view.renderer)
|
||||
if factory is None:
|
||||
raise DocForgeError(
|
||||
"unsupported_renderer", "View does not name a supported built-in renderer"
|
||||
)
|
||||
return factory()
|
||||
return factory(incremental=incremental)
|
||||
|
||||
|
||||
def relative_output(snapshot: ProjectSnapshot, path: Path) -> str:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue