Skip to content

angee.knowledge.models

Source models for the knowledge addon.

A :class:Vault is the permission and namespace boundary; every addressable thing inside it is a :class:Page — a thin identity row whose kind-specific content lives in one-to-one sidecars. This addon ships :class:MarkdownPage, the body sidecar for markdown-based kinds; extension addons contribute further kinds by writing new kind values and their own sidecar model with a one-to-one to knowledge.Page.

OutlineEntry

python
@dataclass(frozen=True)
class OutlineEntry()

One ATX heading in a markdown body's outline.

line is the 0-based source line of the heading in the CRLF-normalized body, the coordinate :meth:MarkdownPage.section_range slices on.

python
def parse_wikilinks(body: str) -> dict[str, str]

Return {target_title: display_text} for each [[wikilink]] in body.

The target is the text before | with any fragment stripped; the display text is the part after |. First occurrence of a target wins.

VaultManager

python
class VaultManager(AngeeManager)

Factories for actor-owned vault writes.

create_for

python
def create_for(owner: Any, **fields: Any) -> Any

Create a vault owned by owner after the REBAC create preflight.

owner must be the acting user — ownership on behalf of someone else is refused so the row the gate authorized is the row written.

Vault

python
class Vault(SqidMixin, AuditMixin, AngeeModel, HistoryMixin)

Top-level page container; the permission and namespace boundary.

Deleting a vault cascade-deletes every page inside it; the crud delete mutation previews that blast radius before confirming. owner is protected so deleting a user account never silently wipes their vaults.

retrieval_class

Registry key for the retrieval backend this vault searches through.

Meta

python
class Meta()

Django model options.

__str__

python
def __str__() -> str

Return the vault name for Django displays.

retrieval

python
@property
def retrieval() -> RetrievalBackend

Return the retrieval backend this vault's retrieval_class selects.

The vault is both the search namespace and the per-namespace selection point: retrieval_class names the backend (default lexical) and this binds it, mirroring InferenceProvider.backend.

retrieval_for

python
def retrieval_for(key: str) -> RetrievalBackend

Return the registered retrieval backend for key, bound to this vault.

