bricklogger 0.2.0__tar.gz → 0.2.2__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.
Files changed (165) hide show
  1. {bricklogger-0.2.0 → bricklogger-0.2.2}/PKG-INFO +12 -13
  2. {bricklogger-0.2.0 → bricklogger-0.2.2}/README.md +11 -12
  3. {bricklogger-0.2.0 → bricklogger-0.2.2}/pyproject.toml +1 -1
  4. {bricklogger-0.2.0 → bricklogger-0.2.2}/pyproject.toml.orig +1 -1
  5. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/__init__.py +1 -1
  6. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/__init__.py +2 -0
  7. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/daemon_commands.py +4 -0
  8. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/plugins.py +2 -2
  9. bricklogger-0.2.2/src/bricklogger/cli/update.py +205 -0
  10. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/__init__.py +2 -0
  11. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/schema.py +14 -0
  12. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/validation.py +55 -1
  13. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/core.py +14 -9
  14. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/notify.py +4 -0
  15. bricklogger-0.2.2/src/bricklogger/daemon/updates.py +99 -0
  16. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/architecture.md +7 -3
  17. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/docker.md +21 -12
  18. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/cli.md +66 -0
  19. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/configuration.md +7 -0
  20. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/daemon.md +7 -5
  21. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/mcp.md +2 -2
  22. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/notifications.md +1 -1
  23. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/plugins.md +43 -15
  24. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/web.md +15 -5
  25. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/getting-started.md +15 -11
  26. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/environment.py +3 -3
  27. bricklogger-0.2.2/src/bricklogger/ops/units.py +152 -0
  28. bricklogger-0.2.2/src/bricklogger/ops/updates.py +663 -0
  29. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/sdk/registry.py +9 -0
  30. bricklogger-0.2.2/src/bricklogger/units/bricklogger-mcp.service +13 -0
  31. bricklogger-0.2.2/src/bricklogger/units/bricklogger-web.service +13 -0
  32. bricklogger-0.2.2/src/bricklogger/units/bricklogger.service +14 -0
  33. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/app.py +82 -0
  34. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/plugins.html +8 -0
  35. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/texts.py +20 -0
  36. {bricklogger-0.2.0 → bricklogger-0.2.2}/LICENSE +0 -0
  37. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/__main__.py +0 -0
  38. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/client.py +0 -0
  39. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/config.py +0 -0
  40. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/context.py +0 -0
  41. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/guided.py +0 -0
  42. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/init.py +0 -0
  43. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/mcp_command.py +0 -0
  44. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/model.py +0 -0
  45. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/notify.py +0 -0
  46. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/output.py +0 -0
  47. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/query.py +0 -0
  48. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/roles.py +0 -0
  49. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/cli/serve.py +0 -0
  50. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/daemon.py +0 -0
  51. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/examples.py +0 -0
  52. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/instances.py +0 -0
  53. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/issues.py +0 -0
  54. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/loader.py +0 -0
  55. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/config/values.py +0 -0
  56. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/__init__.py +0 -0
  57. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/api.py +0 -0
  58. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/codes.py +0 -0
  59. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/explorer.py +0 -0
  60. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/instances.py +0 -0
  61. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/jobs.py +0 -0
  62. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/logging.py +0 -0
  63. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/metadata.py +0 -0
  64. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/overlay.py +0 -0
  65. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/plan.py +0 -0
  66. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/plugins.py +0 -0
  67. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/rules.py +0 -0
  68. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/run.py +0 -0
  69. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/sink.py +0 -0
  70. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/sparql.py +0 -0
  71. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/spool.py +0 -0
  72. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/daemon/state.py +0 -0
  73. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/README.md +0 -0
  74. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/api.md +0 -0
  75. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/destinations.md +0 -0
  76. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/features/sources.md +0 -0
  77. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/docs/vision.md +0 -0
  78. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/mcp/__init__.py +0 -0
  79. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/mcp/http.py +0 -0
  80. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/mcp/instructions.py +0 -0
  81. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/mcp/server.py +0 -0
  82. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/Brick.ttl +0 -0
  83. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/LICENSE-Brick.txt +0 -0
  84. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/__init__.py +0 -0
  85. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/diff.py +0 -0
  86. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/inference.py +0 -0
  87. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/ontology.py +0 -0
  88. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/prefixes.py +0 -0
  89. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/shapes.ttl +0 -0
  90. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/upload.py +0 -0
  91. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/versions.py +0 -0
  92. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/model/working_graph.py +0 -0
  93. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/__init__.py +0 -0
  94. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/catalogue.py +0 -0
  95. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/client.py +0 -0
  96. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/config.py +0 -0
  97. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/context.py +0 -0
  98. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/docs.py +0 -0
  99. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/errors.py +0 -0
  100. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/plugin_volume.py +0 -0
  101. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/status.py +0 -0
  102. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/tools.py +0 -0
  103. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/ops/tree.py +0 -0
  104. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/__init__.py +0 -0
  105. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/bacnet_ip/__init__.py +0 -0
  106. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/bacnet_ip/config.py +0 -0
  107. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/bacnet_ip/declaration.py +0 -0
  108. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/bacnet_ip/references.py +0 -0
  109. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/bacnet_ip/source.py +0 -0
  110. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/bacnet_ip/tools.py +0 -0
  111. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/bacnet_ip/values.py +0 -0
  112. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/timescaledb/__init__.py +0 -0
  113. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/timescaledb/config.py +0 -0
  114. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/timescaledb/declaration.py +0 -0
  115. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/plugins/timescaledb/destination.py +0 -0
  116. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/py.typed +0 -0
  117. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/sdk/__init__.py +0 -0
  118. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/sdk/bacnet.py +0 -0
  119. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/sdk/contract.py +0 -0
  120. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/sdk/declaration.py +0 -0
  121. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/sdk/testing.py +0 -0
  122. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/__init__.py +0 -0
  123. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/client.py +0 -0
  124. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/forms.py +0 -0
  125. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/css/app.css +0 -0
  126. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/css/explorer.css +0 -0
  127. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/fonts/ibm-plex-mono-400.woff2 +0 -0
  128. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/fonts/ibm-plex-mono-500.woff2 +0 -0
  129. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/fonts/ibm-plex-mono-600.woff2 +0 -0
  130. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/fonts/ibm-plex-sans-var.woff2 +0 -0
  131. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/js/explorer.js +0 -0
  132. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/LICENSES.md +0 -0
  133. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/codemirror-yaml.min.js +0 -0
  134. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/codemirror.min.css +0 -0
  135. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/codemirror.min.js +0 -0
  136. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/cytoscape.min.js +0 -0
  137. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/htmx.min.js +0 -0
  138. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/yasgui.min.css +0 -0
  139. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/static/vendor/yasgui.min.js +0 -0
  140. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/base.html +0 -0
  141. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/config.html +0 -0
  142. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/daemon.html +0 -0
  143. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/explorer.html +0 -0
  144. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/instances.html +0 -0
  145. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/login.html +0 -0
  146. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/model.html +0 -0
  147. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/notifications.html +0 -0
  148. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/overview.html +0 -0
  149. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/config_result.html +0 -0
  150. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/daemon.html +0 -0
  151. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/diff.html +0 -0
  152. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/health.html +0 -0
  153. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/instances.html +0 -0
  154. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/job.html +0 -0
  155. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/model_versions.html +0 -0
  156. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/notifications.html +0 -0
  157. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/notify_test.html +0 -0
  158. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/overview.html +0 -0
  159. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/points.html +0 -0
  160. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/statusline.html +0 -0
  161. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/tool_result.html +0 -0
  162. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/partials/unreachable.html +0 -0
  163. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/plugins.html +0 -0
  164. {bricklogger-0.2.0 → bricklogger-0.2.2}/src/bricklogger/web/templates/points.html +0 -0
  165. {bricklogger-0.2.0 → bricklogger-0.2.2}/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.0
