API Reference — The Toolbox
The contained subpackages: data formats, the test harness, scaffolding, diagnostics, and the registry projection. The kernel imports none of them; each is usable — or removable — on its own.
Each listing is derived from the module's own __all__ at build time — a new
export appears here on the next build, with no edit to this page.
spoc.formats
The format errors are part of the same surface and render here alongside the
functions — FormatError is the one to catch.
spoc.formats — read, write, collect, and address structured data.
Five formats normalize to one representation drawn from the JSON data model, and everything
else is expressed against that: Any Format → JSON → Any Format. JSON, CSV, and TOML
reading are standard library and work on a bare install; YAML, XML, and TOML writing live
behind extras (pip install "spoc[full]") and say so when they are missing.
from spoc import formats
settings = formats.read("config/app.yaml")
data = formats.collect("data") # a tree of mixed formats, one mapping
port = formats.pointer(settings, "/server/port") # exact — raises if absent
live = formats.query(data["users"], "$[?@.active == true].email") # query — may be empty
This is a contained subpackage: the kernel never imports it, importing spoc
never loads it, and its errors are not kernel errors — a boundary the test suite
enforces. Nothing here is invoked by Framework.start — reading spoc.toml
remains the kernel's own job through stdlib tomllib.
This module is the composition root. It owns the one registry; everything else takes it as an argument and holds no state.
Codec(name, extensions, reader=None, writer=None, read_extra=None, write_extra=None)
dataclass
One format's declaration: its names, and a lazy factory per direction.
FormatRegistry(codecs)
Codecs keyed by name and by extension, settled on first use and cached.
Settled, not merely resolved: the first probe of a direction records whether it is available or unavailable, so neither answer is derived twice.
names
property
Every declared format name, ordered.
codec(name)
The declaration for one format.
for_extension(extension)
The declaration answering to a file extension, including the dot.
function(name, direction)
Resolve one direction of one format, importing its dependency if needed.
Availability is settled on first probe and holds for the life of the process, failure as much as success: a dependency installed while this process runs is observed by the next one, not this one. That is the same rule every other import in the process already follows, and the price of not re-running discovery that has already answered.
Probe and settle happen under one lock acquisition, the same rule the kernel registry's derive-on-miss reads follow: releasing between them would let two threads resolve the same direction twice and the second write clobber the first settle. Holding the lock across the factory import is deliberate — contention exists only on a direction's first probe, which settles once per process.
supported()
Every format with the directions currently available, probed not assumed.
FormatSupport(name, extensions, can_read, can_write)
dataclass
What one format can do in the current environment.
CollectionError(path, reason)
A file in a collection could not be read, so the whole collection fails.
DecodeError(format_name, reason)
A source is not valid content for the format it was read as.
DuplicateEntryError(key, first, second)
Two collected files derive the same key.
EncodeError(format_name, reason)
A value cannot be expressed in the target format.
FormatError
Base for every data-surface error.
MalformedAddressError(address, standard, reason)
An address or query is not valid syntax under the standard it follows.
MissingDependencyError(capability, extra)
A capability is supported, but the extra that enables it is not installed.
PointerResolutionError(pointer, segment)
An exact address named a location that does not exist.
UnknownFormatError(value, supported)
A format name or file extension maps to no codec.
UnsupportedDirectionError(format_name, direction)
A format is known, but cannot be used in the requested direction at all.
Collection(entries=dict(), skipped=())
dataclass
One mapping of every collected entry, plus what was skipped getting there.
pointer(value, reference)
Resolve an RFC 6901 pointer to exactly one value, or raise naming the segment.
query(value, expression)
Apply an RFC 9535 query, returning every match — possibly none.
loads(text, format, **options)
Decode text in the named format into the representation.
dumps(value, format, **options)
Encode a representation value as text in the named format.
read(path, *, format=None, **options)
Read a file, inferring the format from its extension unless one is given.
write(value, path, *, format=None, **options)
Write a representation value to a file, inferring the format from its extension.
collect(root, *, options=None, ignore=())
Read every supported file under root into one mapping, eagerly.
Hidden entries are skipped; ignore globs extend that skip set.
supported()
Every format with the directions available in this environment.
spoc.testing
Test harness for SPOC applications — a contained subpackage.
The kernel never imports this; importing :mod:spoc never loads it. The
harness composes the kernel's public contracts only, so it is usable from
any test runner or from a plain script:
.. code-block:: python
from pathlib import Path
from spoc.testing import ProjectTree, isolated, mode
tree = ProjectTree(apps={"blog": {"models": MODELS_SOURCE}})
base = tree.build(Path(tmp))
with isolated(base, "models") as fw:
record = fw.resolve("models:blog.post")
Pytest surfaces the same pieces as fixtures through the in-distribution
plugin (:mod:spoc.testing.plugin); nothing here requires pytest.
MissingDependencyError(capability, extra)
A capability is supported, but the extra that enables it is not installed.
Mirrors the spoc.formats contract: name the extra to install, never
leak a transitive ImportError.
ProjectTree(apps=dict(), config=dict())
dataclass
A declarative description of a SPOC project on disk.
apps maps app name → (module name → source). Source strings are
dedented on write, so triple-quoted literals indent naturally at the call
site. config entries merge over the generated [spoc] table —
by default every declared app is listed under the development mode.
build(root, name='project')
Materialize the tree under root and return the project base.
import_state()
Snapshot sys.path and sys.modules; restore both on exit.
The building block under :func:isolated, exposed for suites that manage
framework lifecycles themselves but still must not leak app imports
between tests.
isolated(base_dir, *kinds, framework=None, start=True)
Yield a framework booted against base_dir, torn down on every exit.
The scope makes base_dir importable (exactly as a real entry point's
script directory would be), snapshots sys.path and sys.modules,
and on exit — normal or exceptional — shuts the framework down and
restores both snapshots, so consecutive scopes observe nothing from each
other.
Pass kinds to have the scope construct the framework, or a prebuilt
framework= to configure declaration (hooks, :class:KindSpec) first —
stating both is contradictory and refused. start=False yields an inert
framework for tests that exercise boot itself.
mode(base_dir, mode_name)
Run the body with the tree's declared mode swapped to mode_name.
Rewrites spoc.mode in the tree's spoc.toml on entry and restores
the file's original bytes on exit, so mode-dependent behavior can be
exercised without permanently altering the tree. Boot inside the scope —
the kernel reads the file at start().
spoc.scaffold
The library behind spoc init and spoc app, callable from your own code —
a downstream framework can ship its own templates and entry point.
The operations take their ports as arguments, so generation is callable without
argv and testable without a filesystem or a network. InstalledTemplateSources
resolves a reference by its form; pass it a RemoteTemplateSource to enable
retrieval, or leave it out and only local sets resolve.
Project scaffolding for spoc — the spoc init and spoc app surfaces.
Used once, at the beginning of a project: it emits a configuration file, a framework declaration, one app, an entry point that starts unedited, and a record of the template set it all came from. That generated app is also the worked example for adding the second one by hand.
A template set may be built in, installed by a downstream distribution, read from a directory, or retrieved from a remote location — the reference's own spelling decides which, before anything is looked up. Retrieved content is admitted rather than trusted, and everything after admission is identical to a local set: origin buys no capability.
The kernel does not import anything from here. The dependency runs one way, so this package can be deleted without touching the kernel — and nothing in it requires a dependency the kernel does not already have, retrieval included.
What this namespace publishes. A name appears below only because a consumer outside this package must write it to do something the package offers: invoke an operation, implement a contract it accepts, distinguish a failure it can respond to differently, or supply a value it reads. Everything else — the retrieval ports and their adapters, archive admission, the record-writing half of provenance, and the error leaves that admit no distinct response — is this package assembling itself, and stays in its defining submodule.
Those submodules remain importable, and importing from one is a normal thing to do. It carries no stability promise: reaching an internal element is not a promotion. What changed is what is promised, not what is reachable.
RetrievalError(reference, reason)
Raised when a remote reference cannot be retrieved.
ScaffoldError(message, module_name=None)
Base for every scaffolding failure.
TargetNotEmptyError(target)
Raised when the destination directory already contains content.
TemplateSetNotFoundError(reference, candidates)
Raised when a named template set cannot be resolved.
Candidates come only from sources that can actually enumerate themselves, so this never presents an invented list as the set of possibilities.
UnrecognizedReferenceError(reference, segment, forms)
Raised when a reference matches no known form.
Distinct from :class:TemplateSetNotFoundError on purpose: that one means
"this named a form I understand, and it was not there", this one means "I do
not know what you asked for". Reporting the second as the first is how a
mistyped scheme ends up complaining about a missing directory nobody named.
AddedApp(plan, app_dir, config_reference, divergence=None)
dataclass
What add_app did: the files written (paths relative to app_dir),
where the app landed, the dotted reference that installs it, and anything
the caller should be told about the project it landed in.
divergence = None
class-attribute
instance-attribute
Set when the rendered template set differs from the project's recorded origin, or when the project records none. Never a reason to fail.
EnumerableSource
A source that can list what it holds.
Split from :class:TemplateSource because a remote resolver cannot answer
it: there is no set of "all retrievable references". Only sources that
implement this contribute candidates to a not-found error, so that error
never lists a set of possibilities it made up.
Provisional: may change incompatibly in a minor release. It settles when a template source outside this package implements it — at which point the shape is fixed by a real implementer — or is withdrawn if none appears and the not-found error's candidates can be gathered without a second port.
available()
Every template set name this source can load.
GenerationPlan(files)
dataclass
An immutable, ordered set of files to emit.
Ordering is preserved so generation is deterministic and diffable; nothing in the writing path depends on it.
paths
property
Every destination path this plan would write.
PlannedFile(path, content)
dataclass
One file a plan will emit, with its content already rendered.
path
instance-attribute
Destination path, relative to the target root, using forward slashes.
ProjectSink
Writes a plan, and reports what already exists.
location()
Human-readable name of where the plan would land — used in errors.
existing(paths)
Which of these relative paths already exist at the destination.
is_empty()
True when the destination is absent or contains nothing.
commit(plan)
Write every file in the plan, or none of them. Refuses a non-empty destination itself — the guarantee lives here, not in the caller.
TemplateFile(source, target, content, per_kind=False)
dataclass
One template in a set: where it comes from, where it lands.
source
instance-attribute
Identifier of the template within its set — used only in errors.
target
instance-attribute
Destination path, itself substitutable.
per_kind = False
class-attribute
instance-attribute
When true, this template is emitted once per declared kind.
TemplateSet(name, values, files, reference='', revision='')
dataclass
A loaded template set: its declared substitution values and its files.
values is the enumerable declaration the spec requires — it can be read
without rendering anything.
reference = ''
class-attribute
instance-attribute
The reference this set was resolved from, as the caller spelled it.
revision = ''
class-attribute
instance-attribute
The exact revision it resolved to, when it came from somewhere that has one. Empty for a set that cannot move — a built-in or a local directory.
TemplateSource
Yields template sets by name.
load(name)
Return the named template set, or raise TemplateSetNotFoundError.
Origin(reference, revision, set_name)
dataclass
The template set a project was generated from.
Provisional: may change incompatibly in a minor release. It settles when the
project decides whether the record must also carry the substitution values a
generation used — the project name, app name, and kinds. Today it carries
only where the templates came from, which is enough to notice divergence but
not enough to re-render what was generated. The question was raised when an
update operation was considered and declined; until it is answered, the
record's shape cannot be promised. :data:RECORD_NAME and
:func:read_origin settle with it.
describe()
One line naming this origin as precisely as it can be named.
DirectorySink(destination)
Writes a plan into a destination directory.
Implements the :class:~spoc.scaffold.plan.ProjectSink port.
commit(plan)
Write every file in the plan, or none of them.
The never-overwrite guarantee is enforced here, not left to callers: a non-empty destination is refused before anything is staged.
InstalledTemplateSources(remote=None)
Resolves any template set reference, dispatching on the form it designates.
Implements the :class:~spoc.scaffold.plan.EnumerableSource port.
Resolution is scheme-first and total: the reference's own form decides which kind of source is consulted, before anything is looked up. That ordering is the contract — a reference that designates one kind must never fall through to another because the first came up empty, or a mistyped scheme ends up reported as a missing directory nobody named.
available() lists only what can genuinely be enumerated. A remote
reference has no candidate set, so none is invented for it.
register(subcommands, derive_kinds=None, source_factory=None)
Mount init and app on a parser you own.
This is how a framework built on SPOC publishes the generation line under
its own command name — hello init rather than a second program the
author's users have to know about. The shipped spoc program mounts these
commands the same way, so there is no privileged assembly path this one
cannot reach.
derive_kinds and source_factory are injected by the composition root
— the scaffold never imports the surface that can locate a framework
declaration, and never decides for itself which sources exist. Without a
factory it resolves local template sets only, so mounting this surface never
silently acquires a network path.
Provisional: may change incompatibly in a minor release. What is promised is
which commands the mount contributes and what invoking them does; the type of
subcommands is argparse's and not SPOC's, so promising it would commit
every downstream framework to argparse too. It settles when a framework
outside this package has actually mounted it — at which point the shape is
fixed against a real second caller rather than a guess — or when SPOC commits
to its parser choice and the mount can take a type it owns.
add_app(*, source, sink_factory, app_name, kinds, template_set='default', read_origin=None)
Generate one additional app into an existing project.
The selected template set's app-shaped files (those whose targets carry
$app_name) are rendered exactly as project generation renders them,
then committed under the app's own directory — so the never-overwrite
guarantee falls out of the sink's existing contract: an app that already
exists is refused with nothing written.
The project's configuration is never edited. The returned
config_reference is the dotted path the author adds to a mode list
under [spoc.apps] — stating it is the caller's (CLI's) job.
Raises:
-
InvalidSegmentError–A supplied name is not legal.
-
TemplateSetNotFoundError / IncompleteTemplateSetError–Bad template set, or one with no app-shaped files.
-
TargetNotEmptyError–The app already exists.
init_project(*, source, sink, project_name, app_name=DEFAULT_APP_NAME, kinds=DEFAULT_KINDS, template_set='default')
Generate a runnable project.
Every failure raises before sink.commit is reached, so a raised error
means nothing was written.
Parameters:
-
source(TemplateSource) –Where template sets come from.
-
sink(ProjectSink) –Where the plan is written.
-
project_name(str) –Name of the project; must satisfy the identity grammar.
-
app_name(str, default:DEFAULT_APP_NAME) –Name of the starter app.
-
kinds(tuple[str, ...], default:DEFAULT_KINDS) –The kinds the generated framework declares.
-
template_set(str, default:'default') –Which template set to render.
Returns:
-
GenerationPlan–The plan that was committed.
Raises:
-
InvalidSegmentError / PathEscapeError–A supplied name is not legal.
-
TemplateSetNotFoundError / IncompleteTemplateSetError–Bad template set.
-
TargetNotEmptyError / PathConflictError–The destination is occupied.
read_origin(project_root)
Read a project's origin record, or None when it carries none.
A malformed or partial record reads as absent rather than raising: the record is advisory, and failing an unrelated operation because a note is unparseable would make it a liability instead of a help.
Provisional: may change incompatibly in a minor release. It returns an
:class:Origin and settles with it.
spoc.diagnostics
The library behind spoc check, spoc list, and spoc explain. Records are
described by spoc.projection.ComponentEntry below — one registry has one
description, and these commands render it after a full boot.
The diagnostic operations — library-first, CLI-agnostic.
Each operation is an isolated dry boot composed from :mod:spoc.testing's
scopes: process state (sys.path, sys.modules) is restored and the
framework is shut down before the call returns, so nothing outlives it.
Findings reuse the kernel's own error text verbatim — the diagnostics never rephrase a failure the kernel already states precisely. A non-SPOC exception raised by an app's own module code propagates untouched, the same doctrine the lifecycle holds: that error is the app author's, and check imports your apps.
ComponentEntry(identifier, kind, namespace, object_name, location, shape)
dataclass
One registered component, described for a consumer outside the process.
The three grammar facets travel alongside the identifier they compose, so a
consumer never has to parse kind:namespace.object_name to filter on it —
the format states what the grammar already knows.
Finding(area, message)
dataclass
One problem check found. area is the phase that caught it
(config / locate / lifecycle / boot); message is the
kernel's own text.
CheckReport(findings)
dataclass
Everything check gathered; ok is the exit-code truth.
to_dict()
The check document, as the plain structures json serializes.
Defined on the report rather than in the CLI so a library caller and
spoc check --json emit the identical document. The version leads,
the projection document's own rule: it is what a consumer reads first
to decide whether it understands the rest.
check(base_dir, framework_ref=DEFAULT_FRAMEWORK_REF)
Validate a project before runtime; every finding is gathered, none stops the rest from being looked for.
list_records(base_dir, framework_ref=DEFAULT_FRAMEWORK_REF, kind=None, namespace=None)
Enumerate the registry, optionally narrowed by facet. An unknown kind fails with the kernel's candidate-naming error; namespaces are an open set, so an unknown one is simply empty.
Records are described by the registry projection — the one structure that
describes a component — so what spoc list reports and what the projection
publishes cannot drift. Only the boot depth differs, and deliberately: this
reports on a started project, which is the question list answers.
A kind narrowing is answered by reading that kind's facet, not by walking everything registered and discarding what does not match. That is the whole point of the registry being keyed by the grammar's own segments — a facet is a sub-dictionary of the store — and this was the one reader asking for the store when it wanted a facet.
Order comes from that read rather than being re-established here. Both readers enumerate in canonical identifier order, so sorting again was a second claim to a guarantee the registry already makes — the kind that stays correct right up until the two disagree. The namespace narrowing stays a filter: namespaces are an open set, and it runs over records already reduced to one kind.
explain(identifier, base_dir, framework_ref=DEFAULT_FRAMEWORK_REF)
Resolve one identifier and describe its record. Resolution failures are the kernel's own — a typo names the failing segment and candidates.
spoc.projection
The library behind spoc projection, and the one description of a registered
component that every other describing surface reads.
project() runs a collect-only boot — discovery without initialization — so a
project whose startup hooks would fail is still describable. dumps() renders
the document; schema_path() locates the published JSON Schema it validates
against. The document shape, not this dataclass, is the contract: see
the CLI page
for the format itself.
The registry, projected as data.
kind:namespace.object_name is the most durable thing this project owns: a
naming standard, not a Python API, implementable and queryable by systems that
never import spoc. This subpackage is what makes it portable — one document
describing what a project registered, validated by a published JSON Schema, so
a router generator, an admin surface, a documentation build, or a client in
another language can read a registry without parsing Python.
The document describes the registry as of the completion of discovery. Ready callbacks run inside discovery and are therefore included; anything a startup hook registers afterwards is not, and a consumer must not read the projection as a claim about a fully started process.
Nothing in the kernel imports this package. Like scaffold, stubs, and
formats, it is reached through the CLI or as a library call and depends
inward only — but unlike them it is also depended on: :mod:spoc.stubs builds
its manifest from a projection, so the stub and the document cannot disagree
about what a project registered.
ComponentEntry(identifier, kind, namespace, object_name, location, shape)
dataclass
One registered component, described for a consumer outside the process.
The three grammar facets travel alongside the identifier they compose, so a
consumer never has to parse kind:namespace.object_name to filter on it —
the format states what the grammar already knows.
Projection(kinds, components, format_version=FORMAT_VERSION)
dataclass
A booted registry, described as data.
kinds is the project's declared set, which is why a kind with no
components still appears: "declared and empty" and "never declared" are
different facts, and a consumer that cannot tell them apart cannot report
on either.
to_dict()
The document, as the plain structures json serializes.
Key order is fixed here rather than sorted on the way out: the version leads because it is what a consumer reads first to decide whether it understands the rest.
register(subcommands)
Mount projection on a parser you own.
Lets a framework built on SPOC hand its own users the registry as data under
its own command name. The shipped spoc program mounts this command the
same way.
Provisional: may change incompatibly in a minor release. What is promised is
which commands the mount contributes and what invoking them does; the type of
subcommands is argparse's and not SPOC's, so promising it would commit
every downstream framework to argparse too. It settles when a framework
outside this package has actually mounted it, or when SPOC commits to its
parser choice and the mount can take a type it owns. The document the
command writes is promised separately and more strongly — see
schema:projection/document.
dumps(projection)
Render projection as the canonical document text.
Deterministic by construction: entries arrive in canonical identifier order, key order is the dataclasses' own, and no value here depends on the clock, the filesystem, or the order apps were declared in.
Escapes non-ASCII rather than emitting it. The document is written to standard output and piped onward, and a console whose encoding is not UTF-8 would otherwise turn a describable project into an encoding error — the one failure mode a description must not have.
project(base_dir, framework_ref=DEFAULT_FRAMEWORK_REF)
Describe the project at base_dir, importing and registering nothing
that outlives the call.
The isolation scope is the same one every other dry-boot operation uses:
sys.path and sys.modules are restored, and the framework is reset,
before this returns.
schema_path()
The filesystem path of the published JSON Schema.
Consumers outside Python are expected to fetch the file from the project's repository; this exists so that anything already running in this process — the test suite, a documentation build, a validating consumer — reads the same bytes rather than a copy that could drift.
schema_text()
The published JSON Schema, as text.