bricklogger 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bricklogger-0.2.3 → bricklogger-0.2.4}/PKG-INFO +2 -2
- {bricklogger-0.2.3 → bricklogger-0.2.4}/README.md +1 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/pyproject.toml +1 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/pyproject.toml.orig +1 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/__init__.py +1 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/model.py +24 -3
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/plugins.py +1 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/roles.py +12 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/api.py +12 -4
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/core.py +139 -10
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/instances.py +25 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/plugins.py +1 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/sparql.py +1 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/spool.py +17 -9
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/architecture.md +44 -6
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/docker.md +5 -5
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/api.md +7 -4
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/cli.md +16 -14
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/daemon.md +5 -4
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/destinations.md +77 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/mcp.md +5 -3
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/plugins.md +14 -3
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/web.md +4 -3
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/getting-started.md +1 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/mcp/http.py +5 -1
- bricklogger-0.2.4/src/bricklogger/model/timeseries.py +55 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/working_graph.py +40 -7
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/status.py +51 -13
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/timescaledb/declaration.py +3 -1
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/timescaledb/destination.py +56 -4
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/sdk/__init__.py +7 -3
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/sdk/contract.py +45 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/sdk/declaration.py +1 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/app.py +6 -3
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/instances.html +1 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/texts.py +2 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/LICENSE +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/__main__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/client.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/config.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/context.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/daemon_commands.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/guided.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/init.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/mcp_command.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/notify.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/output.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/query.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/serve.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/services.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/cli/update.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/daemon.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/examples.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/instances.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/issues.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/loader.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/schema.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/validation.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/config/values.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/codes.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/explorer.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/jobs.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/logging.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/metadata.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/notify.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/overlay.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/plan.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/rules.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/run.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/sink.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/state.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/daemon/updates.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/README.md +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/configuration.md +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/notifications.md +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/features/sources.md +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/docs/vision.md +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/mcp/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/mcp/instructions.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/mcp/server.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/Brick.ttl +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/LICENSE-Brick.txt +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/diff.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/inference.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/ontology.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/prefixes.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/shapes.ttl +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/upload.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/model/versions.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/catalogue.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/client.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/config.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/context.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/docs.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/environment.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/errors.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/plugin_volume.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/services.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/tools.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/tree.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/units.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/ops/updates.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/bacnet_ip/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/bacnet_ip/config.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/bacnet_ip/declaration.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/bacnet_ip/references.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/bacnet_ip/source.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/bacnet_ip/tools.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/bacnet_ip/values.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/timescaledb/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/plugins/timescaledb/config.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/py.typed +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/sdk/bacnet.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/sdk/registry.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/sdk/testing.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/units/bricklogger-mcp.service +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/units/bricklogger-web.service +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/units/bricklogger.service +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/__init__.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/client.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/forms.py +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/css/app.css +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/css/explorer.css +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/fonts/ibm-plex-mono-400.woff2 +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/fonts/ibm-plex-mono-500.woff2 +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/fonts/ibm-plex-mono-600.woff2 +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/fonts/ibm-plex-sans-var.woff2 +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/js/explorer.js +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/LICENSES.md +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/codemirror-yaml.min.js +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/codemirror.min.css +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/codemirror.min.js +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/cytoscape.min.js +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/htmx.min.js +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/yasgui.min.css +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/static/vendor/yasgui.min.js +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/base.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/config.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/daemon.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/explorer.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/instances.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/login.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/model.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/notifications.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/overview.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/config_result.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/daemon.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/diff.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/health.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/job.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/model_versions.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/notifications.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/notify_test.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/overview.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/plugins.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/points.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/statusline.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/tool_result.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/partials/unreachable.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/plugins.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/points.html +0 -0
- {bricklogger-0.2.3 → bricklogger-0.2.4}/src/bricklogger/web/templates/query.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bricklogger
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Data bridge for building automation: collects data based on a Brick model and writes it to time-series databases
|
|
5
5
|
Keywords: brick,bacnet,building-automation,timescaledb,data-logger
|
|
6
6
|
Author: Michael Rosenlund
|
|
@@ -84,7 +84,7 @@ uv tool install bricklogger
|
|
|
84
84
|
uv brings a Python that fits, so the machine needs nothing else. Everything
|
|
85
85
|
stays in that login's home directory: the program in uv's tool directory, the
|
|
86
86
|
command in `~/.local/bin`, the configuration in `~/.config/bricklogger` and the
|
|
87
|
-
data in `~/.local/share/bricklogger`. `uv tool install bricklogger==0.2.
|
|
87
|
+
data in `~/.local/share/bricklogger`. `uv tool install bricklogger==0.2.4`
|
|
88
88
|
installs a given version.
|
|
89
89
|
|
|
90
90
|
**As a container** the same program is `ghcr.io/cx1-aps/bricklogger`, with a
|
|
@@ -39,7 +39,7 @@ uv tool install bricklogger
|
|
|
39
39
|
uv brings a Python that fits, so the machine needs nothing else. Everything
|
|
40
40
|
stays in that login's home directory: the program in uv's tool directory, the
|
|
41
41
|
command in `~/.local/bin`, the configuration in `~/.config/bricklogger` and the
|
|
42
|
-
data in `~/.local/share/bricklogger`. `uv tool install bricklogger==0.2.
|
|
42
|
+
data in `~/.local/share/bricklogger`. `uv tool install bricklogger==0.2.4`
|
|
43
43
|
installs a given version.
|
|
44
44
|
|
|
45
45
|
**As a container** the same program is `ghcr.io/cx1-aps/bricklogger`, with a
|
|
@@ -299,11 +299,30 @@ def export(
|
|
|
299
299
|
values: Annotated[
|
|
300
300
|
bool, typer.Option("--values", help="Add the value overlay.")
|
|
301
301
|
] = False,
|
|
302
|
+
timeseries: Annotated[
|
|
303
|
+
bool,
|
|
304
|
+
typer.Option(
|
|
305
|
+
"--timeseries",
|
|
306
|
+
help="Add the time-series references of the destination that stores "
|
|
307
|
+
"the model.",
|
|
308
|
+
),
|
|
309
|
+
] = False,
|
|
310
|
+
destination: Annotated[
|
|
311
|
+
str | None,
|
|
312
|
+
typer.Option(
|
|
313
|
+
"--destination",
|
|
314
|
+
help="With --timeseries: the destination instance, when several "
|
|
315
|
+
"store the model.",
|
|
316
|
+
),
|
|
317
|
+
] = None,
|
|
302
318
|
output: Annotated[
|
|
303
319
|
Path | None, typer.Option("-o", "--output", help="Write to a file.")
|
|
304
320
|
] = None,
|
|
305
321
|
) -> None:
|
|
306
|
-
"""The model as uploaded; the inferred graph
|
|
322
|
+
"""The model as uploaded; the inferred graph, the values and the time-series
|
|
323
|
+
references need the daemon."""
|
|
324
|
+
if destination is not None and not timeseries:
|
|
325
|
+
raise fail("--destination goes with --timeseries")
|
|
307
326
|
context = cli_context(ctx)
|
|
308
327
|
client = reachable_client(context)
|
|
309
328
|
if client is not None:
|
|
@@ -316,11 +335,13 @@ def export(
|
|
|
316
335
|
f"/v1/models/{number}",
|
|
317
336
|
inferred="true" if inferred else None,
|
|
318
337
|
values="true" if values else None,
|
|
338
|
+
timeseries=(destination or "true") if timeseries else None,
|
|
319
339
|
)
|
|
320
340
|
else:
|
|
321
|
-
if inferred or values:
|
|
341
|
+
if inferred or values or timeseries:
|
|
322
342
|
raise fail(
|
|
323
|
-
"--inferred and --
|
|
343
|
+
"--inferred, --values and --timeseries need the running daemon's "
|
|
344
|
+
"working graph"
|
|
324
345
|
)
|
|
325
346
|
store = _store(ctx)
|
|
326
347
|
number = version if version is not None else store.active()
|
|
@@ -327,6 +327,7 @@ def _echo_declaration(data: dict[str, Any]) -> None:
|
|
|
327
327
|
echo_schema(tool.get("parameters"), indent=" ")
|
|
328
328
|
else:
|
|
329
329
|
typer.echo(f"stores metadata: {'yes' if data.get('stores_metadata') else 'no'}")
|
|
330
|
+
typer.echo(f"stores the model: {'yes' if data.get('stores_model') else 'no'}")
|
|
330
331
|
typer.echo("configuration:")
|
|
331
332
|
echo_schema(data.get("config_schema"), indent=" ")
|
|
332
333
|
|
|
@@ -335,6 +335,8 @@ def _row(role: str, name: str, type_name: str, state: str) -> dict[str, Any]:
|
|
|
335
335
|
"written": 0,
|
|
336
336
|
"last_write": None,
|
|
337
337
|
"stores_metadata": None,
|
|
338
|
+
"stores_model": None,
|
|
339
|
+
"model_version": None,
|
|
338
340
|
"spool": {
|
|
339
341
|
"observations": 0,
|
|
340
342
|
"bytes": 0,
|
|
@@ -438,7 +440,8 @@ def _echo_detail(role: str, row: Mapping[str, Any]) -> None:
|
|
|
438
440
|
age = spool.get("oldest_age_seconds")
|
|
439
441
|
typer.echo(
|
|
440
442
|
f"{row['name']}: last write {row.get('last_write') or 'never'}, "
|
|
441
|
-
f"stores metadata {'yes' if row.get('stores_metadata') else 'no'}"
|
|
443
|
+
f"stores metadata {'yes' if row.get('stores_metadata') else 'no'}, "
|
|
444
|
+
f"stores the model {_model_note(row)}"
|
|
442
445
|
)
|
|
443
446
|
typer.echo(
|
|
444
447
|
f" spool: {spool.get('observations', 0)} observations, "
|
|
@@ -683,3 +686,11 @@ def _file(role: str) -> str:
|
|
|
683
686
|
|
|
684
687
|
sources_app = _role_app("source", _SourcesGroup)
|
|
685
688
|
destinations_app = _role_app("destination", _DestinationsGroup)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _model_note(row: Mapping[str, Any]) -> str:
|
|
692
|
+
"""Whether a destination stores the model, and the version it last wrote."""
|
|
693
|
+
if not row.get("stores_model"):
|
|
694
|
+
return "no"
|
|
695
|
+
version = row.get("model_version")
|
|
696
|
+
return "yes" if version is None else f"yes (version {version})"
|
|
@@ -32,6 +32,7 @@ from bricklogger.daemon.core import (
|
|
|
32
32
|
from bricklogger.daemon.jobs import Job
|
|
33
33
|
from bricklogger.daemon.sparql import QueryError
|
|
34
34
|
from bricklogger.model import ModelNotFound, ModelUnreadable
|
|
35
|
+
from bricklogger.ops.status import live_answer
|
|
35
36
|
|
|
36
37
|
PROBLEM = "application/problem+json"
|
|
37
38
|
YAML = "application/yaml"
|
|
@@ -135,8 +136,8 @@ def create_app(
|
|
|
135
136
|
# --- health and daemon ---------------------------------------------------
|
|
136
137
|
|
|
137
138
|
@app.get("/health/live")
|
|
138
|
-
def live() -> dict[str,
|
|
139
|
-
return
|
|
139
|
+
def live() -> dict[str, Any]:
|
|
140
|
+
return live_answer(daemon.started_at)
|
|
140
141
|
|
|
141
142
|
@app.get("/health")
|
|
142
143
|
def health() -> JSONResponse:
|
|
@@ -483,12 +484,19 @@ def create_app(
|
|
|
483
484
|
|
|
484
485
|
@app.get("/v1/models/{version}")
|
|
485
486
|
def export_model(
|
|
486
|
-
version: int,
|
|
487
|
+
version: int,
|
|
488
|
+
inferred: bool = False,
|
|
489
|
+
values: bool = False,
|
|
490
|
+
timeseries: str | None = None,
|
|
487
491
|
) -> Response:
|
|
488
492
|
try:
|
|
489
|
-
data, fmt = daemon.export(
|
|
493
|
+
data, fmt = daemon.export(
|
|
494
|
+
version, inferred=inferred, values=values, timeseries=timeseries
|
|
495
|
+
)
|
|
490
496
|
except ModelNotFound as exc:
|
|
491
497
|
return problem(404, "Model version not found", str(exc))
|
|
498
|
+
except NotFound as exc:
|
|
499
|
+
return problem(404, "No time-series references", str(exc))
|
|
492
500
|
except ExportUnavailable as exc:
|
|
493
501
|
return problem(409, "Export unavailable", str(exc))
|
|
494
502
|
return Response(
|
|
@@ -61,6 +61,7 @@ from bricklogger.model import (
|
|
|
61
61
|
Inference,
|
|
62
62
|
ModelDiff,
|
|
63
63
|
ModelInvalid,
|
|
64
|
+
ModelNotFound,
|
|
64
65
|
ModelReport,
|
|
65
66
|
ModelStore,
|
|
66
67
|
ModelUnreadable,
|
|
@@ -78,8 +79,14 @@ from bricklogger.model.prefixes import (
|
|
|
78
79
|
declared_prefixes,
|
|
79
80
|
expand,
|
|
80
81
|
)
|
|
82
|
+
from bricklogger.model.timeseries import (
|
|
83
|
+
REF,
|
|
84
|
+
TIMESERIES_GRAPH_PREFIX,
|
|
85
|
+
reference_triples,
|
|
86
|
+
timeseries_graph,
|
|
87
|
+
)
|
|
81
88
|
from bricklogger.model.working_graph import GRAPH_DIR
|
|
82
|
-
from bricklogger.sdk.contract import AssignedPoint, Outcome
|
|
89
|
+
from bricklogger.sdk.contract import AssignedPoint, ModelDocument, Outcome
|
|
83
90
|
from bricklogger.sdk.registry import PluginRegistry
|
|
84
91
|
|
|
85
92
|
log = logging.getLogger(__name__)
|
|
@@ -225,6 +232,7 @@ class Daemon:
|
|
|
225
232
|
self.model_error: str | None = None
|
|
226
233
|
self.started_at: datetime | None = None
|
|
227
234
|
self._fallback_active: set[str] = set()
|
|
235
|
+
self._turtle: dict[int, str] = {}
|
|
228
236
|
self._lock = threading.RLock()
|
|
229
237
|
self.jobs = JobRunner()
|
|
230
238
|
|
|
@@ -375,6 +383,9 @@ class Daemon:
|
|
|
375
383
|
self.graph.replace_model(
|
|
376
384
|
inference.model, inference.ontology, inference.inferred
|
|
377
385
|
)
|
|
386
|
+
if number != self.active_version:
|
|
387
|
+
# another version's references until each destination writes this one
|
|
388
|
+
self._clear_timeseries_graphs()
|
|
378
389
|
self.model_store.set_active(number)
|
|
379
390
|
self.state.record_activation(number)
|
|
380
391
|
self.active_version = number
|
|
@@ -488,10 +499,8 @@ class Daemon:
|
|
|
488
499
|
|
|
489
500
|
def models(self) -> dict[str, Any]:
|
|
490
501
|
"""The stored versions, which one is active, and when each was activated."""
|
|
491
|
-
assert self.model_store is not None
|
|
492
|
-
history
|
|
493
|
-
for row in self.state.activations():
|
|
494
|
-
history.setdefault(int(row["version"]), []).append(str(row["activated_at"]))
|
|
502
|
+
assert self.model_store is not None
|
|
503
|
+
history = self._activation_history()
|
|
495
504
|
return {
|
|
496
505
|
"active": self.active_version,
|
|
497
506
|
"prefixes": dict(self.graph_access.prefixes) if self.graph_access else {},
|
|
@@ -512,20 +521,30 @@ class Daemon:
|
|
|
512
521
|
return run_query(self.graph, prefixes, query, accept)
|
|
513
522
|
|
|
514
523
|
def export(
|
|
515
|
-
self,
|
|
524
|
+
self,
|
|
525
|
+
version: int,
|
|
526
|
+
*,
|
|
527
|
+
inferred: bool = False,
|
|
528
|
+
values: bool = False,
|
|
529
|
+
timeseries: str | None = None,
|
|
516
530
|
) -> tuple[bytes, str]:
|
|
517
531
|
"""A version as uploaded, with its format name.
|
|
518
532
|
|
|
519
|
-
With the inferred graph
|
|
520
|
-
|
|
533
|
+
With the inferred graph, the values or a destination's time-series
|
|
534
|
+
references added the result is Turtle built from the working graph,
|
|
535
|
+
which holds them for the active version only. ``timeseries`` names the
|
|
536
|
+
destination instance, or is ``"true"`` for the one that stores the
|
|
537
|
+
model.
|
|
521
538
|
"""
|
|
522
539
|
assert self.model_store is not None and self.graph is not None
|
|
523
540
|
stored = self.model_store.get(version)
|
|
524
|
-
if not inferred and not values:
|
|
541
|
+
if not inferred and not values and timeseries is None:
|
|
525
542
|
return self.model_store.read(version), stored.format
|
|
543
|
+
instance = None if timeseries is None else self._reference_instance(timeseries)
|
|
526
544
|
if version != self.active_version:
|
|
527
545
|
raise ExportUnavailable(
|
|
528
|
-
"the inferred graph
|
|
546
|
+
"the inferred graph, the values and the time-series references "
|
|
547
|
+
"exist for the active version only"
|
|
529
548
|
)
|
|
530
549
|
graph = rdflib.Graph()
|
|
531
550
|
graph.parse(data=self.graph.dump(MODEL_GRAPH), format="nt")
|
|
@@ -533,6 +552,9 @@ class Daemon:
|
|
|
533
552
|
graph.parse(data=self.graph.dump(INFERRED_GRAPH), format="nt")
|
|
534
553
|
if values:
|
|
535
554
|
graph.parse(data=self.graph.dump(VALUES_GRAPH), format="nt")
|
|
555
|
+
if instance is not None:
|
|
556
|
+
graph.parse(data=self.graph.dump(timeseries_graph(instance)), format="nt")
|
|
557
|
+
graph.bind("ref", REF)
|
|
536
558
|
prefixes = self.graph_access.prefixes if self.graph_access else WELL_KNOWN
|
|
537
559
|
for prefix, iri in prefixes.items():
|
|
538
560
|
graph.bind(prefix, iri, override=True)
|
|
@@ -551,6 +573,7 @@ class Daemon:
|
|
|
551
573
|
last is None or last[0] != marker or self.graph.count(MODEL_GRAPH) == 0
|
|
552
574
|
)
|
|
553
575
|
if needs_build:
|
|
576
|
+
self._clear_timeseries_graphs()
|
|
554
577
|
try:
|
|
555
578
|
activate_version(
|
|
556
579
|
self.model_store,
|
|
@@ -571,6 +594,101 @@ class Daemon:
|
|
|
571
594
|
self.active_version = marker
|
|
572
595
|
self._refresh_graph_access()
|
|
573
596
|
|
|
597
|
+
# --- the model in destinations -------------------------------------------
|
|
598
|
+
|
|
599
|
+
def _reference_instance(self, timeseries: str) -> str:
|
|
600
|
+
"""The destination instance whose references an export adds: the one
|
|
601
|
+
named, or with ``"true"`` the only one that stores the model."""
|
|
602
|
+
storing = sorted(
|
|
603
|
+
name for name, runner in self.destinations.items() if runner.stores_model
|
|
604
|
+
)
|
|
605
|
+
if timeseries == "true":
|
|
606
|
+
if not storing:
|
|
607
|
+
raise NotFound("no destination stores the model")
|
|
608
|
+
if len(storing) > 1:
|
|
609
|
+
raise ExportUnavailable(
|
|
610
|
+
"several destinations store the model; name one: "
|
|
611
|
+
+ ", ".join(storing)
|
|
612
|
+
)
|
|
613
|
+
return storing[0]
|
|
614
|
+
if timeseries not in storing:
|
|
615
|
+
raise NotFound(f"no destination {timeseries!r} stores the model")
|
|
616
|
+
return timeseries
|
|
617
|
+
|
|
618
|
+
def _clear_timeseries_graphs(self, instance: str | None = None) -> None:
|
|
619
|
+
assert self.graph is not None
|
|
620
|
+
for iri in self.graph.named_graphs():
|
|
621
|
+
if iri.startswith(TIMESERIES_GRAPH_PREFIX) and (
|
|
622
|
+
instance is None or iri == timeseries_graph(instance)
|
|
623
|
+
):
|
|
624
|
+
self.graph.clear_graph(iri)
|
|
625
|
+
|
|
626
|
+
def _model_document(
|
|
627
|
+
self, version: int, activations: Sequence[str]
|
|
628
|
+
) -> ModelDocument:
|
|
629
|
+
"""A version as Turtle, without references; the runner adds them."""
|
|
630
|
+
assert self.model_store is not None
|
|
631
|
+
stored = self.model_store.get(version)
|
|
632
|
+
turtle = self._turtle.get(version)
|
|
633
|
+
if turtle is None:
|
|
634
|
+
model = parse_model(self.model_store.read(version), stored.format)
|
|
635
|
+
turtle = model.serialize(format="turtle")
|
|
636
|
+
self._turtle[version] = turtle
|
|
637
|
+
return ModelDocument(
|
|
638
|
+
version=version,
|
|
639
|
+
uploaded_at=stored.uploaded_at,
|
|
640
|
+
activations=tuple(datetime.fromisoformat(a) for a in activations),
|
|
641
|
+
turtle=turtle,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def _activation_history(self) -> dict[int, list[str]]:
|
|
645
|
+
assert self.state is not None
|
|
646
|
+
history: dict[int, list[str]] = {}
|
|
647
|
+
for row in self.state.activations():
|
|
648
|
+
history.setdefault(int(row["version"]), []).append(str(row["activated_at"]))
|
|
649
|
+
return history
|
|
650
|
+
|
|
651
|
+
def _offer_models(self, spools: Sequence[Spool], *, every: bool) -> None:
|
|
652
|
+
"""Spool the active version, or every version that has been active,
|
|
653
|
+
for the destinations that store the model."""
|
|
654
|
+
assert self.model_store is not None
|
|
655
|
+
if not spools:
|
|
656
|
+
return
|
|
657
|
+
history = self._activation_history()
|
|
658
|
+
if every:
|
|
659
|
+
versions = sorted(history)
|
|
660
|
+
elif self.active_version is not None:
|
|
661
|
+
versions = [self.active_version]
|
|
662
|
+
else:
|
|
663
|
+
versions = []
|
|
664
|
+
for version in versions:
|
|
665
|
+
try:
|
|
666
|
+
document = self._model_document(version, history.get(version, []))
|
|
667
|
+
except (ModelNotFound, ModelUnreadable):
|
|
668
|
+
log.warning("model version %d cannot be offered", version)
|
|
669
|
+
continue
|
|
670
|
+
for spool in spools:
|
|
671
|
+
spool.append_model(document)
|
|
672
|
+
|
|
673
|
+
def _model_spools(self) -> list[Spool]:
|
|
674
|
+
return [
|
|
675
|
+
self.spools[name]
|
|
676
|
+
for name, runner in self.destinations.items()
|
|
677
|
+
if runner.stores_model
|
|
678
|
+
]
|
|
679
|
+
|
|
680
|
+
def _on_model_written(
|
|
681
|
+
self, instance: str, version: int, ids: Mapping[str, str]
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Called from a destination's thread once it stored a version; the
|
|
684
|
+
active version's keys become the instance's graph of references."""
|
|
685
|
+
if version != self.active_version or self.graph is None:
|
|
686
|
+
return
|
|
687
|
+
try:
|
|
688
|
+
self.graph.replace_graph(timeseries_graph(instance), reference_triples(ids))
|
|
689
|
+
except Exception:
|
|
690
|
+
log.exception("the time-series references of %s were not stored", instance)
|
|
691
|
+
|
|
574
692
|
def _refresh_graph_access(self) -> None:
|
|
575
693
|
assert self.graph is not None and self.model_store is not None
|
|
576
694
|
prefixes = {**WELL_KNOWN, **self.registry.prefixes()}
|
|
@@ -615,8 +733,12 @@ class Daemon:
|
|
|
615
733
|
spool_max_age=instance.spool.max_age,
|
|
616
734
|
stop_timeout=self.configuration.daemon.stop_timeout,
|
|
617
735
|
stores_metadata=declaration.stores_metadata,
|
|
736
|
+
stores_model=declaration.stores_model,
|
|
737
|
+
on_model_written=self._on_model_written,
|
|
618
738
|
)
|
|
619
739
|
self.destinations[name] = runner
|
|
740
|
+
if declaration.stores_model:
|
|
741
|
+
self._offer_models([spool], every=True)
|
|
620
742
|
if self.state.stop_intent(name):
|
|
621
743
|
runner.current_state = "stopped"
|
|
622
744
|
self.state.set_instance(
|
|
@@ -693,6 +815,7 @@ class Daemon:
|
|
|
693
815
|
continue
|
|
694
816
|
self.destinations.pop(name).stop()
|
|
695
817
|
self.spools.pop(name).close()
|
|
818
|
+
self._clear_timeseries_graphs(name)
|
|
696
819
|
for name, instance in new.items():
|
|
697
820
|
if name in old and old[name] == instance:
|
|
698
821
|
continue
|
|
@@ -854,6 +977,8 @@ class Daemon:
|
|
|
854
977
|
if assigned and self.sink is not None:
|
|
855
978
|
entries = graph_metadata(self.graph, self.graph_access.prefixes, assigned)
|
|
856
979
|
self.sink.metadata(entries.values())
|
|
980
|
+
# behind the metadata, so the destinations hold the keys the model needs
|
|
981
|
+
self._offer_models(self._model_spools(), every=False)
|
|
857
982
|
for name, runner in self.sources.items():
|
|
858
983
|
runner.assign(plan.assignments.get(name, []))
|
|
859
984
|
|
|
@@ -1047,6 +1172,8 @@ class Daemon:
|
|
|
1047
1172
|
"type": runner.type_name,
|
|
1048
1173
|
"state": runner.current_state,
|
|
1049
1174
|
"stores_metadata": runner.stores_metadata,
|
|
1175
|
+
"stores_model": runner.stores_model,
|
|
1176
|
+
"model_version": runner.model_version,
|
|
1050
1177
|
"last_error": row.get("last_error"),
|
|
1051
1178
|
"last_write": runner.last_write,
|
|
1052
1179
|
"written": self.state.counter(f"written:{name}"),
|
|
@@ -1067,6 +1194,8 @@ class Daemon:
|
|
|
1067
1194
|
"type": entry.type_name,
|
|
1068
1195
|
"state": "failed",
|
|
1069
1196
|
"stores_metadata": None,
|
|
1197
|
+
"stores_model": None,
|
|
1198
|
+
"model_version": None,
|
|
1070
1199
|
"last_error": entry.error,
|
|
1071
1200
|
"last_write": None,
|
|
1072
1201
|
"written": 0,
|
|
@@ -7,11 +7,13 @@ from __future__ import annotations
|
|
|
7
7
|
import logging
|
|
8
8
|
import threading
|
|
9
9
|
import time
|
|
10
|
-
from collections.abc import Callable, Iterable, Sequence
|
|
10
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
11
|
+
from dataclasses import replace
|
|
11
12
|
from datetime import timedelta
|
|
12
13
|
|
|
13
14
|
from bricklogger.daemon.spool import Entry, Spool
|
|
14
15
|
from bricklogger.daemon.state import RuntimeState
|
|
16
|
+
from bricklogger.model.timeseries import with_references
|
|
15
17
|
from bricklogger.sdk.contract import (
|
|
16
18
|
AssignedPoint,
|
|
17
19
|
Destination,
|
|
@@ -256,6 +258,8 @@ class DestinationRunner:
|
|
|
256
258
|
spool_max_age: timedelta,
|
|
257
259
|
stop_timeout: timedelta,
|
|
258
260
|
stores_metadata: bool,
|
|
261
|
+
stores_model: bool = False,
|
|
262
|
+
on_model_written: Callable[[str, int, Mapping[str, str]], None] | None = None,
|
|
259
263
|
) -> None:
|
|
260
264
|
self.name = name
|
|
261
265
|
self.type_name = type_name
|
|
@@ -268,6 +272,9 @@ class DestinationRunner:
|
|
|
268
272
|
self.spool_max_age = spool_max_age
|
|
269
273
|
self.stop_timeout = stop_timeout
|
|
270
274
|
self.stores_metadata = stores_metadata
|
|
275
|
+
self.stores_model = stores_model
|
|
276
|
+
self.on_model_written = on_model_written
|
|
277
|
+
self.model_version: int | None = None
|
|
271
278
|
self._thread: threading.Thread | None = None
|
|
272
279
|
self._stopping = threading.Event()
|
|
273
280
|
self._failures = 0
|
|
@@ -375,6 +382,10 @@ class DestinationRunner:
|
|
|
375
382
|
if self.stores_metadata:
|
|
376
383
|
for entry in batch:
|
|
377
384
|
self.destination.write_metadata(entry.metadata())
|
|
385
|
+
elif batch[0].kind == "model":
|
|
386
|
+
if self.stores_model:
|
|
387
|
+
for entry in batch:
|
|
388
|
+
self._write_model(entry)
|
|
378
389
|
else:
|
|
379
390
|
observations = [o for entry in batch for o in entry.observations()]
|
|
380
391
|
self.destination.write(observations)
|
|
@@ -397,6 +408,19 @@ class DestinationRunner:
|
|
|
397
408
|
self.last_write = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
398
409
|
return True
|
|
399
410
|
|
|
411
|
+
def _write_model(self, entry: Entry) -> None:
|
|
412
|
+
"""Add the destination's references to a model version and store it.
|
|
413
|
+
|
|
414
|
+
The keys are asked for at write time, after the metadata ahead of the
|
|
415
|
+
model in the spool, so every point the plan offered already has one.
|
|
416
|
+
"""
|
|
417
|
+
model = entry.model()
|
|
418
|
+
turtle, used = with_references(model.turtle, self.destination.timeseries_ids())
|
|
419
|
+
self.destination.write_model(replace(model, turtle=turtle))
|
|
420
|
+
self.model_version = model.version
|
|
421
|
+
if self.on_model_written is not None:
|
|
422
|
+
self.on_model_written(self.name, model.version, used)
|
|
423
|
+
|
|
400
424
|
def _enforce_caps(self) -> None:
|
|
401
425
|
dropped = self.spool.enforce_caps(self.spool_max_size, self.spool_max_age)
|
|
402
426
|
if dropped:
|
|
@@ -80,7 +80,7 @@ def run_query(
|
|
|
80
80
|
if is_update(query):
|
|
81
81
|
raise QueryError("update requests are refused: the graph is read-only")
|
|
82
82
|
try:
|
|
83
|
-
result = graph.query(with_prefixes(query, prefixes))
|
|
83
|
+
result = graph.query(with_prefixes(query, prefixes), references=True)
|
|
84
84
|
except SyntaxError as exc:
|
|
85
85
|
raise QueryError(f"the query does not parse: {exc}") from exc
|
|
86
86
|
except (OSError, ValueError) as exc:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""The persistent spool between the daemon and one destination.
|
|
2
2
|
|
|
3
|
-
An on-disk queue that observations and
|
|
4
|
-
from in order, in batches; it survives a daemon restart and is
|
|
5
|
-
and age, dropping the oldest entries when a cap is reached. See
|
|
3
|
+
An on-disk queue that observations, metadata and model versions are appended
|
|
4
|
+
to and drained from in order, in batches; it survives a daemon restart and is
|
|
5
|
+
bounded by size and age, dropping the oldest entries when a cap is reached. See
|
|
6
6
|
``docs/architecture.md``, "Backpressure: the spool".
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -18,16 +18,17 @@ from datetime import timedelta
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any, Literal
|
|
20
20
|
|
|
21
|
-
from bricklogger.sdk.contract import Observation, PointMetadata
|
|
21
|
+
from bricklogger.sdk.contract import ModelDocument, Observation, PointMetadata
|
|
22
22
|
|
|
23
23
|
SPOOL_DIR = "spool"
|
|
24
24
|
|
|
25
|
-
EntryKind = Literal["observations", "metadata"]
|
|
25
|
+
EntryKind = Literal["observations", "metadata", "model"]
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@dataclass(frozen=True)
|
|
29
29
|
class Entry:
|
|
30
|
-
"""One spooled batch: observations or
|
|
30
|
+
"""One spooled batch: observations, metadata or one model version, as it
|
|
31
|
+
was appended."""
|
|
31
32
|
|
|
32
33
|
id: int
|
|
33
34
|
kind: EntryKind
|
|
@@ -39,6 +40,9 @@ class Entry:
|
|
|
39
40
|
def metadata(self) -> list[PointMetadata]:
|
|
40
41
|
return [PointMetadata.from_dict(item) for item in self.payload]
|
|
41
42
|
|
|
43
|
+
def model(self) -> ModelDocument:
|
|
44
|
+
return ModelDocument.from_dict(self.payload[0])
|
|
45
|
+
|
|
42
46
|
|
|
43
47
|
@dataclass(frozen=True)
|
|
44
48
|
class SpoolStats:
|
|
@@ -82,6 +86,9 @@ class Spool:
|
|
|
82
86
|
if entries:
|
|
83
87
|
self._append("metadata", [e.as_dict() for e in entries])
|
|
84
88
|
|
|
89
|
+
def append_model(self, model: ModelDocument) -> None:
|
|
90
|
+
self._append("model", [model.as_dict()])
|
|
91
|
+
|
|
85
92
|
def _append(self, kind: EntryKind, items: list[dict[str, Any]]) -> None:
|
|
86
93
|
payload = json.dumps(items, separators=(",", ":"))
|
|
87
94
|
with self._lock:
|
|
@@ -95,8 +102,9 @@ class Spool:
|
|
|
95
102
|
def next_entries(self, max_observations: int) -> list[Entry]:
|
|
96
103
|
"""The oldest entries, in order, up to about ``max_observations`` observations.
|
|
97
104
|
|
|
98
|
-
Metadata entries break a run, so a destination learns about
|
|
99
|
-
before it sees the point's values,
|
|
105
|
+
Metadata and model entries break a run, so a destination learns about
|
|
106
|
+
a point before it sees the point's values, and holds its key before
|
|
107
|
+
the model names it, in the order things were appended.
|
|
100
108
|
"""
|
|
101
109
|
entries: list[Entry] = []
|
|
102
110
|
collected = 0
|
|
@@ -110,7 +118,7 @@ class Spool:
|
|
|
110
118
|
break
|
|
111
119
|
entries.append(Entry(int(row["id"]), kind, json.loads(row["payload"])))
|
|
112
120
|
collected += int(row["items"])
|
|
113
|
-
if kind
|
|
121
|
+
if kind != "observations" or collected >= max_observations:
|
|
114
122
|
break
|
|
115
123
|
return entries
|
|
116
124
|
|