provcat.hooks
Each operation of the catalog passes through a spot, for example resolve,
pin or fetch. A hook at a spot wraps the work of the catalog, as
middleware wraps a request in a web framework:
def deny_physical(ctx, req, next):
if req.logical is None and req.spot != "session":
raise AccessDenied(f"{req.physical}: use a logical name")
return next(req)A recorder calls next and then records the request and the answer. A gate
raises an error, or calls next. Hooks run before the base cache, so a read
from the cache passes the gates too.
A program that runs the catalog for others, such as an orchestrator, passes its hooks to each Catalog, so several apps can share one process. The hooks of the process, from the proxy configuration below, run last, next to the store, so they see the request that runs::
cat = Catalog(config, hooks=Hooks({"*": [record]}), app="ingest", principal="alice")
cat = Catalog(config, hooks=hooks.load("ingest", "hooks.yaml"))Otherwise the proxy configuration gives each app its hooks. It is a local
YAML file, in PROVCAT_HOOKS or at ~/.config/provcat/hooks.yaml:
apps:
default: {}
notebook:
cache_root: ~/.cache/provcat-notebook
"*": [provcat.log]
fetch: [provcat.logical_only]Each name is a Python entry point in the group provcat.hooks. The hooks
under "*" run at every spot, first. The app comes from PROVCAT_APP, which
the process reads once, when it opens its first catalog.
class AccessDenied
A gate hook refused a request.
class Context
Context(app: str | None, principal: str, tenant: str | None)Who uses the catalog: the app, the principal and the tenant.
| Parameter | Type | Default | Description |
|---|---|---|---|
app |
str | None | required | The app whose hooks run: “default” for the default section of a hooks file; None with no app and no hooks file |
principal |
str | required | Who uses the catalog: a caller’s principal=, or the user of the process |
tenant |
str | None | required | The tenant of the catalog; None for the admin |
class Request
Request(spot: str, logical: Uri | None = None, physical: Uri | None = None, args: dict[str, Any] = <factory>)One operation at one spot.
args holds the rest of the request: the message of a commit, the
source of a branch, event (“open” or “close”) at session, mode
at open and close, offset and length at read_block, and
the merged ranges read at close.
| Parameter | Type | Default | Description |
|---|---|---|---|
spot |
str | required | The lifecycle spot, such as resolve, pin or fetch |
logical |
Uri | None | None |
The logical URI that started the operation; None for a physical URI |
physical |
Uri | None | None |
The physical URI; None at resolve and session, before any lookup |
args |
dict[str, Any] | <factory> |
The rest of the request, which differs by spot |
class Hooks
Hooks(chains: Mapping[str, list[Hook]] | None = None)The hook chain of each spot for one app.
The hooks under "*" run at every spot, before the hooks of the spot, as in the proxy configuration::
Hooks({"*": [record], "fetch": [only_logical]})| Parameter | Type | Default | Description |
|---|---|---|---|
chains |
Mapping[str, list[Hook]] | None | None |
Each spot, or “*”, to its list of hooks; ConfigError for an unknown spot or a hook listed twice |
Hooks.then
Hooks.then(other: Hooks) -> HooksJoin two sets of hooks, these first. A hook in both runs twice, which a gate or a recorder allows.
| Parameter | Type | Default | Description |
|---|---|---|---|
other |
Hooks | required | The hooks to run after these |
| returns | Hooks | New hooks that run these, and then other’s, at each spot |
Hooks.same_as
Hooks.same_as(other: Hooks) -> boolCompare two sets of hooks, hook by hook; a hook’s own eq is not asked.
| Parameter | Type | Default | Description |
|---|---|---|---|
other |
Hooks | required | The hooks to compare with |
| returns | bool | Whether other runs the same hooks at each spot, in the same order |
Hooks.run
Hooks.run(ctx: Context, req: Request, work: Callable[[Request], Any]) -> AnyRun the chain of the request’s spot around work. A hook that changes the spot raises ProvcatError.
| Parameter | Type | Default | Description |
|---|---|---|---|
ctx |
Context | required | Who uses the catalog, passed to each hook |
req |
Request | required | The request; its spot picks the chain |
work |
Callable[[Request], Any] | required | The catalog’s own work, called with the request that the last hook passes on |
| returns | Any | What work returns, as the hooks pass it back |
class Proxy
Proxy(app: str | None, cache_root: str | None, hooks: Hooks)The part of the proxy configuration that one app gets.
| Parameter | Type | Default | Description |
|---|---|---|---|
app |
str | None | required | The app’s name; None when there was no app and no file |
cache_root |
str | None | required | The app’s cache folder from the file, with ~ expanded; None for none |
hooks |
Hooks | required | The app’s hooks |
load
load(app: str | None, path: str | Path | None = None) -> ProxyLoad one app’s hooks and cache root from the proxy configuration, a hooks file.
With no app and no file there are no hooks. A named app with no file or no section is a ConfigError, so a missing file never turns a gate off quietly.
| Parameter | Type | Default | Description |
|---|---|---|---|
app |
str | None | required | The app to load; None loads the default section |
path |
str | Path | None | None |
The hooks file; None takes PROVCAT_HOOKS, then ~/.config/provcat/hooks.yaml |
| returns | Proxy | The app’s name, cache root and hooks |
log
log(ctx: Context, req: Request, next: Callable[[Request], Any]) -> AnyA recorder: one JSON line on stderr for each request, after it ends.
| Parameter | Type | Default | Description |
|---|---|---|---|
ctx |
Context | required | Who uses the catalog |
req |
Request | required | The request at this spot |
next |
Callable[[Request], Any] | required | The rest of the chain and the work |
| returns | Any | What next returns, unchanged |
logical_only
logical_only(ctx: Context, req: Request, next: Callable[[Request], Any]) -> AnyA gate: refuse a physical URI, so the app reads and writes through the names of its configuration.
It holds back code that cooperates; code that writes its own configuration can name any place. The session spot always passes.
| Parameter | Type | Default | Description |
|---|---|---|---|
ctx |
Context | required | Who uses the catalog |
req |
Request | required | The request at this spot |
next |
Callable[[Request], Any] | required | The rest of the chain and the work |
| returns | Any | What next returns; AccessDenied for a physical URI |