topicgate 1.2.2__tar.gz → 1.3.0__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 (227) hide show
  1. {topicgate-1.2.2/src/topicgate.egg-info → topicgate-1.3.0}/PKG-INFO +7 -2
  2. {topicgate-1.2.2 → topicgate-1.3.0}/README.md +6 -1
  3. {topicgate-1.2.2 → topicgate-1.3.0}/pyproject.toml +1 -2
  4. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/app_dependencies.py +18 -35
  5. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/broker_runtime_state.py +1 -10
  6. topicgate-1.3.0/src/topicgate/app/models/broker_inspection.py +22 -0
  7. topicgate-1.3.0/src/topicgate/app/services/broker_inspection_service.py +67 -0
  8. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/broker_profile_service.py +5 -45
  9. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/broker_snapshot_service.py +45 -29
  10. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/observation_cache_service.py +33 -57
  11. topicgate-1.3.0/src/topicgate/app/services/observation_query_service.py +55 -0
  12. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/topicgate_runtime.py +69 -33
  13. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/interfaces/broker_profile_store.py +0 -3
  14. topicgate-1.3.0/src/topicgate/core/interfaces/current_topic_reader.py +20 -0
  15. topicgate-1.2.2/src/topicgate/core/interfaces/observer_repository.py → topicgate-1.3.0/src/topicgate/core/interfaces/observer_repo_metadata.py +3 -26
  16. topicgate-1.3.0/src/topicgate/core/interfaces/observer_repository.py +13 -0
  17. topicgate-1.3.0/src/topicgate/core/interfaces/stored_observation_administrator.py +21 -0
  18. topicgate-1.3.0/src/topicgate/core/interfaces/stored_observation_reader.py +36 -0
  19. topicgate-1.3.0/src/topicgate/core/interfaces/topic_message_recorder.py +23 -0
  20. topicgate-1.3.0/src/topicgate/core/models/current_topic.py +36 -0
  21. topicgate-1.3.0/src/topicgate/core/models/message_filter.py +25 -0
  22. topicgate-1.3.0/src/topicgate/core/models/observation_status.py +8 -0
  23. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/observer_workspace.py +0 -2
  24. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/topic_message.py +0 -1
  25. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/observer_limits.py +0 -5
  26. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/app.py +3 -0
  27. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/stored_observations_dialog.py +295 -10
  28. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/main_view_model.py +201 -6
  29. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/main_window.py +24 -0
  30. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/observer_state_reader.py +0 -4
  31. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/mappers/observer_workspace_mapper.py +1 -8
  32. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/repository/observer_mqtt_repository.py +48 -75
  33. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/repository/topic_message_repository.py +288 -87
  34. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/broker_api.py +31 -2
  35. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/connection_api.py +2 -2
  36. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/mcp_api.py +0 -2
  37. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/publish_api.py +2 -2
  38. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/snapshot_api.py +4 -1
  39. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/subscription_api.py +2 -2
  40. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/topic_api.py +13 -7
  41. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/server.py +19 -8
  42. {topicgate-1.2.2 → topicgate-1.3.0/src/topicgate.egg-info}/PKG-INFO +7 -2
  43. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate.egg-info/SOURCES.txt +12 -6
  44. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_app.py +20 -0
  45. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_app_dependencies.py +6 -2
  46. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_broker_config_repository.py +1 -12
  47. topicgate-1.3.0/tests/test_broker_inspection_service.py +69 -0
  48. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_broker_repository_persistence.py +7 -17
  49. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_broker_snapshot_service.py +25 -5
  50. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_dashboard_api.py +28 -16
  51. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_gui.py +145 -10
  52. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_main_view_model.py +211 -53
  53. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mcp_apis.py +72 -21
  54. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_observation_cache_service.py +3 -1
  55. topicgate-1.3.0/tests/test_observation_query_service.py +76 -0
  56. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_observer_model_hydration.py +10 -22
  57. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_observer_repository.py +77 -39
  58. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_observer_workspace_mapper.py +1 -9
  59. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_plugin_bundle.py +1 -0
  60. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_retention_administration.py +5 -2
  61. topicgate-1.3.0/tests/test_topic_message_repository.py +748 -0
  62. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_topic_presentation.py +1 -1
  63. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_topicgate_runtime.py +191 -10
  64. topicgate-1.2.2/src/topicgate/core/interfaces/topic_message_store.py +0 -13
  65. topicgate-1.2.2/src/topicgate/core/models/message_filter.py +0 -9
  66. topicgate-1.2.2/src/topicgate/core/models/observer_model.py +0 -22
  67. topicgate-1.2.2/src/topicgate/processors/observer_model_mqtt_message_processor.py +0 -79
  68. topicgate-1.2.2/src/topicgate/processors/observer_model_processor.py +0 -223
  69. topicgate-1.2.2/tests/test_observer_model_processor.py +0 -151
  70. topicgate-1.2.2/tests/test_observer_model_service.py +0 -153
  71. topicgate-1.2.2/tests/test_topic_message_repository.py +0 -386
  72. {topicgate-1.2.2 → topicgate-1.3.0}/LICENCE +0 -0
  73. {topicgate-1.2.2 → topicgate-1.3.0}/setup.cfg +0 -0
  74. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/__init__.py +0 -0
  75. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/__main__.py +0 -0
  76. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/__init__.py +0 -0
  77. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/models/__init__.py +0 -0
  78. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/models/broker_snapshot.py +0 -0
  79. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/models/mcp_setup.py +0 -0
  80. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/__init__.py +0 -0
  81. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/broker_resolver.py +0 -0
  82. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/control_operation_service.py +0 -0
  83. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/mcp_setup_service.py +0 -0
  84. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/observation_retention_policy_service.py +0 -0
  85. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/persistence_lifecycle.py +0 -0
  86. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/service_container.py +0 -0
  87. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/app/services/service_item.py +0 -0
  88. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/assets/icon.png +0 -0
  89. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/assets/icon.svg +0 -0
  90. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/__init__.py +0 -0
  91. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/config/__init__.py +0 -0
  92. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/config/app_config.py +0 -0
  93. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/config/mqtt_config.py +0 -0
  94. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/interfaces/__init__.py +0 -0
  95. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/interfaces/mqtt_message_processor.py +0 -0
  96. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/__init__.py +0 -0
  97. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/broker_profile.py +0 -0
  98. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/broker_profile_identity.py +0 -0
  99. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/broker_summary.py +0 -0
  100. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/connection_status.py +0 -0
  101. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/mqtt_message.py +0 -0
  102. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/mqtt_observation.py +0 -0
  103. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/mqtt_state.py +0 -0
  104. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/observation_cache_administration.py +0 -0
  105. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/observation_deletion_preview.py +0 -0
  106. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/observation_retention_policy.py +0 -0
  107. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/models/subscription.py +0 -0
  108. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/mqtt_topics.py +0 -0
  109. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/core/payload_limits.py +0 -0
  110. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/__init__.py +0 -0
  111. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/broker_state_reader.py +0 -0
  112. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/about_dialog.py +0 -0
  113. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/add_subscription_dialog.py +0 -0
  114. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/application_header.py +0 -0
  115. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/broker_settings_dialog.py +0 -0
  116. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/connection_controls.py +0 -0
  117. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/connection_status.py +0 -0
  118. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/log_console.py +0 -0
  119. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/mcp_setup_dialog.py +0 -0
  120. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/observer_tree.py +0 -0
  121. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/onboarding_panel.py +0 -0
  122. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/publish_pane.py +0 -0
  123. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/snapshot_panel.py +0 -0
  124. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/subscription_settings.py +0 -0
  125. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/topic_details.py +0 -0
  126. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/topic_metadata.py +0 -0
  127. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/components/workspace_pane.py +0 -0
  128. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/gui.py +0 -0
  129. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/settings_migration.py +0 -0
  130. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/gui/theme.py +0 -0
  131. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/__init__.py +0 -0
  132. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/credentials/__init__.py +0 -0
  133. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/credentials/credential_store.py +0 -0
  134. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/credentials/os_credential_store.py +0 -0
  135. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/__init__.py +0 -0
  136. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/README +0 -0
  137. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/env.py +0 -0
  138. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/script.py.mako +0 -0
  139. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/versions/7c3e9f1a2b4d_add_observation_retention_policy.py +0 -0
  140. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/versions/93fa5748f4b5_initial_revision.py +0 -0
  141. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/versions/b84d61a9c2e7_rename_retention_database_bytes.py +0 -0
  142. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/versions/d2a4c6e8f010_add_control_operation_lease.py +0 -0
  143. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic/versions/f1490ca3b908_persist_latest_observed_mqtt_state.py +0 -0
  144. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/alembic.ini +0 -0
  145. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/base.py +0 -0
  146. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/database_context.py +0 -0
  147. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/mappers/broker_profile_mapper.py +0 -0
  148. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/mappers/config_mapper.py +0 -0
  149. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/mappers/mapper_helper.py +0 -0
  150. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/mappers/observation_retention_policy_mapper.py +0 -0
  151. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/mappers/subscription_mapper.py +0 -0
  152. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/mappers/topic_message_mapper.py +0 -0
  153. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/migrations.py +0 -0
  154. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/__init__.py +0 -0
  155. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/app_config_row.py +0 -0
  156. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/broker_profile_row.py +0 -0
  157. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/mqtt_config_row.py +0 -0
  158. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/mqtt_message_row.py +0 -0
  159. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/observation_retention_policy_row.py +0 -0
  160. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/observer_workspace_row.py +0 -0
  161. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/database/models/subscription_row.py +0 -0
  162. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/__init__.py +0 -0
  163. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/async_callback_bridge.py +0 -0
  164. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/callbacks/__init__.py +0 -0
  165. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/callbacks/basic_callbacks.py +0 -0
  166. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/callbacks/observer_repository_callbacks.py +0 -0
  167. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/mqtt_callbacks.py +0 -0
  168. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/mqtt_client.py +0 -0
  169. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/mqtt/mqtt_gate.py +0 -0
  170. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/repository/__init__.py +0 -0
  171. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/repository/broker_config_repository.py +0 -0
  172. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/repository/broker_repository.py +0 -0
  173. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/repository/observation_retention_policy_repository.py +0 -0
  174. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/infrastructure/repository/subscription_repository.py +0 -0
  175. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/__init__.py +0 -0
  176. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/dashboard_api.py +0 -0
  177. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/dashboard_snapshot.py +0 -0
  178. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/api/preview/dashboard_preview.py +0 -0
  179. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/capabilities.py +0 -0
  180. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/instructions.py +0 -0
  181. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/middleware/__init__.py +0 -0
  182. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/middleware/error_handling_middleware.py +0 -0
  183. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/middleware/logging_middleware.py +0 -0
  184. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/mcp/models.py +0 -0
  185. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/paths.py +0 -0
  186. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/presentation/__init__.py +0 -0
  187. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/presentation/retention_presentation.py +0 -0
  188. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/presentation/snapshot_presentation.py +0 -0
  189. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/presentation/topic_presentation.py +0 -0
  190. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/processors/__init__.py +0 -0
  191. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/processors/observation_retention_processor.py +0 -0
  192. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate/processors/subscription_manager.py +0 -0
  193. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate.egg-info/dependency_links.txt +0 -0
  194. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate.egg-info/entry_points.txt +0 -0
  195. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate.egg-info/requires.txt +0 -0
  196. {topicgate-1.2.2 → topicgate-1.3.0}/src/topicgate.egg-info/top_level.txt +0 -0
  197. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_async_callback_bridge.py +0 -0
  198. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_broker_resolver.py +0 -0
  199. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_control_operation_service.py +0 -0
  200. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_dashboard_dependencies.py +0 -0
  201. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_dashboard_preview.py +0 -0
  202. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_database_migrations.py +0 -0
  203. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_desktop_snapshot_states.py +0 -0
  204. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_github_workflows.py +0 -0
  205. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mcp_disconnected_startup.py +0 -0
  206. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mcp_middleware.py +0 -0
  207. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mcp_server_instructions.py +0 -0
  208. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mcp_setup_service.py +0 -0
  209. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_migrate_app_versions.py +0 -0
  210. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mqtt_callbacks.py +0 -0
  211. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mqtt_client.py +0 -0
  212. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_mqtt_message_row.py +0 -0
  213. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_observation_retention_policy.py +0 -0
  214. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_observation_retention_policy_repository.py +0 -0
  215. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_observation_retention_policy_service.py +0 -0
  216. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_os_credential_store.py +0 -0
  217. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_paths.py +0 -0
  218. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_persistence_lifecycle.py +0 -0
  219. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_service_container.py +0 -0
  220. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_settings_migration.py +0 -0
  221. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_snapshot_mcp_api.py +0 -0
  222. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_snapshot_presentation.py +0 -0
  223. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_subscription.py +0 -0
  224. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_subscription_manager.py +0 -0
  225. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_subscription_mapper.py +0 -0
  226. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_subscription_repository.py +0 -0
  227. {topicgate-1.2.2 → topicgate-1.3.0}/tests/test_verify_distribution.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topicgate
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Secure local access to your MQTT topics
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/Dumdart/TopicGate
@@ -49,7 +49,6 @@ Dynamic: license-file
49
49
  <a href="#get-started">Get started</a> ·