3
+ Version: 0.2.2
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
@@ -82,8 +82,8 @@ The script brings its own Python, so the machine needs only `curl`. With
82
82
  `sudo` it installs a service: the program under `/opt/bricklogger`, the
83
83
  configuration in `/etc/bricklogger`, the data in `/var/lib/bricklogger` and
84
84
  three systemd units, left stopped. Without `sudo` everything stays in your home
85
- directory. `--version 0.2.0` installs a given version; options come after
86
- `--` when the script is piped: `… | sudo sh -s -- --version 0.2.0`.
85
+ directory. `--version 0.2.2` installs a given version; options come after
86
+ `--` when the script is piped: `… | sudo sh -s -- --version 0.2.2`.
87
87
 
88
88
  **As a container** the same program is `ghcr.io/cx1-aps/bricklogger`, with a
89
89
  compose file for the daemon, the web interface and the MCP server; see
@@ -149,8 +149,8 @@ sudo systemctl restart bricklogger # the daemon reads plugins at start
149
149
  ```
150
150
 
151
151
  A plugin is a Python package that provides a source or a destination type. A
152
- plugin that cannot load stops nothing: its instances show as failed and the
153
- rest keep running. Writing one is described under
152
+ plugin that cannot load, or is configured but not installed, stops nothing:
153
+ its instances show as failed and the rest keep running. Writing one is described under
154
154
  [plugins](https://cx1-aps.github.io/bricklogger/features/plugins/).
155
155
 
156
156
  ## Mail notifications
@@ -167,17 +167,16 @@ See [notifications](https://cx1-aps.github.io/bricklogger/features/notifications
167
167
 
168
168
  ## Upgrade
169
169
 
170
- Run the install script again; it leaves the plugins, the configuration and the
171
- data where they are. Then restart the daemon. A container pulls the new image
172
- instead:
173
-
174
170
  ```bash