The single public resolution seam over the vault-owned retrieval_class registry: callers (this model's retrieval property, a semantic plugin forcing its own key) ask the vault rather than re-deriving the field's internals — so ImplClassField stays the only thing that decodes the registry, and the boundary is a method, not _meta shape-probing.

PageManager

python
class PageManager(AngeeManager)

Factories for actor-scoped page writes.

create_in

python
def create_in(vault: Any, **fields: Any) -> Any

Create a page in vault after the REBAC create preflight.

The preflight evaluates the schema's create = vault->write with the relations the new row would carry, so only actors who can write the vault may add pages to it. A parent from another vault is refused — the vault is the permission boundary, and a foreign parent would extend parent->read/parent->write across it.

Page

python
class Page(SqidMixin, AuditMixin, AngeeModel, HistoryMixin)

Universal addressable content node inside a vault.

A page is thin identity — title, hierarchy, and the kind discriminator. Kind-specific content lives in one-to-one sidecar models; this addon ships :class:MarkdownPage for markdown-based kinds.

Kind

python
class Kind(models.TextChoices)

Built-in page kinds.

kind itself is an open CharField — extension addons store their own kind values and pair them with their own sidecar model (a one-to-one to knowledge.Page) without touching this model.

Meta

python
class Meta()

Django model options.

__str__

python
def __str__() -> str

Return the page title for Django displays.

StaleBodyError

python
class StaleBodyError(ValueError)

Raised when a body write expects a hash the stored body no longer has.

UnsupportedPageKindError

python
class UnsupportedPageKindError(ValueError)

Raised when a body write targets a page kind without a markdown sidecar.

StructuredEditError

python
class StructuredEditError(ValueError)

Base for a structure-aware markdown edit that cannot be applied.

SectionNotFoundError

python
class SectionNotFoundError(StructuredEditError)

Raised when a heading path (or replace target) matches nothing.

AmbiguousMatchError

python
class AmbiguousMatchError(StructuredEditError)

Raised when a heading path (or replace target) matches more than once.

MarkdownPageManager

python
class MarkdownPageManager(AngeeManager)

Factories for actor-scoped markdown body writes.

write_body

python
def write_body(page: Any,
               body: str,
               *,
               expected_hash: str | None = None) -> Any

Create or update page's markdown body, last-write-wins.

expected_hash is an optimistic-concurrency token: when supplied and the stored body_hash differs, the write is rejected with :class:StaleBodyError so the caller can reload and retry.

patch_section

python
def patch_section(page: Any,
                  heading_path: str | list[str],
                  op: str,
                  content: str,
                  *,
                  expected_hash: str | None = None) -> Any

Replace/append/prepend the section at heading_path and write the body.

Splices through :meth:MarkdownPage.spliced_section, which fails fast with :class:SectionNotFoundError/:class:AmbiguousMatchError before any write.

replace_unique

python
def replace_unique(page: Any,
                   old: str,
                   new: str,
                   *,
                   expected_hash: str | None = None) -> Any

Replace the single occurrence of old with new and write the body.

Splices through :meth:MarkdownPage.spliced_unique (exact-string, uniqueness enforced), so a non-unique or absent target fails fast before any write.

append

python
def append(page: Any,
           content: str,
           *,
           expected_hash: str | None = None) -> Any

Append content to the end of page's body and write it.

prepend

python
def prepend(page: Any,
            content: str,
            *,
            expected_hash: str | None = None) -> Any

Prepend content to the start of page's body and write it.

MarkdownPage

python
class MarkdownPage(SqidMixin, AuditMixin, AngeeModel, RevisionMixin)

Markdown body sidecar for markdown-based page kinds.

body is the canonical content store; body_hash and word_count are derived on save. Body edits are versioned through revisions so they can be rolled back.

page_kinds

Page kinds that carry a markdown body sidecar.

excerpt_chars

Number of body characters surfaced by :attr:excerpt.

SECTION_OPS

Section splice operations accepted by :meth:spliced_section.

Meta

python
class Meta()

Django model options.

__str__

python
def __str__() -> str

Return the owning page id for Django displays.

hash_body

python
@staticmethod
def hash_body(body: str) -> str

Return the canonical content hash for one body text.

excerpt

python
@property
def excerpt() -> str

Return the leading body characters used for list previews.

outline

python
@property
def outline() -> list[OutlineEntry]

Return this body's heading outline (see :meth:parse_outline).

parse_outline

python
@staticmethod
def parse_outline(body: str) -> list[OutlineEntry]

Return the ordered ATX headings in body as :class:OutlineEntry.

Heading levels and source lines come straight from markdown-it-py's heading_open block tokens (.tag → level, .map[0] → line); the text is the following inline token's .content. Setext (underline) headings are skipped — section addressing keys on single-line ATX headings.

section_range

python
@staticmethod
def section_range(body: str, heading_path: str | list[str]) -> tuple[int, int]

Resolve heading_path to its [start, end) line range in body.

heading_path is a single heading text or an ancestor chain (["Usage", "CLI"]); it tail-matches each heading's ancestor path, case-insensitively, so ["CLI"] and the qualified path both resolve. Lines are 0-based into the CRLF-normalized body. The range runs from the heading line to the next heading of the same-or-higher level (children included), or end-of-body. Fail-fast: :class:SectionNotFoundError when nothing matches, :class:AmbiguousMatchError when more than one does.

spliced_section

python
@staticmethod
def spliced_section(body: str, heading_path: str | list[str], op: str,
                    content: str) -> str

Return body with one section's content spliced, never re-rendered.

op is one of :attr:SECTION_OPS: replace swaps the section body, append/prepend add content after/before it (after nested children for append — the range is section-inclusive). The heading line and everything outside the section are byte-identical (after CRLF normalization). One blank line separates the section from the next heading (or terminates the body); blank lines inside the preserved body — e.g. inside a code block — are untouched.

spliced_unique

python
@staticmethod
def spliced_unique(body: str, old: str, new: str) -> str

Return body with the single occurrence of old replaced by new.

Exact-string match, uniqueness enforced: :class:SectionNotFoundError when old is absent, :class:AmbiguousMatchError when it occurs more than once, so an edit can never silently land on the wrong span.

appended

python
@staticmethod
def appended(body: str, content: str) -> str

Return body with content joined after it, one blank line apart.

Whole-body assembly counterpart to :meth:spliced_section: content lands after the existing text with the same single-blank seam :meth:_join_blocks gives a section splice — no markdown is parsed or re-rendered.

prepended

python
@staticmethod
def prepended(body: str, content: str) -> str

Return body with content joined before it, one blank line apart.

The prepend counterpart to :meth:appended (same single-blank seam).

save

python
def save(*args: Any, **kwargs: Any) -> None

Persist the body together with its derived hash and word count.

LinkManager

python
class LinkManager(AngeeManager)

Owns the wikilink edge set derived from page bodies.

rebuild_for

python
def rebuild_for(markdown: Any) -> None

Replace the source page's outgoing links from its current body.

The indexer is the author: it resolves [[title]] targets against the page's own vault and DELETE+INSERTs the edge set under system_context. There is no per-link gate — backlink reads inherit the source page's permissions through the schema. A target created after the link still resolves on the source page's next save.

python
class Link(SqidMixin, AngeeModel)

Wikilink edge from one page to another, derived from the source body.

Indexer-authored (see :class:LinkManager) — no user-facing mutation, no audit author. REBAC read/write inherit through source_page.

Meta

python
class Meta()

Django model options.

__str__

python
def __str__() -> str

Return the link target text for Django displays.

Released under the AGPL-3.0 License.