Skip to content

symfonic.core.prompt.blocks.protocol_read

protocol_read

The two read capabilities a block source can present.

Split out of :mod:symfonic.core.prompt.blocks.protocol (441 lines against the 300-line budget). protocol keeps the capability argument -- the long docstring on why these are structural Protocols rather than self-reported flags -- along with WritableBlockSource, the one capability that mutates. The two read capabilities live here.

The direction is one-way and must stay that way: WritableBlockSource extends HistoryCapableBlockSource, so protocol imports this module and nothing here may import protocol back. protocol re-exports both names.

BlockSource

Bases: Protocol

Reads the current revision of a prompt block for a scope.

offline_safe and scope_aware are declared members, so isinstance requires them to be present: a source that never decided whether it can be reached offline does not satisfy the Protocol.

  • offline_safe -- False forfeits authored-spine survival when the backing system is unreachable.
  • scope_aware -- False means the source is deployment-global: it serves one value for every tenant. Pairing such a source with a non-deployment block scope is a construction-time error.

isinstance verifies that load and both flags exist. It does not verify that load honours scope or block_id -- a source that was never implemented is caught; a source implemented badly is not.

Both flags are declared as abstract properties, not bare annotations, so the nominal inheritance path is closed the same way the methods are: typing._ProtocolMeta.__instancecheck__ short-circuits on the real-subclass check before ever consulting a Protocol's data members, so a bare annotation is only ever enforced on the duck-typed path -- a class Mine(BlockSource) that implements load but forgets offline_safe would answer isinstance truthfully while mine.offline_safe raised AttributeError. Marking them abstract makes that subclass un-instantiable instead, matching what @abstractmethod already does for load itself. A duck-typed source setting a plain offline_safe = True class attribute is unaffected: ABCMeta clears an inherited abstract name the moment the subclass provides any non-abstract value for it, property or plain attribute alike.

load abstractmethod async

load(scope: TenantScope, block_id: str) -> BlockRevision

Return the current revision of block_id for scope.

scope is required and typed :class:TenantScope: the isolation argument cannot be dropped, and the isolation key is scope.scope_path (see :func:block_isolation_key), never scope.tenant_id alone.

block_id is the block spec's name. A source backing several blocks selects by it instead of returning an arbitrary row for the scope. A single-block source may ignore it, but must accept it.

Source code in src/symfonic/core/prompt/blocks/protocol_read.py
@abstractmethod
async def load(self, scope: TenantScope, block_id: str) -> BlockRevision:
    """Return the current revision of ``block_id`` for ``scope``.

    ``scope`` is required and typed :class:`TenantScope`: the
    isolation argument cannot be dropped, and the isolation key is
    ``scope.scope_path`` (see :func:`block_isolation_key`), never
    ``scope.tenant_id`` alone.

    ``block_id`` is the block spec's name. A source backing several
    blocks selects by it instead of returning an arbitrary row for
    the scope. A single-block source may ignore it, but must accept
    it.
    """
    ...

HistoryCapableBlockSource

Bases: BlockSource, Protocol

A :class:BlockSource whose prior revisions remain retrievable.

An operator-editable block must be backed by one of these: being able to see what a block used to say, and to identify the revision to go back to, is the whole restore path core requires.

Both methods take the same (scope, block_id) key as :meth:BlockSource.load, by signature. History is cumulative, so a history read that forgot isolation would leak strictly more than a current-value read; there is no overload omitting scope, so the argument cannot be forgotten.

isinstance(src, HistoryCapableBlockSource) is True only when both history methods exist. It catches the source that never implemented them -- the common case, since a store with no versioning cannot present them. It does not catch a source that implements them badly: returning an empty list, ignoring block_id, or fabricating revisions all pass presence checks. Only that adapter's own tests can catch those.

Note that this Protocol carries no write verb. Reading history does not imply permission to change it.

list_revisions abstractmethod async

list_revisions(scope: TenantScope, block_id: str) -> Sequence[BlockRevision]

Return the known revisions of block_id for scope.

Ordering is the adapter's own (a git log, an append-only table). Every returned revision must belong to this scope_path and this block_id.

Source code in src/symfonic/core/prompt/blocks/protocol_read.py
@abstractmethod
async def list_revisions(
    self, scope: TenantScope, block_id: str
) -> Sequence[BlockRevision]:
    """Return the known revisions of ``block_id`` for ``scope``.

    Ordering is the adapter's own (a git log, an append-only table).
    Every returned revision must belong to this ``scope_path`` and
    this ``block_id``.
    """
    ...

load_revision abstractmethod async

load_revision(scope: TenantScope, block_id: str, revision: str) -> BlockRevision

Return one specific prior revision of block_id.

scope is required for the same reason it is on :meth:BlockSource.load: a revision table keyed only by block_id, with the scope recorded on the current-value row alone, would expose every tenant's history through this method.

Source code in src/symfonic/core/prompt/blocks/protocol_read.py
@abstractmethod
async def load_revision(
    self, scope: TenantScope, block_id: str, revision: str
) -> BlockRevision:
    """Return one specific prior revision of ``block_id``.

    ``scope`` is required for the same reason it is on
    :meth:`BlockSource.load`: a revision table keyed only by
    ``block_id``, with the scope recorded on the current-value row
    alone, would expose every tenant's history through this method.
    """
    ...