Version 0.2.0-beta
This commit is contained in:
2 files changed
+192
-28
No files matched your search
+191
-27
@@ -43,6 +43,7 @@ again.
|
|||||||
| `host` | `str` | `"127.0.0.1"` | TCP bind address. |
|
| `host` | `str` | `"127.0.0.1"` | TCP bind address. |
|
||||||
| `port` | `int` | `58763` | TCP bind port; validated from 0 through 65535. |
|
| `port` | `int` | `58763` | TCP bind port; validated from 0 through 65535. |
|
||||||
| `thread_debug` | `bool` | `False` | Enables periodic per-thread CPU debug logging. |
|
| `thread_debug` | `bool` | `False` | Enables periodic per-thread CPU debug logging. |
|
||||||
|
| `derive_queue_capacity` | `int` | `16384` | Maximum queued requests in the derive router and each derive unit; minimum 1. |
|
||||||
| `playback_mode` | `"reprocess" \| "read"` | `"reprocess"` | Whether playback recalculates bound derived signals or reads their recorded values. Offline `reprocess()` requires `"read"`. |
|
| `playback_mode` | `"reprocess" \| "read"` | `"reprocess"` | Whether playback recalculates bound derived signals or reads their recorded values. Offline `reprocess()` requires `"read"`. |
|
||||||
|
|
||||||
`CoreConfig.bind_str() -> str` returns `"host:port"`.
|
`CoreConfig.bind_str() -> str` returns `"host:port"`.
|
||||||
@@ -68,8 +69,12 @@ Blocks until `stop()` is called or the optional timeout elapses.
|
|||||||
### `stop() -> bool`
|
### `stop() -> bool`
|
||||||
|
|
||||||
Stops the server, connector registry, derive registry, input worker, and
|
Stops the server, connector registry, derive registry, input worker, and
|
||||||
playback worker. Returns `True` only when all required components stop within
|
playback worker. It also waits up to five seconds for an active data operation;
|
||||||
their time limits; it returns `False` if shutdown is incomplete.
|
an operation still running makes shutdown incomplete. A processing operation
|
||||||
|
that finishes after stopping begins cannot commit a new processing buffer.
|
||||||
|
Returns `True` only when all required components stop within their time limits;
|
||||||
|
it returns `False` if shutdown is incomplete. Calling it on a constructed Core
|
||||||
|
before `start()`, or calling it again after shutdown, is supported.
|
||||||
|
|
||||||
## Modes and buffers
|
## Modes and buffers
|
||||||
|
|
||||||
@@ -89,24 +94,34 @@ values are routed into Core.
|
|||||||
### `set_mode(mode: CoreMode) -> None` / `get_mode() -> CoreMode`
|
### `set_mode(mode: CoreMode) -> None` / `get_mode() -> CoreMode`
|
||||||
|
|
||||||
Sets or returns the current mode. `CoreMode` is a type contract; callers must
|
Sets or returns the current mode. `CoreMode` is a type contract; callers must
|
||||||
pass one of the three values above. Changing mode does not automatically pause
|
pass one of the three values above. Leaving playback mode automatically pauses
|
||||||
playback, so pause it before leaving playback mode.
|
it at its current position. Returning to playback mode does not automatically
|
||||||
|
resume it; call `play()` when ready.
|
||||||
|
|
||||||
### `load_playback_engine(input: Path | None = None) -> None`
|
### `load_playback_engine(input: Path | None = None, *, background: bool = False) -> UUID | None`
|
||||||
|
|
||||||
Available only in `"playback"` mode (`CoreError(code="invalid_mode")`). Pass a
|
Available only in `"playback"` mode (`CoreError(code="invalid_mode")`). Pass a
|
||||||
`.dlpak` path, or omit `input` to load the current processing buffer. With no
|
`.dlpak` path, or omit `input` to load the current processing buffer. With no
|
||||||
processing buffer, omitting it raises `CoreError(code="no_playback_data")`.
|
processing buffer, omitting it raises `CoreError(code="no_playback_data")`.
|
||||||
Loading does not start playback; use `play()`.
|
Loading takes a deep copy of the archive, resets the playback position to zero,
|
||||||
|
and clears cached live values. It does not start playback; use `play()`.
|
||||||
|
|
||||||
### `load_processing_buffer(input: Path | None = None) -> None`
|
### `load_processing_buffer(input: Path | None = None, *, background: bool = False) -> UUID | None`
|
||||||
|
|
||||||
Available only in `"offline"` mode (`CoreError(code="invalid_mode")`). Pass a
|
Available only in `"offline"` mode (`CoreError(code="invalid_mode")`). Pass a
|
||||||
`.dlpak` path to load it, or omit `input` to turn the completed recording into
|
`.dlpak` path to load it, or omit `input` to turn the completed recording into
|
||||||
a processing buffer and create its manifest. Omitting `input` before a
|
a processing buffer and create its manifest. Omitting `input` before a
|
||||||
recording is available raises `CoreError(code="no_playback_data")`.
|
recording is available raises `CoreError(code="no_playback_data")`.
|
||||||
|
|
||||||
### `reprocess() -> None`
|
Completed recordings, processing buffers, and loaded playback archives have
|
||||||
|
independent sample containers, indexes, locks, and manifest metadata. Loading
|
||||||
|
a processing buffer deep-copies the completed recording. Loading playback
|
||||||
|
deep-copies its input archive. Reprocessing, editing metadata, or beginning a
|
||||||
|
new recording therefore does not mutate earlier snapshots. `DLPak.set_data()`
|
||||||
|
also takes an independent data snapshot; `copy()` and `deepcopy()` of a DLPak
|
||||||
|
copy both its data and its nested manifest metadata.
|
||||||
|
|
||||||
|
### `reprocess(*, background: bool = False) -> UUID | None`
|
||||||
|
|
||||||
Recalculates derived signals in the current processing buffer using the bound
|
Recalculates derived signals in the current processing buffer using the bound
|
||||||
derive units. Call it in `"offline"` mode after `load_processing_buffer()`, with
|
derive units. Call it in `"offline"` mode after `load_processing_buffer()`, with
|
||||||
@@ -116,20 +131,21 @@ in sample order using per-signal binary searches instead of full-buffer scans.
|
|||||||
For other inputs it selects the nearest recorded value, preferring the earlier
|
For other inputs it selects the nearest recorded value, preferring the earlier
|
||||||
sample when distances tie. This retains the previous offline alignment policy,
|
sample when distances tie. This retains the previous offline alignment policy,
|
||||||
including selection of future samples. The operation
|
including selection of future samples. The operation
|
||||||
removes previously recorded derived samples and descriptors, then merges the
|
works on a deep copy, removes its previously recorded derived samples and
|
||||||
new outputs into the processing buffer. Calling it again replaces those
|
descriptors, and merges the new outputs. It replaces the processing buffer
|
||||||
|
only after all units finish successfully. Calling it again replaces those
|
||||||
outputs. It does not write an archive or start playback; use `write()` to save
|
outputs. It does not write an archive or start playback; use `write()` to save
|
||||||
the result, or switch to `"playback"` mode, call `load_playback_engine()`, and
|
the result, or switch to `"playback"` mode, call `load_playback_engine()`, and
|
||||||
then `play()`.
|
then `play()`.
|
||||||
|
|
||||||
The mode, configuration, and buffer checks raise `CoreError` with codes
|
The mode, configuration, and buffer checks raise `CoreError` with codes
|
||||||
`"invalid_mode"`, `"invalid_config"`, and `"empty_buffer"`, respectively.
|
`"invalid_mode"`, `"invalid_config"`, and `"empty_buffer"`, respectively.
|
||||||
Unresolved derive dependencies raise `DeriveError(code="unresolved_dependencies")`;
|
Missing inputs raise `DeriveError(code="missing_inputs")`, cycles raise
|
||||||
a unit that produces no values raises `DeriveError(code="no_values")`. Errors
|
`DeriveError(code="dependency_cycle")`, and a unit that produces no values raises
|
||||||
from a derive function propagate to the caller. Reprocessing changes the
|
`DeriveError(code="no_values")`. Errors from a derive function propagate to the
|
||||||
in-memory processing buffer before all units finish, so a failed call may
|
synchronous caller or are stored in the background operation status. Failure
|
||||||
leave its old derived data removed. Reload an unchanged archive before retrying
|
leaves the original processing buffer, including its prior derived results
|
||||||
if that data is needed.
|
and metadata, unchanged.
|
||||||
|
|
||||||
## Signals and live values
|
## Signals and live values
|
||||||
|
|
||||||
@@ -167,16 +183,30 @@ guide for details.
|
|||||||
|
|
||||||
### `start_recording() -> None` / `stop_recording() -> None`
|
### `start_recording() -> None` / `stop_recording() -> None`
|
||||||
|
|
||||||
Recording is available only in `"realtime"` mode. Starting clears the prior
|
Recording is available only in `"realtime"` mode. Start and stop insert markers
|
||||||
in-memory buffer; stopping normalizes and sorts samples and creates an
|
into Core's input queue and wait for the input worker to acknowledge them.
|
||||||
offload buffer with a manifest. Errors use `"incorrect_mode"`,
|
Samples queued before the start marker are excluded; samples between the start
|
||||||
`"recording_active"`, or `"not_recording"` as appropriate.
|
and stop markers are included. This is a boundary at Core's input queue:
|
||||||
|
packets still buffered in an endpoint and derive results not yet queued when
|
||||||
|
the stop marker is inserted are outside the recording. There is no fixed sleep
|
||||||
|
or guess about when queued samples have finished.
|
||||||
|
|
||||||
|
Starting creates a new in-memory buffer. Stopping normalizes and sorts the
|
||||||
|
completed samples and creates an isolated offload archive. Signal descriptors
|
||||||
|
are retained throughout recording, including descriptors attached to incoming
|
||||||
|
values. A connector can join after recording starts and disconnect before its
|
||||||
|
queued samples are ingested without losing their metadata.
|
||||||
|
|
||||||
|
Errors use `"incorrect_mode"`, `"recording_active"`, or `"not_recording"` as
|
||||||
|
appropriate. A failed input worker raises `CoreError(code="worker_failed")`;
|
||||||
|
a full queue that cannot accept a control marker within five seconds raises
|
||||||
|
`CoreError(code="input_overload")`.
|
||||||
|
|
||||||
### `get_recording_state() -> bool`
|
### `get_recording_state() -> bool`
|
||||||
|
|
||||||
Returns whether a recording is active.
|
Returns whether a recording is active.
|
||||||
|
|
||||||
### `write(dir: str | Path, filename: str, *, source: BufferSource = "processing") -> None`
|
### `write(dir: str | Path, filename: str, *, source: BufferSource = "processing", background: bool = False) -> UUID | None`
|
||||||
|
|
||||||
Writes the selected buffer as a `.dlpak` archive; the extension is appended
|
Writes the selected buffer as a `.dlpak` archive; the extension is appended
|
||||||
when absent. `BufferSource` is `"recording" | "processing" | "playback"`, defined
|
when absent. `BufferSource` is `"recording" | "processing" | "playback"`, defined
|
||||||
@@ -288,18 +318,40 @@ selected samples, rather than scanning unrelated signals.
|
|||||||
|
|
||||||
Start or resume, and pause, a loaded playback engine. Both require
|
Start or resume, and pause, a loaded playback engine. Both require
|
||||||
`"playback"` mode (`CoreError(code="invalid_mode")`). `play()` before loading
|
`"playback"` mode (`CoreError(code="invalid_mode")`). `play()` before loading
|
||||||
data raises `PlaybackError(code="no_data")`. Playback returns to timestamp zero
|
data raises `PlaybackError(code="no_data")`. Pause preserves the current
|
||||||
after it reaches the end.
|
position and resume continues with the next pending sample. Controls interrupt
|
||||||
|
waits across long sample gaps; a full input queue does not block pause or stop.
|
||||||
|
Playback returns to timestamp zero after its last playable sample, and a later
|
||||||
|
`play()` restarts it. An empty archive completes without emitting samples.
|
||||||
|
|
||||||
|
Playback timing starts at the archive's first stored timestamp, even if it is
|
||||||
|
nonzero. The loaded archive itself keeps its original timestamps for range
|
||||||
|
queries and saving. In reprocess mode, recorded derived values are skipped and
|
||||||
|
bound derive units calculate their replacements.
|
||||||
|
|
||||||
### `seek(timestamp_ns: int) -> None`
|
### `seek(timestamp_ns: int) -> None`
|
||||||
|
|
||||||
Sets the playback position relative to the recording's first sample. Requires
|
Sets the playback position relative to the recording's first sample, in
|
||||||
`"playback"` mode; a negative timestamp raises
|
nanoseconds. Works while playing or paused and preserves that state. The next
|
||||||
`PlaybackError(code="invalid_timestamp")`.
|
emitted sample is at or after the requested position, including all samples
|
||||||
|
exactly at that timestamp. Positions past the recording's end are clamped to
|
||||||
|
its duration. Seeking clears cached live values; fresh values arrive when
|
||||||
|
playback runs. Queued samples and derive results from before the seek are
|
||||||
|
discarded rather than entering the new timeline.
|
||||||
|
|
||||||
|
Requires `"playback"` mode; a negative timestamp raises
|
||||||
|
`PlaybackError(code="invalid_timestamp")`, and seeking before loading an archive
|
||||||
|
raises `PlaybackError(code="no_data")`.
|
||||||
|
|
||||||
### `get_playback_state() -> bool`
|
### `get_playback_state() -> bool`
|
||||||
|
|
||||||
Returns `True` while the playback engine is emitting data.
|
Returns `True` while playback is running, including waits between samples.
|
||||||
|
|
||||||
|
### `get_playback_position() -> int`
|
||||||
|
|
||||||
|
Returns the current playback position in nanoseconds relative to the archive's
|
||||||
|
first sample. The position advances while playing, remains fixed while paused,
|
||||||
|
and resets to zero when playback finishes or a new archive is loaded.
|
||||||
|
|
||||||
## Derived signals
|
## Derived signals
|
||||||
|
|
||||||
@@ -314,10 +366,122 @@ Derive source operates on internal `ValueDescriptor` and `SignalDescriptor`
|
|||||||
dataclasses, not their `Json*` equivalents. Source is executed with `exec()`;
|
dataclasses, not their `Json*` equivalents. Source is executed with `exec()`;
|
||||||
only load units from trusted sources.
|
only load units from trusted sources.
|
||||||
|
|
||||||
|
### `validate_derive_units() -> None`
|
||||||
|
|
||||||
|
Checks the configured graph against signals available in the current mode.
|
||||||
|
Missing inputs (including archive signals with no samples when reprocessing)
|
||||||
|
raise `DeriveError(code="missing_inputs")`; dependency cycles raise
|
||||||
|
`"dependency_cycle"`; duplicate outputs or outputs colliding with sources raise
|
||||||
|
`"duplicate_output"`. Outputs must have `origin="derived"`, otherwise validation
|
||||||
|
raises `"invalid_origin"`. A derive function must return a `ValueDescriptor`
|
||||||
|
whose `signal_id` is its configured output, or it raises `"invalid_result"`.
|
||||||
|
|
||||||
|
Structural validation runs before a unit is registered, so cycles and duplicate
|
||||||
|
outputs cannot begin realtime execution. Units can be bound before their sources
|
||||||
|
or upstream units connect; missing inputs are checked before execution. Realtime
|
||||||
|
routing revalidates when connectors, bound units, mode, or loaded data change.
|
||||||
|
An incomplete graph blocks derive routing until its dependencies become
|
||||||
|
available, and reports the reason in the routing worker's status. It does not
|
||||||
|
stop raw acquisition or recording. Offline reprocessing validates the entire
|
||||||
|
graph and sample availability before calling any derive function.
|
||||||
|
|
||||||
|
Connector signal IDs must also be unique across connectors and derive outputs;
|
||||||
|
registration rejects collisions with `ConnectorError(code="duplicate_signal")`.
|
||||||
|
Connector descriptors must have `origin="source"`.
|
||||||
|
|
||||||
|
### Saving DLUnit archives
|
||||||
|
|
||||||
|
`DLUnit.set_data(source: str)` accepts the Python source text.
|
||||||
|
`DLUnit.write(dir, filename)` writes `manifest.json` and the UTF-8 source as
|
||||||
|
`unit.py` inside a ZIP archive, appending `.dlunit` when needed.
|
||||||
|
`DLUnit().read(path)` restores both the manifest and source, ready to pass to
|
||||||
|
`bind_derive_unit()`. Saving without source or a manifest raises `DLUnitError`
|
||||||
|
with code `"no_data"` or `"no_manifest"`, respectively. Writes use a temporary
|
||||||
|
file followed by atomic replacement, so a failed write preserves an existing
|
||||||
|
unit archive.
|
||||||
|
|
||||||
### `unbind_derive_unit(unit_id: UUID) -> None`
|
### `unbind_derive_unit(unit_id: UUID) -> None`
|
||||||
|
|
||||||
Stops and removes a bound derive unit. An unknown ID has no effect.
|
Stops and removes a bound derive unit. An unknown ID has no effect.
|
||||||
|
|
||||||
|
## Background operations and progress
|
||||||
|
|
||||||
|
`load_processing_buffer()`, `load_playback_engine()`, `write()`, and `reprocess()`
|
||||||
|
remain synchronous by default: they return `None` on success and raise errors
|
||||||
|
on failure. Pass `background=True` to return an operation UUID immediately.
|
||||||
|
Only one data operation can run at a time. Data operations are rejected while
|
||||||
|
recording; mode changes, recording controls, derive binding/unbinding, and
|
||||||
|
playback controls are rejected while a data operation is active, with
|
||||||
|
`CoreError(code="operation_active")`. Queries and status polling remain available.
|
||||||
|
|
||||||
|
### `get_operation(operation_id: UUID | None = None) -> OperationStatus | None`
|
||||||
|
|
||||||
|
Returns an immutable snapshot from `dynalab_core.operations`. With no ID, it
|
||||||
|
returns the most recently started operation, or `None` before any operation.
|
||||||
|
Fields are `id`, `name`, `state` (`"running"`, `"completed"`, or `"failed"`),
|
||||||
|
`completed`, `total`, `message`, `error_code`, and `error_message`. The `progress`
|
||||||
|
property is the fraction `completed / total`. Load/save progress reports coarse
|
||||||
|
archive stages; reprocessing reports completed derive units, rather than a
|
||||||
|
predicted remaining time. Completion is determined by `state`, not the progress
|
||||||
|
fraction alone. The latest 100 operations are retained.
|
||||||
|
|
||||||
|
### `wait_operation(operation_id: UUID, timeout: float | None = None) -> OperationStatus`
|
||||||
|
|
||||||
|
Waits for completion or the timeout. On timeout, returns the current running
|
||||||
|
status. On failure, raises the original exception; the failed status remains
|
||||||
|
available through `get_operation()`. An unknown or expired ID raises
|
||||||
|
`CoreError(code="unknown_operation")`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
operation_id = core.reprocess(background=True)
|
||||||
|
status = core.get_operation(operation_id) # Poll from the GUI timer.
|
||||||
|
if status.state == "failed":
|
||||||
|
show_error(status.error_message)
|
||||||
|
# Outside the GUI event loop, wait and propagate any error:
|
||||||
|
core.wait_operation(operation_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connector, derive, and worker status
|
||||||
|
|
||||||
|
### `get_state() -> str`
|
||||||
|
|
||||||
|
Returns Core's lifecycle state: `"initd"`, `"started"`, `"stopping"`, `"stopped"`,
|
||||||
|
or `"stop_incomplete"` after construction.
|
||||||
|
|
||||||
|
### `get_connectors() -> list[ConnectorStatus]`
|
||||||
|
|
||||||
|
Returns immutable snapshots of registered endpoints: `id`, `name`, `version`,
|
||||||
|
`signal_ids`, and `state` (`"connected"`, `"stopping"`, or `"failed"`). Endpoints
|
||||||
|
are removed after disconnect cleanup; a GUI can compare snapshots to detect
|
||||||
|
connection and disconnection.
|
||||||
|
|
||||||
|
### `get_derive_units() -> list[DeriveUnitStatus]`
|
||||||
|
|
||||||
|
Returns each bound unit's `id`, `input_signal_ids`, `output_signal_id`, and
|
||||||
|
`worker` status. Failed units retain their error until unbound; unbind and rebind
|
||||||
|
a repaired unit to start a new worker.
|
||||||
|
|
||||||
|
### `get_worker_statuses() -> list[WorkerStatus]`
|
||||||
|
|
||||||
|
Returns the input, JSON server, playback, derive router, and individual derive
|
||||||
|
workers. Each status contains `name`, `state` (`"running"`, `"stopped"`,
|
||||||
|
`"failed"`, or `"blocked"`), `error`, `queue_size`, `queue_capacity`, and
|
||||||
|
`dropped_samples`. A blocked routing worker reports missing dependencies.
|
||||||
|
A server not started yet is stopped. Worker status describes worker health;
|
||||||
|
use `get_playback_state()` to distinguish playing from paused.
|
||||||
|
The status dataclasses are defined in `dynalab_core.types`.
|
||||||
|
|
||||||
|
### Derive overload policy
|
||||||
|
|
||||||
|
`CoreConfig.derive_queue_capacity` bounds both the routing queue and every
|
||||||
|
unit's input queue. Full derive queues drop the newest request, increment their
|
||||||
|
`dropped_samples` counter, and log a warning on the first drop. They do not block
|
||||||
|
raw ingestion or discard raw samples already accepted into Core's recording
|
||||||
|
buffer. Unit output waits for space in Core's bounded input queue and remains
|
||||||
|
interruptible during shutdown. Core's input queue has its own existing limit;
|
||||||
|
connector input overload disconnects the affected endpoint and is logged.
|
||||||
|
Offline processing does not use these derive queues and does not drop samples.
|
||||||
|
|
||||||
## Exceptions
|
## Exceptions
|
||||||
|
|
||||||
Expected library errors inherit from `DynaLabError` in `dynalab_core.errors`
|
Expected library errors inherit from `DynaLabError` in `dynalab_core.errors`
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "dynalab-core"
|
name = "dynalab-core"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "The core data processing component of the dynalab project"
|
description = "The core data processing component of the dynalab project"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
Reference in new issue
Block a user