# Registry

The workbench catalogue of what can be opened, the runner that streams catalogue scripts, and the process tree a stop ends.

## femforge\_workbench.registry

The workbench catalogue: specs of what can be opened, discovered from the built-ins and the femforge.workbenches entry points, imported only on open.

Type Aliases:

- **`Target`** –

  A `pkg.module:attr` string, or the module-level function it names.

Classes:

- **`WorkbenchKind`** –

  How a spec is opened: a dock of panels, one Renderable in a plotter, or a script run in a subprocess.
- **`OutputKind`** –

  How a script output is shown.
- **`OutputSpec`** –

  One file a script produces.
- **`WorkbenchSpec`** –

  One card of the catalogue: what it is, how it opens, and what it leaves behind.
- **`EntryPointLike`** –

  What discover needs from an entry point.

Functions:

- **`split_target`** –

  The module and optional attribute of a `pkg.module:attr` target.
- **`resolve`** –

  The object a target names, importing its module now: `pkg.module:attr` gives the attribute, `pkg.module` the module.
- **`script_module`** –

  The module name the running script imports as: its `-m` name, else its file's stem, since the script's directory leads sys.path.
- **`target_of`** –

  The `pkg.module:attr` string of a target; a function defined in the running script is named by the module its file imports as.
- **`script`** –

  A script card, run in a subprocess.
- **`panel`** –

  A dock card.
- **`renderable`** –

  A viewer card, rebuilt on save.
- **`legacy`** –

  The spec of a module following the old `main() -> Renderable` contract.
- **`discover`** –

  Every spec: the built-ins plus what each entry point in the group provides (a spec or a sequence), sorted by group and title; a name given two different specs is an error.

Attributes:

- **`BUILTIN`** (`tuple[WorkbenchSpec, ...]`) –

  The specs discover() carries without an entry point.

### BUILTIN

```python
BUILTIN: tuple[WorkbenchSpec, ...] = (LAUNCHER, DEMO, BROWSERS, FIELD_VIEWER, MONITORS)
```

The specs discover() carries without an entry point.

### Target

```python
Target = str | Callable[[], object]
```

A `pkg.module:attr` string, or the module-level function it names.

### WorkbenchKind

Bases: `StrEnum`

How a spec is opened: a dock of panels, one Renderable in a plotter, or a script run in a subprocess.

### OutputKind

Bases: `StrEnum`

How a script output is shown.

Methods:

- **`from_path`** –

  The kind a file's suffix implies, text when unknown.

#### from\_path

```python
from_path(path: Path) -> OutputKind
```

The kind a file's suffix implies, text when unknown.

### OutputSpec

```python
OutputSpec(path: Path, title: str, kind: OutputKind)
```

One file a script produces.

Methods:

- **`at`** –

  An output typed from its suffix and titled by its name unless given.
- **`modified`** –

  When the file was last written, None while it does not exist.

#### at

```python
at(path: Path, title: str | None = None) -> OutputSpec
```

An output typed from its suffix and titled by its name unless given.

#### modified

```python
modified() -> datetime | None
```

When the file was last written, None while it does not exist.

### WorkbenchSpec

```python
WorkbenchSpec(name: str, title: str, kind: WorkbenchKind, target: str, description: str = '', group: str = '', args: tuple[str, ...] = (), cwd: Path | None = None, outputs: tuple[OutputSpec, ...] = ())
```

One card of the catalogue: what it is, how it opens, and what it leaves behind.

Methods:

- **`last_run`** –

  The newest output timestamp, None when nothing has been produced.
- **`command`** –

  The subprocess that runs this spec: the module for a script, the workbench CLI for the rest.

Attributes:

- **`module`** (`str`) –

  The module the target names.

#### module

```python
module: str
```

The module the target names.

#### last\_run

```python
last_run() -> datetime | None
```

The newest output timestamp, None when nothing has been produced.

#### command

```python
command() -> list[str]
```

The subprocess that runs this spec: the module for a script, the workbench CLI for the rest.

### EntryPointLike

Bases: `Protocol`

What discover needs from an entry point.

Methods:

- **`load`** –

  Imports and returns what the entry point names.

Attributes:

- **`name`** (`str`) –

  The entry point's name.

#### name

```python
name: str
```

The entry point's name.

#### load

```python
load() -> object
```

Imports and returns what the entry point names.

### split\_target

```python
split_target(target: str) -> tuple[str, str | None]
```

The module and optional attribute of a `pkg.module:attr` target.

### resolve

```python
resolve(target: str) -> object
```

The object a target names, importing its module now: `pkg.module:attr` gives the attribute, `pkg.module` the module.

### script\_module

```python
script_module() -> str
```

The module name the running script imports as: its `-m` name, else its file's stem, since the script's directory leads sys.path.

### target\_of

```python
target_of(target: Target) -> str
```

