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.

RecordBindingManager

python
class RecordBindingManager(AngeeManager)

Own polymorphic knowledge-to-record binding writes and reverse reads.

Django/Zed cannot field-back a relation through GenericForeignKey. The model's own Zed definition is therefore deliberately fail-closed; these authored methods are the only read/write surface and gate every operation on the target's canonical REBAC identity. Returned querysets are exact-filtered system querysets, never a general bypass.

create

python
def create(**kwargs: Any) -> models.Model

Create through the idempotent role-keyed upsert contract.

upsert

python
def upsert(*,
           target: models.Model,
           page: models.Model | None = None,
           vault: models.Model | None = None,
           role: str = DEFAULT_ROLE) -> models.Model

Return one binding per knowledge owner, canonical target, and role.

unbind

python
def unbind(*,
           target: models.Model,
           page: models.Model | None = None,
           vault: models.Model | None = None,
           role: str = DEFAULT_ROLE) -> int

Delete one role-keyed binding after both owners authorize the write.

teardown_for_record

python
def teardown_for_record(record: models.Model) -> None

Delete every binding to record before the target row disappears.

Canonical targets have no reverse GenericRelation because knowledge can bind any current or future REBAC model. The global knowledge-owned pre_delete receiver therefore delegates here so primary-key reuse cannot make an old binding resolve to a new row. Deleting the bindings normally keeps their post_delete change publisher and target read anchor intact.

for_record

python
def for_record(record: models.Model,
               *,
               role: str | None = None) -> models.QuerySet[Any]

Return bindings on one actor-readable canonical record.

pages_for_record

python
def pages_for_record(record: models.Model,
                     *,
                     role: str | None = None) -> models.QuerySet[Any]

Return page bindings on record without loading gated page content.

vaults_for_record

python
def vaults_for_record(record: models.Model,
                      *,
                      role: str | None = None) -> models.QuerySet[Any]

Return vault bindings on record without loading gated vault content.

for_knowledge

python
def for_knowledge(knowledge: models.Model,
                  *,
                  role: str | None = None) -> models.QuerySet[Any]

Return bindings from one readable page/vault to actor-readable targets.

records_for_page

python
def records_for_page(page: models.Model,
                     *,
                     role: str | None = None) -> tuple[RecordRef, ...]

Return public refs for actor-readable records bound from page.

records_for_vault

python
def records_for_vault(vault: models.Model,
                      *,
                      role: str | None = None) -> tuple[RecordRef, ...]

Return public refs for actor-readable records bound from vault.

RecordBinding

python
class RecordBinding(SqidMixin, AuditMixin, RecordRefMixin, AngeeModel)

Role-keyed edge from a knowledge Page/Vault to any REBAC record.

The target is canonicalized to its topmost REBAC-typed MTI ancestor. The edge never grants access to either side: authored reads ride target read while a returned page/vault id must still be resolved through that knowledge row's own REBAC policy before content is available.

Meta

python
class Meta()

Django model options for knowledge record bindings.

clean

python
def clean() -> None

Require exactly one knowledge owner and a REBAC-typed target.

change_read_resource

python
def change_read_resource() -> ObjectRef

Gate binding change events on the canonical target's read.

__str__

python
def __str__() -> str

Return a readable knowledge-to-record edge label.

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.