175
- curl -fsSL https://github.com/CX1-ApS/bricklogger/releases/latest/download/install.sh | sudo sh
176
- sudo systemctl restart bricklogger
171
+ bricklogger update status # what there is to upgrade
172
+ sudo bricklogger update all # Bricklogger and the plugins together
177
173
  ```
178
174
 
179
- After an upgrade to a new minor version, `bricklogger plugins` shows whether
180
- every plugin still loads.
175
+ `update` checks the configuration with the new version before it restarts
176
+ anything, and puts the previous versions back if it does not hold; the
177
+ configuration and the data stay where they are. `update core` upgrades
178
+ Bricklogger alone, `update <type>` one plugin. Running the install script
179
+ again works too. A container pulls the new image instead.
181
180
 
182
181
  **Upgrading from 0.1 with an iBOS source:** iBOS is no longer built in. The
183
182
  `ibos` instance shows as failed until the plugin is added with
@@ -37,8 +37,8 @@ The script brings its own Python, so the machine needs only `curl`. With
37
37
  `sudo` it installs a service: the program under `/opt/bricklogger`, the
38
38
  configuration in `/etc/bricklogger`, the data in `/var/lib/bricklogger` and
39
39
  three systemd units, left stopped. Without `sudo` everything stays in your home
40
- directory. `--version 0.2.0` installs a given version; options come after
41
- `--` when the script is piped: `… | sudo sh -s -- --version 0.2.0`.
40
+ directory. `--version 0.2.2` installs a given version; options come after
41
+ `--` when the script is piped: `… | sudo sh -s -- --version 0.2.2`.
42
42
 
43
43
  **As a container** the same program is `ghcr.io/cx1-aps/bricklogger`, with a
44
44
  compose file for the daemon, the web interface and the MCP server; see
@@ -104,8 +104,8 @@ sudo systemctl restart bricklogger # the daemon reads plugins at start
104
104
  ```
105
105
 
106
106
  A plugin is a Python package that provides a source or a destination type. A
