provcat.hooks

Hooks: functions that run at the lifecycle spots of the catalog.

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

source

A gate hook refused a request.

class Context

source

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

source

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

source

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

source

Hooks.then(other: Hooks) -> Hooks

Join 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

source

Hooks.same_as(other: Hooks) -> bool

Compare 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

source

Hooks.run(ctx: Context, req: Request, work: Callable[[Request], Any]) -> Any

Run 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

source

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

source

load(app: str | None, path: str | Path | None = None) -> Proxy

Load 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

source

log(ctx: Context, req: Request, next: Callable[[Request], Any]) -> Any

A 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

source

logical_only(ctx: Context, req: Request, next: Callable[[Request], Any]) -> Any

A 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