50
50
  <a href="#connect-an-agent">Connect an agent</a> ·
51
51
  <a href="#how-observations-work">Understand observations</a> ·
52
- <a href="docs/desktop-workflow.md">Desktop workflow</a>
53
52
  </p>
54
53
 
55
54
  <p align="center">
@@ -60,6 +59,12 @@ TopicGate gives you a local, intentional view of MQTT data. Configure broker pro
60
59
 
61
60
  It is built for a practical boundary: broker credentials stay on your machine, observed state is persisted locally, and the MCP server starts in **read-only mode**. MQTT control—connecting, changing subscriptions, refreshing observations, or publishing—requires an explicit opt-in.
62
61
 
62
+ ## Demo
63
+
64
+ See TopicGate connect to an MQTT broker, inspect live topic state, answer an agent query with provenance, and enforce the boundary between observation and authorized control.
65
+
66
+ [Watch the TopicGate demo →](https://www.youtube.com/watch?v=_Qtc01kABkg)
67
+
63
68
  ## What it does
64
69
 
65
70
  | Desktop | MCP server |
@@ -11,7 +11,6 @@
11
11
  <a href="#get-started">Get started</a> ·
12
12
  <a href="#connect-an-agent">Connect an agent</a> ·
13
13
  <a href="#how-observations-work">Understand observations</a> ·
14
- <a href="docs/desktop-workflow.md">Desktop workflow</a>
15
14
  </p>
16
15
 
17
16
  <p align="center">
@@ -22,6 +21,12 @@ TopicGate gives you a local, intentional view of MQTT data. Configure broker pro
22
21
 
23
22
  It is built for a practical boundary: broker credentials stay on your machine, observed state is persisted locally, and the MCP server starts in **read-only mode**. MQTT control—connecting, changing subscriptions, refreshing observations, or publishing—requires an explicit opt-in.
24
23
 
24
+ ## Demo
25
+
26
+ See TopicGate connect to an MQTT broker, inspect live topic state, answer an agent query with provenance, and enforce the boundary between observation and authorized control.
27
+
28
+ [Watch the TopicGate demo →](https://www.youtube.com/watch?v=_Qtc01kABkg)
29
+
25
30
  ## What it does
26
31
 
27
32
  | Desktop | MCP server |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "topicgate"
7
- version = "1.2.2"
7
+ version = "1.3.0"
8
8
  description = "Secure local access to your MQTT topics"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -62,7 +62,6 @@ test = [
62
62
  testpaths = ["tests"]
63
63
  pythonpath = ["."]
64
64
  cache_dir = ".pytest_cache"
65
- addopts = "--basetemp=.pytest-tmp"
66
65
  asyncio_mode = "auto"
67
66
 
68
67
  [project.scripts]
@@ -1,6 +1,5 @@
1
1
  from pathlib import Path
2
- from uuid import UUID
3
-
2
+ from topicgate.app.services.observation_query_service import ObservationQueryService
4
3
  from topicgate.app.services.service_item import ServiceItem
5
4
  from topicgate.app.services.persistence_lifecycle import PersistenceLifecycle
6
5
  from topicgate.app.services.broker_profile_service import BrokerProfileService
@@ -9,14 +8,13 @@ from topicgate.app.services.observation_retention_policy_service import (
9
8
  ObservationRetentionPolicyService,
10
9
  )
11
10
  from topicgate.app.services.broker_snapshot_service import BrokerSnapshotService
11
+ from topicgate.app.services.broker_resolver import BrokerResolver
12
12
  from topicgate.app.services.control_operation_service import ControlOperationService
13
13
  from topicgate.app.services.mcp_setup_service import McpSetupService
14
14
  from topicgate.app.broker_runtime_state import BrokerRuntimeState
15
15
  from topicgate.app.topicgate_runtime import TopicGateRuntime
16
16
  from topicgate.core.interfaces.observer_repository import ObserverRepository
17
17
  from topicgate.core.models.broker_profile import BrokerProfile
18
- from topicgate.core.models.mqtt_observation import MqttObservation
19
- from topicgate.core.models.topic_message import TopicMessage
20
18
  from topicgate.infrastructure.database.database_context import DatabaseContext
21
19
  from topicgate.infrastructure.credentials.credential_store import CredentialStore
22
20
  from topicgate.infrastructure.credentials.os_credential_store import OSCredentialStore
@@ -64,7 +62,9 @@ class AppDependencies:
64
62
  self.observation_cache = ObservationCacheService(
65
63
  self.topic_messages,
66
64
  self.retention_policy,
65
+ administrator=self.topic_messages,
67
66
  )
67
+ self.observation_query = ObservationQueryService(self.topic_messages)
68
68
  self.control_operations = ControlOperationService(
69
69
  self._db_context,
70
70
  control_owner,
@@ -90,13 +90,19 @@ class AppDependencies:
90
90
 
91
91
  self.runtime = TopicGateRuntime(
92
92
  self.broker_profiles,
93
- self.broker_runtime_state.repositories,
93
+ self.broker_runtime_state.repositories,
94
94
  profile.id,
95
- self._create_observer_repository,
96
- self.observation_cache,
97
- self.control_operations,
95
+ mqtt_repository_factory=self._create_observer_repository,
96
+ observation_cache=self.observation_cache,
97
+ observation_query=self.observation_query,
98
+ current_topics=self.topic_messages,
99
+ control_operations=self.control_operations,
100
+ )
101
+ self.broker_resolver = BrokerResolver(self.runtime)
102
+ self.snapshot_service = BrokerSnapshotService(
103
+ self.runtime,
104
+ resolver=self.broker_resolver,
98
105
  )
99
- self.snapshot_service = BrokerSnapshotService(self.runtime)
100
106
  self.mcp_setup = McpSetupService(
101
107
  self.runtime,
102
108
  self.snapshot_service,
@@ -118,31 +124,8 @@ class AppDependencies:
118
124
  return ObserverMqttRepository(
119
125
  profile.config,
120
126
  list(profile.workspace.subscriptions),
121
- profile.workspace.model,
122
127
  retention_policy=self.retention_policy.get,
123
- observation_sink=lambda observation: self._persist_observation(
124
- profile.id,
125
- observation,
126
- ),
127
- )
128
-
129
- def _persist_observation(
130
- self,
131
- broker_id: UUID,
132
- observation: MqttObservation,
133
- ) -> None:
134
- if observation.observation_id is None:
135
- raise ValueError("A live observation requires an observation ID.")
136
- self.topic_messages.update_message(
137
- TopicMessage(
138
- broker_id=broker_id,
139
- topic=observation.topic,
140
- payload=observation.payload,
141
- qos=observation.qos,
142
- retain=observation.retain,
143
- received_at=observation.received_at,
144
- payload_size=observation.payload_size or len(observation.payload),
145
- message_count=observation.message_count,
146
- observation_id=observation.observation_id,
147
- )
128
+ broker_id=profile.id,
129
+ message_recorder=self.topic_messages,
130
+ current_topics=self.topic_messages,
148
131
  )
@@ -1,27 +1,18 @@
1
1
  from uuid import UUID
2
2
 
3
3
  from topicgate.core.interfaces.observer_repository import ObserverRepository
4
- from topicgate.core.models.observer_model import ObserverModel
5
4
  from topicgate.core.models.broker_profile import BrokerProfile
6
5
 
7
6
 
8
7
  class BrokerRuntimeState:
9
- """In-memory observer models and repositories, keyed by broker identity."""
8
+ """In-memory repositories and profile metadata, keyed by broker identity."""
10
9
 
11
10
  def __init__(self) -> None:
12
- self._models: dict[UUID, ObserverModel] = {}
13
11
  self._repositories: dict[UUID, ObserverRepository] = {}
14
12
  self._profile_handles: dict[UUID, BrokerProfile] = {}
15
13
  self._config_ids: dict[UUID, int | None] = {}
16
14
 
17
- def get_model(self, broker_id: UUID) -> ObserverModel | None:
18
- return self._models.get(broker_id)
19
-
20
- def set_model(self, broker_id: UUID, model: ObserverModel) -> None:
21
- self._models[broker_id] = model
22
-
23
15
  def remove(self, broker_id: UUID) -> None:
24
- self._models.pop(broker_id, None)
25
16
  self._repositories.pop(broker_id, None)
26
17
  self._profile_handles.pop(broker_id, None)
27
18
  self._config_ids.pop(broker_id, None)
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+
3
+ from topicgate.app.models.broker_snapshot import BrokerSnapshot
4
+ from topicgate.core.models.broker_summary import BrokerSummary
5
+ from topicgate.core.models.observation_cache_administration import BrokerCacheUsage
6
+ from topicgate.core.models.subscription import Subscription
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class BrokerConnectionState:
11
+ status: str
12
+ dropped_message_count: int
13
+ topic_update_interval: float
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class BrokerInspection:
18
+ identity: BrokerSummary
19
+ connection: BrokerConnectionState
20
+ subscriptions: tuple[Subscription, ...]
21
+ cache: BrokerCacheUsage
22
+ snapshot: BrokerSnapshot | None
@@ -0,0 +1,67 @@
1
+ from uuid import UUID
2
+
3
+ from topicgate.app.models.broker_inspection import (
4
+ BrokerConnectionState,
5
+ BrokerInspection,
6
+ )
7
+ from topicgate.app.services.broker_resolver import BrokerResolver
8
+ from topicgate.app.services.broker_snapshot_service import (
9
+ DEFAULT_SNAPSHOT_RESULT_LIMIT,
10
+ BrokerSnapshotService,
11
+ )
12
+ from topicgate.app.topicgate_runtime import TopicGateRuntime
13
+ from topicgate.core.models.observation_cache_administration import BrokerCacheUsage
14
+ from topicgate.core.payload_limits import MAX_RENDERED_PAYLOAD_BYTES
15
+
16
+
17
+ class BrokerInspectionService:
18
+ """Compose passive broker reads into one presentation-neutral response."""
19
+
20
+ def __init__(
21
+ self,
22
+ runtime: TopicGateRuntime,
23
+ snapshot_service: BrokerSnapshotService,
24
+ resolver: BrokerResolver,
25
+ ) -> None:
26
+ self._runtime = runtime
27
+ self._snapshot_service = snapshot_service
28
+ self._resolver = resolver
29
+
30
+ def inspect(
31
+ self,
32
+ broker: UUID | str,
33
+ *,
34
+ include_snapshot: bool = False,
35
+ snapshot_limit: int = DEFAULT_SNAPSHOT_RESULT_LIMIT,
36
+ payload_limit_bytes: int = MAX_RENDERED_PAYLOAD_BYTES,
37
+ ) -> BrokerInspection:
38
+ resolved = self._resolver.resolve(broker)
39
+ cache_summary = self._runtime.get_observation_storage_summary(resolved.id)
40
+ cache = next(
41
+ iter(cache_summary.brokers),
42
+ BrokerCacheUsage(resolved.id, 0, 0, None, None),
43
+ )
44
+ snapshot = (
45
+ self._snapshot_service.build_current(
46
+ resolved.id,
47
+ result_limit=snapshot_limit,
48
+ payload_limit_bytes=payload_limit_bytes,
49
+ )
50
+ if include_snapshot
51
+ else None
52
+ )
53
+ return BrokerInspection(
54
+ identity=resolved,
55
+ connection=BrokerConnectionState(
56
+ status=str(self._runtime.get_connection_status(resolved.id)),
57
+ dropped_message_count=self._runtime.get_dropped_message_count(
58
+ resolved.id
59
+ ),
60
+ topic_update_interval=self._runtime.get_topic_update_interval(
61
+ resolved.id
62
+ ),
63
+ ),
64
+ subscriptions=self._runtime.list_subscriptions(resolved.id),
65
+ cache=cache,
66
+ snapshot=snapshot,
67
+ )
@@ -5,12 +5,7 @@ from topicgate.app.broker_runtime_state import BrokerRuntimeState
5
5
  from topicgate.core.config.app_config import AppConfig
6
6
  from topicgate.core.config.mqtt_config import MqttConfig
7
7
  from topicgate.core.models.broker_profile import BrokerProfile
8
- from topicgate.core.models.observer_model import ObserverModel
9
- from topicgate.core.models.mqtt_observation import (
10
- MqttObservation,
11
- ObservationSource,
12
- )
13
- from topicgate.core.interfaces.topic_message_store import TopicMessageStore
8
+ from topicgate.core.interfaces.topic_message_recorder import TopicMessageRecorder
14
9
  from topicgate.core.models.observer_workspace import ObserverWorkspace
15
10
  from topicgate.infrastructure.credentials.credential_store import CredentialStore
16
11
  from topicgate.infrastructure.database.database_context import DatabaseContext
@@ -21,7 +16,6 @@ from topicgate.infrastructure.repository.broker_repository import BrokerReposito
21
16
  from topicgate.infrastructure.repository.subscription_repository import (
22
17
  SubscriptionRepository,
23
18
  )
24
- from topicgate.processors.observer_model_processor import ObserverModelProcessor
25
19
 
26
20
 
27
21
  class BrokerProfileService:
@@ -34,7 +28,7 @@ class BrokerProfileService:
34
28
  *,
35
29
  credential_store: CredentialStore,
36
30
  runtime_state: BrokerRuntimeState | None = None,
37
- topic_messages: TopicMessageStore | None = None,
31
+ topic_messages: TopicMessageRecorder | None = None,
38
32
  ) -> None:
39
33
  if isinstance(settings, DatabaseContext):
40
34
  self._db = settings
@@ -49,7 +43,7 @@ class BrokerProfileService:
49
43
  self.brokers = BrokerRepository(self._db)
50
44
  self.configs = BrokerConfigRepository(self._db)
51
45
  self.subscriptions = SubscriptionRepository(self._db)
52
- self._topic_messages = topic_messages
46
+ self._topic_message_recorder = topic_messages
53
47
  self._settings_id = supplied_settings.id if supplied_settings else None
54
48
 
55
49
  identities = self.brokers.list_profiles()
@@ -81,13 +75,9 @@ class BrokerProfileService:
81
75
  id=self._runtime_state.get_config_id(identity.id, config.id),
82
76
  )
83
77
  subscriptions = self.subscriptions.list_for_workspace(identity.workspace_id)
84
- model = self._runtime_state.get_model(identity.id)
85
- if model is None:
86
- model = self._hydrate_observer_model(identity.id, subscriptions)
87
78
  workspace = ObserverWorkspace(
88
79
  id=identity.workspace_id,
89
80
  profile_id=identity.id,
90
- model=model,
91
81
  subscriptions=subscriptions,
92
82
  )
93
83
  return BrokerProfile(
@@ -140,6 +130,8 @@ class BrokerProfileService:
140
130
  profile_id
141
131
  )
142
132
  self.brokers.delete_profile(profile_id)
133
+ if self._topic_message_recorder is not None:
134
+ self._topic_message_recorder.remove_current_broker(profile_id)
143
135
  self._delete_password(profile_id)
144
136
  self._runtime_state.remove(profile_id)
145
137
  self.save()
@@ -164,13 +156,6 @@ class BrokerProfileService:
164
156
  def update_observer_workspace(self, workspace: ObserverWorkspace) -> None:
165
157
  self.replace_subscriptions(workspace.id, workspace.subscriptions)
166
158
 
167
- def update_observer_model(self, model: ObserverModel) -> None:
168
- self._runtime_state.set_model(self.brokers.get_profile().id, model)
169
- self.save()
170
-
171
- def get_observer_model(self) -> ObserverModel:
172
- return self.get_profile().workspace.model
173
-
174
159
  def get_observer_workspace(self) -> ObserverWorkspace:
175
160
  return self.get_profile().workspace
176
161
 
@@ -209,31 +194,6 @@ class BrokerProfileService:
209
194
  self._store_password(identity.id, config.password)
210
195
  self._runtime_state.set_config_id(identity.id, config.id)
211
196
 
212
- def _hydrate_observer_model(self, broker_id: UUID, subscriptions) -> ObserverModel:
213
- model = ObserverModelProcessor.add_topics(
214
- ObserverModel(root_stats=[]),
215
- (item.topic_filter for item in subscriptions),
216
- )
217
- if self._topic_messages is None:
218
- return model
219
- for message in self._topic_messages.get_latest_messages(broker_id):
220
- node = ObserverModelProcessor.find_or_create_node(model, message.topic)
221
- state = MqttObservation(
222
- name=node.segment,
223
- topic=message.topic,
224
- payload=message.payload,
225
- qos=message.qos,
226
- retain=message.retain,
227
- recieved_at=message.received_at,
228
- payload_size=message.payload_size,
229
- message_count=message.message_count,
230
- source=ObservationSource.STORED,
231
- observation_id=message.observation_id,
232
- )
233
- node.state = state
234
- model.topic_states[message.topic] = state
235
- return model
236
-
237
197
  def _store_password(self, profile_id: UUID, password: str) -> None:
238
198
  if password:
239
199
  self._credentials.set_password(profile_id, password)
@@ -21,10 +21,14 @@ from topicgate.app.models.broker_snapshot import (
21
21
  )
22
22
  from topicgate.app.services.broker_resolver import BrokerResolver
23
23
  from topicgate.app.topicgate_runtime import TopicGateRuntime
24
+ from topicgate.core.interfaces.current_topic_reader import CurrentTopicReader
24
25
  from topicgate.core.models.broker_summary import BrokerSummary
25
26
  from topicgate.core.models.connection_status import ConnectionStatus
26
- from topicgate.core.models.mqtt_observation import MqttObservation, ObservationSource
27
+ from topicgate.core.models.current_topic import CurrentTopic
28
+ from topicgate.core.models.mqtt_observation import ObservationSource
29
+ from topicgate.core.models.observation_status import ObservationStatus
27
30
  from topicgate.core.models.subscription import Subscription
31
+ from topicgate.core.models.topic_message import TopicMessage
28
32
  from topicgate.core.mqtt_topics import mqtt_filter_matches
29
33
  from topicgate.core.observer_limits import MAX_OBSERVED_TOPICS
30
34
  from topicgate.core.payload_limits import MAX_RENDERED_PAYLOAD_BYTES
@@ -46,12 +50,14 @@ class BrokerSnapshotService:
46
50
  self,
47
51
  runtime: TopicGateRuntime,
48
52
  *,
53
+ current_topics: CurrentTopicReader | None = None,
49
54
  resolver: BrokerResolver | None = None,
50
55
  clock: Clock | None = None,
51
56
  monotonic_clock: MonotonicClock | None = None,
52
57
  sleep: Sleep | None = None,
53
58
  ) -> None:
54
59
  self._runtime = runtime
60
+ self._current_topics = current_topics or runtime
55
61
  self._resolver = resolver or BrokerResolver(runtime)
56
62
  self._clock = clock or (lambda: datetime.now(timezone.utc))
57
63
  self._monotonic_clock = monotonic_clock or time.monotonic
@@ -142,22 +148,24 @@ class BrokerSnapshotService:
142
148
  actual_wait_seconds: float,
143
149
  ) -> BrokerSnapshot:
144
150
  captured_at = self._as_utc(self._clock())
145
- model = self._runtime.get_observer_model(resolved.id)
146
151
  matching = sorted(
147
152
  (
148
- state
149
- for state in model.topic_states.values()
150
- if mqtt_filter_matches(topic_filter, state.topic)
153
+ current
154
+ for current in self._current_topics.get_current_topics(resolved.id)
155
+ if mqtt_filter_matches(topic_filter, current.message.topic)
151
156
  ),
152
- key=lambda state: state.topic,
157
+ key=lambda current: current.message.topic,
153
158
  )
154
159
  aged = tuple(
155
- (state, self._age_seconds(captured_at, state.received_at))
156
- for state in matching
160
+ (
161
+ current,
162
+ self._age_seconds(captured_at, current.message.received_at),
163
+ )
164
+ for current in matching
157
165
  )
158
166
  fresh = tuple(
159
- (state, age)
160
- for state, age in aged
167
+ (current, age)
168
+ for current, age in aged
161
169
  if max_age_seconds is None or age <= max_age_seconds
162
170
  )
163
171
  stale_count = len(aged) - len(fresh)
@@ -173,12 +181,12 @@ class BrokerSnapshotService:
173
181
  )