The `pkg.module:attr` string of a target; a function defined in the running script is named by the module its file imports as.

### script

```python
script(name: str, title: str, *, target: str, description: str = '', group: str = '', args: Sequence[str] = (), cwd: Path | None = None, outputs: Sequence[OutputSpec] = ()) -> WorkbenchSpec
```

A script card, run in a subprocess.

Parameters:

- **`name`** (`str`) –

  The card's unique name, which the command line opens it by.
- **`title`** (`str`) –

  The title the card and the title menu show.
- **`target`** (`str`) –

  The module run with `-m`.
- **`description`** (`str`, default: `''` ) –

  The one-line summary under the title.
- **`group`** (`str`, default: `''` ) –

  The catalogue group the card is listed under.
- **`args`** (`Sequence[str]`, default: `()` ) –

  The arguments the script runs with.
- **`cwd`** (`Path | None`, default: `None` ) –

  The directory the script runs in; None keeps the shell's.
- **`outputs`** (`Sequence[OutputSpec]`, default: `()` ) –

  The files the script produces, which the launcher shows.

### panel

```python
panel(name: str, title: str, *, target: Target, description: str = '', group: str = '', args: Sequence[str] = ()) -> WorkbenchSpec
```

A dock card.

Parameters:

- **`name`** (`str`) –

  The card's unique name, which the command line opens it by.
- **`title`** (`str`) –

  The title the card and the title menu show.
- **`target`** (`Target`) –

  A module-level function returning the dock Split, or its `pkg.module:attr` name.
- **`description`** (`str`, default: `''` ) –

  The one-line summary under the title.
- **`group`** (`str`, default: `''` ) –

  The catalogue group the card is listed under.
- **`args`** (`Sequence[str]`, default: `()` ) –

  The arguments the card's command passes to the workbench command line.

### renderable

```python
renderable(name: str, title: str, *, target: Target, description: str = '', group: str = '', args: Sequence[str] = ()) -> WorkbenchSpec
```

A viewer card, rebuilt on save.

Parameters:

- **`name`** (`str`) –

  The card's unique name, which the command line opens it by.
- **`title`** (`str`) –

  The title the card and the title menu show.
- **`target`** (`Target`) –

  A module-level function returning the Renderable, or its `pkg.module:attr` name.
- **`description`** (`str`, default: `''` ) –

  The one-line summary under the title.
- **`group`** (`str`, default: `''` ) –

  The catalogue group the card is listed under.
- **`args`** (`Sequence[str]`, default: `()` ) –

  The arguments the card's command passes to the workbench command line.

### legacy

```python
legacy(module: str) -> WorkbenchSpec
```

The spec of a module following the old `main() -> Renderable` contract.

### discover

```python
discover(entry_points: Sequence[EntryPointLike] | None = None) -> list[WorkbenchSpec]
```

Every spec: the built-ins plus what each entry point in the group provides (a spec or a sequence), sorted by group and title; a name given two different specs is an error.

## femforge\_workbench.runner

Script runs on the server loop: each catalogue script as a subprocess whose lines stream as they arrive and whose process tree dies on stop.

Type Aliases:

- **`LineHandler`** –

  Receives the task name and one output line without its newline.
- **`TaskHandler`** –

  Receives the task after it started and again after it exited.

Classes:

- **`TaskStatus`** –

  Where a script run stands.
- **`TaskField`** –

  The keys of a task row in the client state.
- **`Task`** –

  One script run: its process, when it started and how it ended.
- **`ScriptRunner`** –

  Starts named commands, streaming their output lines and reporting each status change.

Attributes:

- **`LINE_LIMIT`** –

  Bytes one streamed line may hold before the reader gives up on it.
- **`STREAM_ENV`** –

  Makes the child flush every line as it is written, in UTF-8 whatever the console code page, laid out for a wide terminal.

### LINE\_LIMIT

```python
LINE_LIMIT = 1 << 20
```

Bytes one streamed line may hold before the reader gives up on it.

### STREAM\_ENV

```python
STREAM_ENV = {'PYTHONUNBUFFERED': '1', 'PYTHONIOENCODING': 'utf-8', 'COLUMNS': '200'}
```

Makes the child flush every line as it is written, in UTF-8 whatever the console code page, laid out for a wide terminal.

### LineHandler

```python
LineHandler = Callable[[str, str], None]
```

Receives the task name and one output line without its newline.

### TaskHandler

```python
TaskHandler = Callable[[Task], None]
```

Receives the task after it started and again after it exited.

### TaskStatus

Bases: `StrEnum`

Where a script run stands.

### TaskField

Bases: `StrEnum`

The keys of a task row in the client state.

### Task

```python
Task(name: str, process: Process, tree: ProcessTree, started: datetime = now(), status: TaskStatus = RUNNING, returncode: int | None = None, stopping: bool = False)
```

