Writing a third-party extension
tiny_pacs aims for an ecosystem of third-party extensions. Extensions
are discovered through Python entry points; the core treats a third-party
package exactly like its own first-party extensions. This guide covers the
extension contract: the entry-point groups, the compatibility promise, and
the rules an extension must follow.
Compatibility promise
Within a core minor series (e.g. 0.3.x) the following never break:
the entry-point group names and value formats;
the extension-facing surface:
tiny_pacs.component.Component,tiny_pacs.component.ComponentConfig,tiny_pacs.config.register_component();everything documented in
tiny_pacs.events,tiny_pacs.schemaand the API reference;the CLI helpers
tiny_pacs.__main__.add_common_arguments()and the headless admin runtime.
Breaking changes land in a new minor series and are announced in the changelog together with a migration note. Extensions pin the core minor series they were built and tested against:
[tool.poetry.dependencies]
tiny_pacs = ">=0.3,<0.4"
Entry point groups
Two groups are defined. The entry point name is user-visible in both
cases: for components it is the name used in the components section of
YAML configs, for the CLI it is the subcommand name.
Group |
Entry point value |
Used for |
|---|---|---|
|
|
auto-register components |
|
|
add |
Any distribution installed into the same environment — first-party or third-party — participates; no core configuration, code change or “plugin list” is needed. Entry points are loaded lazily: an extension module is only imported when the core runs discovery.
Entry-point syntax
The entry-point table key depends on the package-metadata style of the
declaring pyproject.toml; a file must use the one matching its own
metadata table and never mix the two. This repository uses the legacy
Poetry style; PEP 621 projects declare the identical entry points under
[project.entry-points."group"] — the installed metadata is the same, so
discovery sees no difference.
Metadata style in |
Entry-point table |
|---|---|
Legacy Poetry |
|
PEP 621 |
|
Legacy Poetry example:
# pacs_audit/pyproject.toml
[tool.poetry.plugins."tiny_pacs.components"]
StoreAudit = "pacs_audit.components:StoreAudit"
[tool.poetry.plugins."tiny_pacs.cli"]
audit = "pacs_audit.cli:register"
The same declarations from a PEP 621 package read:
# pacs_audit/pyproject.toml using [project] metadata
[project.entry-points."tiny_pacs.components"]
StoreAudit = "pacs_audit.components:StoreAudit"
[project.entry-points."tiny_pacs.cli"]
audit = "pacs_audit.cli:register"
Component entry points
The value is the import path of a
Component subclass. The component is
registered under the entry point name on the first Config
construction, before any components section is validated — install the
package, add a matching YAML entry and it works:
components:
StoreAudit:
on: true
audit_log: /var/log/tiny_pacs/audit.log
Precedence rules:
an entry point named like a built-in (
Database,Devices,PACS, …) replaces that built-in — the documented override mechanism, logged at INFO;two distributions advertising the same name: the last-loaded wins, logged at WARNING. Pick unique names; prefix them with your project name when in doubt;
a programmatic
register_component()call always wins over installed plugins, regardless of when it runs (embedder use case).
CLI entry points
Each value is a callable receiving a guarded facade over argparse’s
subparsers action; it builds its own command tree and returns nothing.
Subcommands provide their execution function via
parser.set_defaults(command_handler=...); tiny-pacs dispatches to
it after parsing.
Use tiny_pacs.__main__.add_common_arguments() for the shared
-c/--config flags so third-party commands stay consistent with the
built-ins. Additional shared helpers keep subcommand output uniform:
tiny_pacs.__main__.add_action_parser() adds an action subparser with
the config flags and registers its handler,
format_table() renders plain-text tables and
fail() reports an error on stderr and exits with
status 1 (the shared error contract of every subcommand).
Registrations are isolated from the parser. Reserved subcommand names are
run, config (and argparse’s help); a plugin that tries to add
one — whether as its entry point name or inside register() — is logged
and the registration ignored. The same holds for a name another plugin (or
a built-in) already uses: the existing command wins and the duplicate is
logged with a WARNING. A broken register() is logged and reverted, so
it never leaves a half-registered subcommand behind. A plugin can never
break the tiny-pacs binary.
Discovery lifecycle
Per-process order:
First
Config()construction (CLI or embedder):tiny_pacs.componentsentry points are loaded once and registered.Configuration sources are read;
componentssections are validated against the registry — third-party components are visible here.The
tiny-pacsCLI builds its parser.tiny_pacs.clientry points are loaded and theirregister()called, except for invocations of the built-inrunandconfigcommands, which skip plugin discovery entirely and never import plugin modules.Serverinstantiates enabled components in configuration order and starts the event bus.
Installing an extension never changes server behaviour by itself: a
component stays disabled until its YAML entry sets on: true
(on defaults to False),
and CLI subcommands are additive. Installing a third-party plugin into a
running deployment is a no-op until the operator opts in.