Start & Stop
A SPOC project has a simple life: asleep → starting → running → stopped. Nothing happens by accident, and every step either finishes or fails loudly.
What start() does, in order
- Read settings —
spoc.tomland the active mode's env file. - Import apps — every installed app's modules, in dependency order.
- Fill the shelf — discover every decorated block, tag it, register it.
- Ready callbacks — your
@framework.on_readyfunctions run once, with the full registry. - Wake the modules — for each app module: the kind's
on_startuphook fires, then the module's owninitialize()runs.
If anything fails, SPOC tears down what already came up and returns the
framework to its inert state — fix the cause and start() again cleanly.
shutdown() walks the same road backwards: each module's teardown() and the
kind's on_shutdown hook run in reverse order, then everything resets.
Per-module wake-up and clean-up
Any app module may define two plain functions:
from framework import model
@model
class Post: ...
def initialize():
print("blog models are awake")
def teardown():
print("blog models are done")
No decorator, no registration — if the functions exist, SPOC calls them at the
right moments. A module whose initialize() never completed is never asked to
teardown().
Around it, the quick-start shape — rules, settings, start button — and both moments print on cue:
from pathlib import Path
from framework import framework
BASE_DIR = Path(__file__).resolve().parent
framework.start(BASE_DIR) # blog models are awake
framework.shutdown() # blog models are done
Per-kind hooks
A kind can watch all its blocks come and go. The hook fires once per app module, receiving that app's blocks of the kind, in tag order:
import spoc
def warm_up(components):
for c in components:
print("starting:", c)
framework = spoc.Framework(
spoc.KindSpec("models", on_startup=warm_up),
)
Async projects
Everything above has an async twin. Declare coroutine hooks or an async
initialize()/teardown(), and boot with:
The rule is strict on purpose: the sync path (start) refuses coroutine
hooks with a clear error instead of quietly not awaiting them. Pick one path
per project.
Guard rails
- Starting twice → error. Stop first.
- Calling
start()orshutdown()from inside a hook or ready callback → error. The boot is half-built; there is nothing correct that call could do. - Two threads racing to start → exactly one boots; the other gets the already-started error.
- A
teardown()that raises → the error reaches you unchanged, and the framework still goes back to sleep. You can fix the cause andstart()again; it never gets stuck reporting itself running. - An
async defhook orinitialize()on the synchronous path → refused before anything runs, naming every one it found. Useastart().
A failing teardown() skips the ones behind it
Teardown stops at the module that raised, so modules earlier in the reverse order do not get torn down — and because shutdown resets the framework, they will not get a second chance. Anything they held open stays open for the life of the process.
This is deliberate. The alternative is to keep going and hand you a bundle of
errors, which would mean you no longer receive the exact exception your code
raised. So the kernel gives you the real error and a framework you can
restart, and leaves the leak to you: a teardown() that can raise should
catch its own failures if what it releases matters.
Reading SPOC's own log records
SPOC configures no logging and prints nothing. It writes to the spoc
logger, which is the handle to configure:
import logging
logging.getLogger("spoc").addHandler(logging.StreamHandler())
logging.getLogger("spoc").setLevel(logging.DEBUG)
Names below spoc follow the module path — spoc.framework, spoc.core.loader
— so you can turn one subsystem up without the rest. Treat those as internal:
they can move between releases. spoc itself will not.
The whole life, at a glance
stateDiagram-v2
[*] --> Asleep: Framework(...)
Asleep --> Running: start() — settings, apps, shelf, ready, initialize
Running --> Asleep: shutdown() — teardown in reverse, reset
Asleep --> Asleep: failed start() rolls itself back
Running --> Asleep: failed teardown() — error raised, reset happens anyway