devman-gen generates managed client/server bridge packages for Python hardware-control libraries.
- Client wrapper module with the same Python function signatures as the original library.
- Manager server that validates resource ownership before calling the real backend library.
- SQLite-backed ownership tracking (
acquire/release). - Client/server session handshake with duplicate-name protection.
- Generator CLI that can build from live module introspection or a JSON spec.
- Split standalone output: separate installable client and server packages.
python -m venv .venv
source .venv/bin/activate
pip install -e .
# Create a spec from an installed module
devman-gen introspect --module some_library --output spec.json
# Generate split bridge packages
devman-gen generate --spec spec.json --output ./generated_bridge --package-name my-bridge
# output:
# generated_bridge/my-bridge-client
# generated_bridge/my-bridge-serverInstall client machine:
pip install ./generated_bridge/my-bridge-clientInstall server machine:
pip install ./generated_bridge/my-bridge-serverUse generated client package:
import os
from my_bridge_client.client import configure, connect, disconnect, acquire, release
os.environ["DEVMAN_CLIENT"] = "alice" # required
connect()
acquire("channel:0")
configure(0, voltage=100.0)
release("channel:0")
disconnect()Start manager from server package:
python -m my_bridge_server.server --backend-module some_library --host 127.0.0.1 --port 50250 --db ./ownership.db
# or script entrypoint (same args):
# my_bridge_server --backend-module some_library ...- Client name is required (
DEVMAN_CLIENTorconfigure_connection(..., client_name=...)). - Each client must
connect()before use. Runtime auto-connects on first call, but explicitconnect()is recommended. - Duplicate names are rejected by default.
- Use
connect(force=True)to take over an existing same-name session.- This evicts only the old session state for that name.
disconnect()removes only the active session.- Resource ownership is not auto-cleaned on
disconnect()or forced reconnect.
You can run startup/shutdown logic in either of two ways:
- Dotted backend callables:
--init-function backend.path.to_init--deinit-function backend.path.to_deinit
- Python hook files:
--init-file /path/hooks.py --init-file-function init--deinit-file /path/hooks.py --deinit-file-function deinit
- Singleton provider callables:
--singleton-function backend.path.to_get_singleton--singleton-file /path/hooks.py --singleton-file-function get_singleton
Server accepts arbitrary extra CLI arguments (parse_known_args) and passes them to hooks as extra_args.
You can also pass structured key-value pairs with repeatable --hook-arg key=value, available in hooks as hook_args.
If no explicit singleton provider is configured, the return value of init is used as the singleton object (when non-None).
Example:
python -m my_bridge_server.server \
--backend-module some_library \
--init-file ./hooks.py \
--deinit-file ./hooks.py \
--hook-arg device_ip=192.168.1.100 \
--hook-arg username=admin \
--hook-arg password=secret \
--crate-port 1234In hook functions, consume:
hook_argsforkey=valuepairsextra_argsfor arbitrary pass-through CLI tokens
Equivalent environment variables:
DEVMAN_INIT_FUNCTION,DEVMAN_DEINIT_FUNCTIONDEVMAN_INIT_FILE,DEVMAN_DEINIT_FILEDEVMAN_INIT_FILE_FUNCTION,DEVMAN_DEINIT_FILE_FUNCTIONDEVMAN_SINGLETON_FUNCTIONDEVMAN_SINGLETON_FILEDEVMAN_SINGLETON_FILE_FUNCTION
--init-file and --init-function are mutually exclusive (same for deinit).
--singleton-file and --singleton-function are mutually exclusive.
devman-gen introspect emits JSON with:
-
module: backend import path -
functions[]:namesignature(Python signature text)param_order(ordered parameter names)param_kindsresource_template(optional, default uses first argument)dispatch(optional override)dispatch_target(optional target name for non-default dispatchers)
-
default_dispatch(spec-level default applied to all functions unless overridden by functiondispatch)
resource_template uses Python str.format variables from call arguments.
Example: "channel:{channel}".
Dispatch notes:
- Use
default_dispatchfor the common case. - Set function
dispatchonly where behavior differs fromdefault_dispatch. - For singleton dispatch, function names are not rewritten implicitly; set
dispatch_targetwhen needed.
List expansion is supported with []:
- Template:
"slot:{slot}:ch:{channel_list[]}" - Args:
slot=2,channel_list=[3, 7] - Resources:
["slot:2:ch:3", "slot:2:ch:7"]