One script run: its process, when it started and how it ended.

Methods:

- **`row`** –

  The task as a row of the client state.

Attributes:

- **`pid`** (`int`) –

  The process id.

#### pid

```python
pid: int
```

The process id.

#### row

```python
row() -> dict[str, object]
```

The task as a row of the client state.

### ScriptRunner

```python
ScriptRunner(on_line: LineHandler, on_change: TaskHandler)
```

Starts named commands, streaming their output lines and reporting each status change.

Methods:

- **`running`** –

  Whether the named task runs.
- **`rows`** –

  Every task as a row of the client state.
- **`run`** –

  Runs the command to completion, streaming its lines; a name still running is an error.
- **`stop`** –

  Kills the task's process tree; its run then ends as stopped.

#### running

```python
running(name: str) -> bool
```

Whether the named task runs.

#### rows

```python
rows() -> list[dict[str, object]]
```

Every task as a row of the client state.

#### run

```python
run(name: str, command: Sequence[str], *, cwd: Path | None = None) -> Task
```

Runs the command to completion, streaming its lines; a name still running is an error.

Parameters:

- **`name`** (`str`) –

  The task name, unique among the running tasks.
- **`command`** (`Sequence[str]`) –

  The command and its arguments.
- **`cwd`** (`Path | None`, default: `None` ) –

  The directory the command runs in; None keeps the server's.

#### stop

```python
stop(name: str) -> None
```

Kills the task's process tree; its run then ends as stopped.

## femforge\_workbench.process\_tree

One child process and everything it starts, ended together: a job object on Windows, a process group elsewhere.

Classes:

- **`ProcessTree`** –

  One process and every process it starts: a job object on Windows, the process group the leader heads elsewhere.

Attributes:

- **`WINDOWS`** (`Final`) –

  Whether a tree is a job object (Windows) or a process group (elsewhere).
- **`CREATION_FLAGS`** (`Final`) –

  creationflags for Popen: CREATE\_SUSPENDED on Windows, so the child waits until it is in its job and nothing it starts can escape; 0 elsewhere.
- **`NEW_SESSION`** (`Final`) –

  start\_new\_session for Popen: on POSIX the child leads a process group of its own, whose id is its pid.
- **`KILL_GRACE`** (`Final`) –

  Seconds a terminated tree gets before what is left is killed.
- **`POLL`** (`Final`) –

  Seconds between looks at a tree that was asked to exit.

### WINDOWS

```python
WINDOWS: Final = sys.platform == 'win32'
```

Whether a tree is a job object (Windows) or a process group (elsewhere).

### CREATION\_FLAGS

```python
CREATION_FLAGS: Final = 4 if WINDOWS else 0
```

creationflags for Popen: CREATE\_SUSPENDED on Windows, so the child waits until it is in its job and nothing it starts can escape; 0 elsewhere.

### NEW\_SESSION

```python
NEW_SESSION: Final = not WINDOWS
```

start\_new\_session for Popen: on POSIX the child leads a process group of its own, whose id is its pid.

### KILL\_GRACE

```python
KILL_GRACE: Final = 2.0
```

Seconds a terminated tree gets before what is left is killed.

### POLL

```python
POLL: Final = 0.05
```

Seconds between looks at a tree that was asked to exit.

### ProcessTree

```python
ProcessTree(pid: int, job: int = 0)
```

One process and every process it starts: a job object on Windows, the process group the leader heads elsewhere.

Methods:

- **`adopt`** –

  Takes a process spawned with `CREATION_FLAGS` and `NEW_SESSION`: into a new job and let run on Windows, checked to lead its own group elsewhere.
- **`kill`** –

  Ends every process in the tree and returns how many there were; POSIX asks first and kills what is left after `grace` seconds, Windows has no gentle signal.
- **`close`** –

  Ends anything still in the tree and releases it: a job dies with its handle, a group gets SIGKILL.

Attributes:

- **`pid`** (`int`) –

  The process the tree was built around; on POSIX also the group id.
- **`job`** (`int`) –

  The Windows job handle; 0 on POSIX, where a group needs none.

#### pid

```python
pid: int
```

The process the tree was built around; on POSIX also the group id.

#### job

```python
job: int = 0
```

The Windows job handle; 0 on POSIX, where a group needs none.

#### adopt

```python
adopt(pid: int) -> ProcessTree
```

Takes a process spawned with `CREATION_FLAGS` and `NEW_SESSION`: into a new job and let run on Windows, checked to lead its own group elsewhere.

#### kill

```python
kill(grace: float = KILL_GRACE) -> int
```

Ends every process in the tree and returns how many there were; POSIX asks first and kills what is left after `grace` seconds, Windows has no gentle signal.

#### close

```python
close() -> None
```

Ends anything still in the tree and releases it: a job dies with its handle, a group gets SIGKILL.