174
182
  topics = tuple(
175
183
  self._topic_snapshot(
176
- state,
184
+ current,
177
185
  age,
178
186
  payload_limit_bytes,
179
187
  observation_started_at=observation_started_at,
180
188
  )
181
- for state, age in selected
189
+ for current, age in selected
182
190
  )
183
191
 
184
192
  observed_for_seconds = (
@@ -240,47 +248,53 @@ class BrokerSnapshotService:
240
248
  @classmethod
241
249
  def _topic_snapshot(
242
250
  cls,
243
- state: MqttObservation,
251
+ current: CurrentTopic,
244
252
  age_seconds: float,
245
253
  payload_limit_bytes: int,
246
254
  *,
247
255
  observation_started_at: datetime | None,
248
256
  ) -> SnapshotTopicState:
257
+ message = current.message
249
258
  return SnapshotTopicState(
250
- topic=state.topic,
251
- payload=cls._render_payload(state, payload_limit_bytes),
252
- qos=state.qos,
253
- retain=state.retain,
254
- received_at=cls._as_utc(state.received_at),
259
+ topic=message.topic,
260
+ payload=cls._render_payload(message, payload_limit_bytes),
261
+ qos=message.qos,
262
+ retain=message.retain,
263
+ received_at=cls._as_utc(message.received_at),
255
264
  age_seconds=age_seconds,
256
- message_count=state.message_count,
257
- source=state.source,
258
- status=cls._topic_status(state, observation_started_at),
265
+ message_count=message.message_count,
266
+ source=(
267
+ ObservationSource.STORED
268
+ if current.status is ObservationStatus.CACHED
269
+ else ObservationSource.LIVE
270
+ ),
271
+ status=cls._topic_status(current, observation_started_at),
259
272
  )
260
273
 
261
274
  @classmethod
262
275
  def _topic_status(
263
276
  cls,
264
- state: MqttObservation,
277
+ current: CurrentTopic,
265
278
  observation_started_at: datetime | None,
266
279
  ) -> SnapshotTopicStatus:
280
+ message = current.message
267
281
  if (
268
282
  observation_started_at is not None
269
- and cls._as_utc(state.received_at)
283
+ and cls._as_utc(message.received_at)
270
284
  < cls._as_utc(observation_started_at)
271
285
  ):
272
286
  return SnapshotTopicStatus.STALE
273
- if state.source == ObservationSource.STORED:
287
+ if current.status is ObservationStatus.CACHED:
274
288
  return SnapshotTopicStatus.CACHED
275
289
  return SnapshotTopicStatus.LIVE
276
290
 
277
291
  @staticmethod
278
292
  def _render_payload(
279
- state: MqttObservation,
293
+ message: TopicMessage,
280
294
  payload_limit_bytes: int,
281
295
  ) -> SnapshotPayload:
282
- available = bytes(state.payload)
283
- original_size = max(state.payload_size or 0, len(available))
296
+ available = bytes(message.payload)
297
+ original_size = max(message.payload_size, len(available))
284
298
  rendered = available[:payload_limit_bytes]
285
299
  try:
286
300
  available.decode("utf-8")
@@ -310,7 +324,7 @@ class BrokerSnapshotService:
310
324
  def _limitations(
311
325
  *,
312
326
  status: object,
313
- matching: list[MqttObservation],
327
+ matching: list[CurrentTopic],
314
328
  topics: tuple[SnapshotTopicState, ...],
315
329
  observation_started_at: datetime | None,
316
330
  dropped_message_count: int,
@@ -325,7 +339,9 @@ class BrokerSnapshotService:
325
339
  limitations.append(SnapshotLimitation.BROKER_DISCONNECTED)
326
340
  if observation_started_at is None:
327
341
  limitations.append(SnapshotLimitation.OBSERVATION_NOT_STARTED)
328
- if any(state.source == ObservationSource.STORED for state in matching):
342
+ if any(
343
+ current.status is ObservationStatus.CACHED for current in matching
344
+ ):
329
345
  limitations.append(
330
346
  SnapshotLimitation.STORED_STATE_PREDATES_OBSERVATION
331
347
  )