107
- plugin that cannot load stops nothing: its instances show as failed and the
108
- rest keep running. Writing one is described under
107
+ plugin that cannot load, or is configured but not installed, stops nothing:
108
+ its instances show as failed and the rest keep running. Writing one is described under
109
109
  [plugins](https://cx1-aps.github.io/bricklogger/features/plugins/).
110
110
 
111
111
  ## Mail notifications
@@ -122,17 +122,16 @@ See [notifications](https://cx1-aps.github.io/bricklogger/features/notifications
122
122
 
123
123
  ## Upgrade
124
124
 
125
- Run the install script again; it leaves the plugins, the configuration and the
126
- data where they are. Then restart the daemon. A container pulls the new image
127
- instead:
128
-
129
125
  ```bash
130
- curl -fsSL https://github.com/CX1-ApS/bricklogger/releases/latest/download/install.sh | sudo sh
131
- sudo systemctl restart bricklogger
126
+ bricklogger update status # what there is to upgrade
127
+ sudo bricklogger update all # Bricklogger and the plugins together
132
128
  ```
133
129
 
134
- After an upgrade to a new minor version, `bricklogger plugins` shows whether
135
- every plugin still loads.
130
+ `update` checks the configuration with the new version before it restarts
131
+ anything, and puts the previous versions back if it does not hold; the
132
+ configuration and the data stay where they are. `update core` upgrades
133
+ Bricklogger alone, `update <type>` one plugin. Running the install script
134
+ again works too. A container pulls the new image instead.
136
135
 
137
136
  **Upgrading from 0.1 with an iBOS source:** iBOS is no longer built in. The
138
137
  `ibos` instance shows as failed until the plugin is added with
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bricklogger"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Data bridge for building automation: collects data based on a Brick model and writes it to time-series databases"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bricklogger"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Data bridge for building automation: collects data based on a Brick model and writes it to time-series databases"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -14,4 +14,4 @@ The package is laid out after the architecture in ``docs/architecture.md``:
14
14
  external plugins.
15
15
  """
16
16
 
17
- __version__ = "0.2.0"
17
+ __version__ = "0.2.2"
@@ -24,6 +24,7 @@ from bricklogger.cli.plugins import plugins_app
24
24
  from bricklogger.cli.query import query_command
25
25
  from bricklogger.cli.roles import destinations_app, sources_app
26
26
  from bricklogger.cli.serve import serve_command
27
+ from bricklogger.cli.update import update_app
27
28
  from bricklogger.config import resolve_config_dir
28
29
 
29
30
  app = typer.Typer(
@@ -96,6 +97,7 @@ app.add_typer(destinations_app, name="destinations")
96
97
  app.add_typer(rules_app, name="rules")
97
98
  app.add_typer(notify_app, name="notify")
98
99
  app.add_typer(plugins_app, name="plugins")
100
+ app.add_typer(update_app, name="update")
99
101
  app.add_typer(model_app, name="model")
100
102
  app.command("query")(query_command)
101
103
  app.command("serve")(serve_command)
@@ -28,6 +28,7 @@ from bricklogger.ops.status import PID_FILE as PID_FILE
28
28
  from bricklogger.ops.status import probe
29
29
  from bricklogger.ops.status import process_alive as process_alive
30
30
  from bricklogger.ops.status import read_pid as read_pid
31
+ from bricklogger.ops.updates import available_line
31
32
 
32
33
  daemon_app = typer.Typer(
33
34
  no_args_is_help=True,
@@ -363,6 +364,9 @@ def _echo_summary(data: dict[str, Any]) -> None:
363
364
  ]
364
365
  for row in data.get("instances", []):
365
366
  lines.append(f" {row['role']} {row['name']} ({row['type']}): {row['state']}")
367
+ newer = available_line(data.get("updates"))
368
+ if newer is not None:
369
+ lines.append(newer)
366
370
  for line in lines:
367
371
  typer.echo(line)
368
372
 
@@ -129,8 +129,8 @@ def remove_command(
129
129
  ) -> None:
130
130
  """Uninstall the distribution that provides a plugin type.
131
131
 
132
- Refuses while an instance of the type is configured, unless --force; the
133
- daemon would reject such a configuration at its next start.
132
+ Refuses while an instance of the type is configured, unless --force; from
133
+ the daemon's next start such an instance would be failed.
134
134
  """
135
135
  context = cli_context(ctx)
136
136
  try:
@@ -0,0 +1,205 @@
1
+ """``bricklogger update``: what there is to upgrade, and the upgrade itself.
2
+
3
+ ``update status`` installs nothing. ``update core``, ``update all`` and
4
+ ``update TYPE`` upgrade from PyPI with the environment's own uv, validate with
5
+ the new version, put the previous versions back when that fails, write the
6
+ units again and restart what runs as a unit. ``update`` alone shows this help,
7
+ as every group does. See ``docs/features/cli.md``, "update".
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Annotated, Any
13
+
14
+ import typer
15
+ from rich.table import Table
16
+ from typer.core import TyperGroup
17
+ from typer.main import get_command as typer_command
18
+
19
+ from bricklogger.cli.context import cli_context
20
+ from bricklogger.cli.output import fail, print_json, stderr, stdout
21
+ from bricklogger.ops.errors import OperationError
22
+ from bricklogger.ops.updates import CORE, Updated, UpdateRefused, status, update
23
+
24
+ NoRestart = Annotated[
25
+ bool,
26
+ typer.Option(
27
+ "--no-restart",
28
+ help="Leave the daemon, the web interface and the MCP server alone.",
29
+ ),
30
+ ]
31
+
32
+
33
+ class _UpdateGroup(TyperGroup):
34
+ """A group whose unknown subcommand is a plugin type to upgrade."""
35
+
36
+ # Typer carries its own copy of click, so the types are left open here.
37
+ def get_command(self, ctx: Any, cmd_name: str) -> Any:
38
+ command = super().get_command(ctx, cmd_name)
39
+ if command is not None or cmd_name.startswith("-"):
40
+ return command
41
+ return _type_command(cmd_name)
42
+
43
+
44
+ update_app = typer.Typer(
45
+ cls=_UpdateGroup,
46
+ add_completion=False,
47
+ no_args_is_help=True,
48
+ help="Upgrade Bricklogger and its plugins from PyPI. `update status` shows what "
49
+ "there is; `update core` upgrades Bricklogger alone, as far as the plugins "
50
+ "allow; `update TYPE` upgrades the plugin that provides the type; `update all` "
51
+ "upgrades everything together. The new version validates the configuration "
52
+ "before anything is restarted, and the previous versions are put back when it "
53
+ "does not hold.",
54
+ )
55
+
56
+
57
+ @update_app.command("status")
58
+ def status_command(
59
+ json_output: Annotated[
60
+ bool, typer.Option("--json", help="Emit JSON instead of a table.")
61
+ ] = False,
62
+ ) -> None:
63
+ """For Bricklogger and every plugin: installed, newest, and newest that fits."""
64
+ try:
65
+ rows = status()
66
+ except OperationError as exc:
67
+ raise fail(exc.message) from exc
68
+ if json_output:
69
+ print_json([row.as_dict() for row in rows])
70
+ return
71
+ table = Table(box=None)
72
+ for column in ("Package", "Installed", "Newest", "Fits", "Note"):
73
+ table.add_column(column)
74
+ for row in rows:
75
+ table.add_row(
76
+ row.name,
77
+ row.installed,
78
+ row.newest or "?",
79
+ row.fits or "?",
80
+ _note(row.name, row.error, row.newest, row.fits, row.held_by),
81
+ )
82
+ stdout.print(table)
83
+
84
+
85
+ def _note(
86
+ name: str,
87
+ error: str | None,
88
+ newest: str | None,
89
+ fits: str | None,
90
+ held_by: tuple[str, ...],
91
+ ) -> str:
92
+ if error is not None:
93
+ return f"not looked up: {error}"
94
+ if newest is not None and fits is not None and newest != fits and held_by:
95
+ return (
96
+ f"{newest} held back by {', '.join(held_by)}; "
97
+ "`update all` moves them together"
98
+ )
99
+ return ""
100
+
101
+
102
+ @update_app.command("core")
103
+ def core_command(ctx: typer.Context, no_restart: NoRestart = False) -> None:
104
+ """Upgrade Bricklogger alone, to the newest release the plugins allow."""
105
+ _update(ctx, "core", no_restart)
106
+
107
+
108
+ @update_app.command("all")
109
+ def all_command(ctx: typer.Context, no_restart: NoRestart = False) -> None:
110
+ """Upgrade Bricklogger and every plugin to the newest releases that fit together."""
111
+ _update(ctx, "all", no_restart)
112
+
113
+
114
+ def _type_command(type_name: str) -> Any:
115
+ """``update TYPE``, made for the type that was typed."""
116
+ sub = typer.Typer(add_completion=False)
117
+
118
+ @sub.command(type_name)
119
+ def command(ctx: typer.Context, no_restart: NoRestart = False) -> None:
120
+ """Upgrade the plugin that provides this type, the rest held."""
121
+ _update(ctx, type_name, no_restart)
122
+
123
+ return typer_command(sub)
124
+
125
+
126
+ def _update(ctx: typer.Context, target: str, no_restart: bool) -> None:
127
+ context = cli_context(ctx)
128
+ try:
129
+ result = update(target, config_dir=context.config_dir, restart=not no_restart)
130
+ except UpdateRefused as exc:
131
+ for error in exc.errors:
132
+ where = ".".join(
133
+ str(part)
134
+ for part in (
135
+ f"{error.get('file')}.yaml",
136
+ error.get("subject"),
137
+ error.get("key"),
138
+ )
139
+ if part and part != "None.yaml"
140
+ )
141
+ stderr.print(f" {where}: {error.get('message')}", highlight=False)
142
+ back = "; the previous versions are back in place" if exc.restored else ""
143
+ raise fail(f"{exc.message}{back}") from exc
144
+ except OperationError as exc:
145
+ raise fail(exc.message) from exc
146
+ _report(result, target, no_restart)
147
+
148
+
149
+ def _report(result: Updated, target: str, no_restart: bool) -> None:
150
+ if not result.moved:
151
+ typer.echo("already up to date")
152
+ if target == "core":
153
+ _held_back()
154
+ return
155
+ typer.echo(
156
+ "updated "
157
+ + ", ".join(
158
+ _movement(name, old, new) for name, (old, new) in result.moved.items()
159
+ )
160
+ )
161
+ for warning in result.warnings:
162
+ subject = warning.get("subject") or warning.get("file")
163
+ typer.echo(f"warning: {subject}: {warning.get('message')}")
164
+ if result.units:
165
+ typer.echo(f"rewrote {', '.join(result.units)} from the new templates")
166
+ if result.restarted:
167
+ typer.echo(f"restarted {', '.join(result.restarted)}")
168
+ for unit in result.not_answering:
169
+ typer.echo(
170
+ f"{unit} was restarted but does not answer yet; check its log with "
171
+ f"journalctl -u {unit}"
172
+ )
173
+ for note in result.notes:
174
+ typer.echo(note)
175
+ if result.container:
176
+ typer.echo("Restart the containers to run it: docker compose restart")
177
+ if target == "all":
178
+ typer.echo("Bricklogger itself comes with a new image.")
179
+ elif no_restart:
180
+ typer.echo("nothing was restarted (--no-restart)")
181
+ if target == "core":
182
+ _held_back()
183
+
184
+
185
+ def _movement(name: str, old: str | None, new: str | None) -> str:
186
+ if old is None:
187
+ return f"{name} {new} (new)"
188
+ if new is None:
189
+ return f"{name} {old} (removed)"
190
+ return f"{name} {old} → {new}"
191
+
192
+
193
+ def _held_back() -> None:
194
+ """Name a newer Bricklogger the installed plugins exclude."""
195
+ try:
196
+ rows = status()
197
+ except OperationError:
198
+ return
199
+ core = next((row for row in rows if row.name == CORE), None)
200
+ if core is None or not core.held_by or core.newest is None:
201
+ return
202
+ typer.echo(
203
+ f"bricklogger {core.newest} is out, but {', '.join(core.held_by)} does not "
204
+ "allow it yet; `bricklogger update all` moves them together"
205
+ )
@@ -51,6 +51,7 @@ from bricklogger.config.schema import (
51
51
  from bricklogger.config.validation import (
52
52
  RESERVED_INSTANCE_NAMES,
53
53
  ValidationResult,
54
+ not_installed,
54
55
  validate_configuration,
55
56
  )
56
57
 
@@ -81,6 +82,7 @@ __all__ = [
81
82
  "examples_for",
82
83
  "instances_in",
83
84
  "load_configuration",
85
+ "not_installed",
84
86
  "parse_text",
85
87
  "read_env_file",
86
88
  "read_texts",
@@ -185,6 +185,16 @@ class NotificationSettings(_Strict):
185
185
  )
186
186
 
187
187
 
188
+ class UpdateSettings(_Strict):
189
+ """``updates`` in ``daemon.yaml``. See ``docs/features/cli.md``, "update"."""
190
+
191
+ check: bool = Field(
192
+ default=True,
193
+ description="Whether the daemon looks on PyPI once a day for newer "
194
+ "releases of Bricklogger and its plugins; off on a network with no way out",
195
+ )
196
+
197
+
188
198
  class DaemonSettings(_Strict):
189
199
  """``daemon.yaml``."""
190
200
 
@@ -216,6 +226,10 @@ class DaemonSettings(_Strict):
216
226
  default_factory=NotificationSettings,
217
227
  description="Mail to the administrator, off by default",
218
228
  )
229
+ updates: UpdateSettings = Field(
230
+ default_factory=UpdateSettings,
231
+ description="The daily look for newer releases",
232
+ )
219
233
 
220
234
  @property
221
235
  def log_file(self) -> Path:
@@ -65,6 +65,51 @@ def _unloadable(file: str, name: str, error: str) -> ConfigIssue:
65
65
  )
66
66
 
67
67
 
68
+ def not_installed(type_name: str) -> str:
69
+ """What is said of a configured type that no installed plugin provides,
70
+ by validation and by the daemon that shows its instance failed."""
71
+ return (
72
+ f"the type {type_name!r} is not installed; add its plugin with "
73
+ "`bricklogger plugins add`"
74
+ )
75
+
76
+
77
+ def _not_installed(file: str, name: str, type_name: str) -> ConfigIssue:
78
+ """A warning, not an error: the instance will be failed while everything
79
+ else runs, as for a plugin that cannot load. See
80
+ ``docs/features/plugins.md``, "A type that is not installed"."""
81
+ return ConfigIssue(
82
+ file, f"the instance will be failed: {not_installed(type_name)}", name, "type"
83
+ )
84
+
85
+
86
+ def _changed_instances(
87
+ config_dir: Path,
88
+ config: Configuration,
89
+ env: Mapping[str, str] | None,
90
+ texts: Mapping[str, str | None] | None,
91
+ ) -> set[tuple[str, str]]:
92
+ """``(file, name)`` for every instance a proposed change adds or changes,
93
+ compared with the files on disk; nothing when no change is proposed.
94
+ When the files on disk do not load, every instance counts as changed."""
95
+ if not texts:
96
+ return set()
97
+ current = load_configuration(config_dir, env).configuration
98
+ changed: set[tuple[str, str]] = set()
99
+ for file, new, old in (
100
+ ("sources", config.sources, current.sources if current else {}),
101
+ (
102
+ "destinations",
103
+ config.destinations,
104
+ current.destinations if current else {},
105
+ ),
106
+ ):
107
+ for name, instance in new.items():
108
+ if old.get(name) != instance:
109
+ changed.add((file, name))
110
+ return changed
111
+
112
+
68
113
  def _reserved(file: str, name: str) -> ConfigIssue:
69
114
  return ConfigIssue(
70
115
  file,
@@ -81,7 +126,9 @@ def validate_configuration(
81
126
  ) -> ValidationResult:
82
127
  """Validate the whole directory; the configuration comes back only when valid.
83
128
 
84
- ``texts`` replaces files as they are on disk, for a proposed change.
129
+ ``texts`` replaces files as they are on disk, for a proposed change. A
130
+ configured type that is not installed is a warning, except in an
131
+ instance the proposed change adds or changes, where it is an error.
85
132
  """
86
133
  loaded = load_configuration(config_dir, env, texts)
87
134
  if loaded.configuration is None:
@@ -89,6 +136,7 @@ def validate_configuration(
89
136
  config = loaded.configuration
90
137
  errors: list[ConfigIssue] = []
91
138
  warnings: list[ConfigIssue] = []
139
+ changed = _changed_instances(config_dir, config, env, texts)
92
140
 
93
141
  daemon = config.daemon
94
142
  if not is_loopback(daemon.api.host) and not daemon.api.token:
@@ -126,6 +174,9 @@ def validate_configuration(
126
174
  if failure is not None:
127
175
  warnings.append(_unloadable("sources", name, failure.error))
128
176
  continue
177
+ if ("sources", name) not in changed:
178
+ warnings.append(_not_installed("sources", name, source.type))
179
+ continue
129
180
  errors.append(
130
181
  ConfigIssue(
131
182
  "sources",
@@ -150,6 +201,9 @@ def validate_configuration(
150
201
  if failure is not None:
151
202
  warnings.append(_unloadable("destinations", name, failure.error))
152
203
  continue
204
+ if ("destinations", name) not in changed:
205
+ warnings.append(_not_installed("destinations", name, destination.type))
206
+ continue
153
207
  errors.append(
154
208
  ConfigIssue(
155
209
  "destinations",
@@ -29,6 +29,7 @@ from bricklogger.config import (
29
29
  DestinationInstance,
30
30
  SourceInstance,
31
31
  ValidationResult,
32
+ not_installed,
32
33
  read_texts,
33
34
  validate_configuration,
34
35
  write_text,
@@ -51,6 +52,7 @@ from bricklogger.daemon.sink import DaemonSink
51
52
  from bricklogger.daemon.sparql import run_query
52
53
  from bricklogger.daemon.spool import SPOOL_DIR, Spool
53
54
  from bricklogger.daemon.state import STATE_FILE, RuntimeState
55
+ from bricklogger.daemon.updates import Lookup, UpdateWatch
54
56
  from bricklogger.model import (
55
57
  INFERRED_GRAPH,
56
58
  MODEL_GRAPH,
@@ -197,6 +199,7 @@ class Daemon:
197
199
  config_dir: Path,
198
200
  registry: PluginRegistry | None = None,
199
201
  env: Mapping[str, str] | None = None,
202
+ update_lookup: Lookup | None = None,
200
203
  ) -> None:
201
204
  self.config_dir = config_dir
202
205
  if registry is None:
@@ -215,6 +218,8 @@ class Daemon:
215
218
  self.unloadable: dict[str, Unloadable] = {}
216
219
  self.spools: dict[str, Spool] = {}
217
220
  self.notifier: Notifier | None = None
221
+ self.updates: UpdateWatch | None = None
222
+ self._update_lookup = update_lookup
218
223
  self.plan: Plan | None = None
219
224
  self.active_version: int | None = None
220
225
  self.model_error: str | None = None
@@ -258,10 +263,14 @@ class Daemon:
258
263
  self.notifier = Notifier(self, config.daemon.notifications)
259
264
  self.notifier.record_event("started")
260
265
  self.notifier.start()
266
+ self.updates = UpdateWatch(config.daemon.updates.check, self._update_lookup)
267
+ self.updates.start()
261
268
  log.info("daemon started with config %s", self.config_dir)
262
269
 
263
270
  def stop(self) -> None:
264
271
  self.jobs.stop()
272
+ if self.updates is not None:
273
+ self.updates.stop()
265
274
  if self.notifier is not None:
266
275
  # Held rather than sent, so an upgrade gives one mail that says
267
276
  # the daemon stopped and started; see docs/features/notifications.md.
@@ -297,6 +306,8 @@ class Daemon:
297
306
  self._start_sources()
298
307
  if self.notifier is not None:
299
308
  self.notifier.reconfigure(new.daemon.notifications)
309
+ if self.updates is not None:
310
+ self.updates.reconfigure(new.daemon.updates.check)
300
311
  log.info("configuration reloaded from %s", self.config_dir)
301
312
  return result
302
313
 
@@ -656,11 +667,7 @@ class Daemon:
656
667
  and the same warning a crashed instance raises."""
657
668
  assert self.state is not None
658
669
  failure = self.registry.failure_of(type_name)
659
- error = (
660
- failure.error
661
- if failure is not None
662
- else f"the plugin {type_name!r} is not installed"
663
- )
670
+ error = failure.error if failure is not None else not_installed(type_name)
664
671
  self.unloadable[name] = Unloadable(role, type_name, error)
665
672
  self.state.set_instance(
666
673
  name, role=role, type_name=type_name, state="failed", error=error
@@ -780,10 +787,7 @@ class Daemon:
780
787
  else:
781
788
  entry = self.unloadable.get(instance)
782
789
  if entry is not None and entry.type_name == type_name:
783
- raise InstanceNotRunning(
784
- f"{instance} cannot start: its plugin could not be loaded: "
785
- f"{entry.error}"
786
- )
790
+ raise InstanceNotRunning(f"{instance} cannot start: {entry.error}")
787
791
  raise NotFound(f"unknown plugin type {type_name!r}")
788
792
  if runner is None or runner.type_name != type_name:
789
793
  raise NotFound(f"no instance {instance!r} of {type_name!r}")
@@ -982,6 +986,7 @@ class Daemon:
982
986
  ],
983
987
  "warnings": len(self.state.warnings()),
984
988
  "observations_received": self.state.counter("observations_received"),
989
+ "updates": self.updates.summary() if self.updates else None,
985
990
  }
986
991
 
987
992
  def status_sources(self) -> list[dict[str, Any]]:
@@ -38,6 +38,7 @@ from bricklogger.daemon.codes import (
38
38
  from bricklogger.daemon.rules import BRICK, REC
39
39
  from bricklogger.daemon.state import RuntimeState
40
40
  from bricklogger.model.working_graph import MODEL_GRAPH, WorkingGraph
41
+ from bricklogger.ops.updates import available_line
41
42
 
42
43
  log = logging.getLogger("bricklogger.notify")
43
44
 
@@ -711,6 +712,9 @@ def _digest_sections(
711
712
  )
712
713
  else:
713
714
  sections.append(("Warnings", ["Nothing stands."], []))
715
+ newer = available_line(status.get("updates"))
716
+ if newer is not None:
717
+ sections.append(("Updates", [newer], []))
714
718
  return sections
715
719
 
716
720