topos-node 0.1.0__py3-none-any.whl

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 (249) hide show
  1. shared/__init__.py +59 -0
  2. shared/filtering.py +640 -0
  3. shared/schema_registry.py +229 -0
  4. topos/__init__.py +5 -0
  5. topos/__version__.py +6 -0
  6. topos/analytics/__init__.py +15 -0
  7. topos/analytics/duckdb_adapter.py +48 -0
  8. topos/analytics/messenger_communities.py +349 -0
  9. topos/analytics/messenger_graph.py +522 -0
  10. topos/analytics/messenger_labels.py +321 -0
  11. topos/analytics/profiles.py +22 -0
  12. topos/analytics/query_engine.py +64 -0
  13. topos/analytics/raw_queries.py +174 -0
  14. topos/api/__init__.py +1 -0
  15. topos/api/analytics.py +52 -0
  16. topos/api/app_registry.py +31 -0
  17. topos/api/backup.py +15 -0
  18. topos/api/compute_remote.py +175 -0
  19. topos/api/data_commit.py +158 -0
  20. topos/api/data_explorer_table_prefs.py +81 -0
  21. topos/api/db.py +10 -0
  22. topos/api/device.py +25 -0
  23. topos/api/enrichment.py +959 -0
  24. topos/api/filter_lab.py +195 -0
  25. topos/api/health.py +61 -0
  26. topos/api/ingestion_api.py +37 -0
  27. topos/api/ingestion_compat.py +21 -0
  28. topos/api/ingestion_sources.py +600 -0
  29. topos/api/llm.py +76 -0
  30. topos/api/local_mcp.py +46 -0
  31. topos/api/messenger_analytics.py +385 -0
  32. topos/api/query_api.py +13 -0
  33. topos/api/sanitization_ollama_config.py +64 -0
  34. topos/api/source_install.py +324 -0
  35. topos/api/sources.py +13 -0
  36. topos/api/sync.py +10 -0
  37. topos/api/ui_config.py +83 -0
  38. topos/api/uma_data.py +311 -0
  39. topos/api/usage.py +49 -0
  40. topos/api/user_identity.py +46 -0
  41. topos/app.py +239 -0
  42. topos/auth.py +17 -0
  43. topos/canonicalization/__init__.py +1 -0
  44. topos/canonicalization/mappers/__init__.py +22 -0
  45. topos/canonicalization/mappers/base.py +26 -0
  46. topos/canonicalization/mappers/chatgpt_mapper.py +40 -0
  47. topos/canonicalization/mappers/grok_mapper.py +17 -0
  48. topos/canonicalization/mappers/messenger_mapper.py +58 -0
  49. topos/canonicalization/models.py +31 -0
  50. topos/canonicalization/resolver.py +23 -0
  51. topos/cli/__init__.py +1 -0
  52. topos/cli/__main__.py +6 -0
  53. topos/cli/commands.py +132 -0
  54. topos/config/__init__.py +1 -0
  55. topos/config/sanitization_ollama.py +189 -0
  56. topos/config/settings.py +310 -0
  57. topos/contacts/__init__.py +5 -0
  58. topos/contacts/identity.py +24 -0
  59. topos/control_plane_client.py +300 -0
  60. topos/core/__init__.py +1 -0
  61. topos/core/api_models.py +128 -0
  62. topos/core/connection_resilience.py +99 -0
  63. topos/core/device_helpers.py +8 -0
  64. topos/core/errors.py +13 -0
  65. topos/core/events.py +12 -0
  66. topos/core/handlers.py +5625 -0
  67. topos/core/logging.py +175 -0
  68. topos/core/metrics.py +21 -0
  69. topos/core/startup_banner.py +62 -0
  70. topos/core/state.py +682 -0
  71. topos/core/table_layers.py +45 -0
  72. topos/core/types.py +13 -0
  73. topos/data_explorer_table_prefs.py +150 -0
  74. topos/engine/__init__.py +29 -0
  75. topos/engine/backends/__init__.py +50 -0
  76. topos/engine/backends/base.py +21 -0
  77. topos/engine/backends/huggingface.py +151 -0
  78. topos/engine/backends/ollama.py +181 -0
  79. topos/engine/backends/stub.py +22 -0
  80. topos/engine/engine.py +165 -0
  81. topos/engine/intake.py +32 -0
  82. topos/engine/queue_manager.py +112 -0
  83. topos/engine/registration.py +126 -0
  84. topos/engine/result_formatter.py +38 -0
  85. topos/engine/router.py +19 -0
  86. topos/engine/scoped_token.py +82 -0
  87. topos/engine/tasks.py +154 -0
  88. topos/engine/transport.py +44 -0
  89. topos/engine/usage_guard.py +100 -0
  90. topos/engine/usage_observation.py +129 -0
  91. topos/engine/validator.py +23 -0
  92. topos/enrichment/__init__.py +1 -0
  93. topos/enrichment/derived_tables.py +214 -0
  94. topos/enrichment/jobs/__init__.py +30 -0
  95. topos/enrichment/jobs/base.py +54 -0
  96. topos/enrichment/jobs/canonical/__init__.py +1 -0
  97. topos/enrichment/jobs/canonical/embeddings_job.py +27 -0
  98. topos/enrichment/jobs/canonical/emo_27_job.py +97 -0
  99. topos/enrichment/jobs/canonical/entities_job.py +27 -0
  100. topos/enrichment/jobs/canonical/sentiment_job.py +27 -0
  101. topos/enrichment/jobs/canonical/topics_job.py +27 -0
  102. topos/enrichment/jobs/raw/__init__.py +1 -0
  103. topos/enrichment/jobs/raw/attachments_job.py +12 -0
  104. topos/enrichment/jobs/raw/language_job.py +12 -0
  105. topos/enrichment/jobs/raw/time_normalization_job.py +12 -0
  106. topos/enrichment/jobs/raw/tool_calls_job.py +12 -0
  107. topos/enrichment/models/__init__.py +1 -0
  108. topos/enrichment/models/manager.py +8 -0
  109. topos/enrichment/models/registry.py +71 -0
  110. topos/enrichment/models/versioning.py +8 -0
  111. topos/enrichment/orchestrator.py +177 -0
  112. topos/enrichment/processor.py +17 -0
  113. topos/enrichment/progress_bar.py +122 -0
  114. topos/enrichment/website_classifier.py +31 -0
  115. topos/filter_lab/__init__.py +1 -0
  116. topos/filter_lab/bundles.py +300 -0
  117. topos/filter_lab/schema.py +86 -0
  118. topos/filter_lab/service.py +167 -0
  119. topos/filter_lab/store.py +374 -0
  120. topos/filter_lab/worker.py +250 -0
  121. topos/hosted_pool_lease.py +153 -0
  122. topos/ingestion/__init__.py +1 -0
  123. topos/ingestion/checkpoints/__init__.py +6 -0
  124. topos/ingestion/checkpoints/checkpoint_store.py +24 -0
  125. topos/ingestion/checkpoints/sqlite_checkpoint_store.py +82 -0
  126. topos/ingestion/ingest_helpers.py +504 -0
  127. topos/ingestion/jobs.py +91 -0
  128. topos/ingestion/local_sync.py +823 -0
  129. topos/ingestion/log_preview.py +21 -0
  130. topos/ingestion/manager.py +1100 -0
  131. topos/ingestion/parser.py +174 -0
  132. topos/ingestion/parsers/__init__.py +32 -0
  133. topos/ingestion/parsers/base.py +24 -0
  134. topos/ingestion/parsers/browser_parser.py +171 -0
  135. topos/ingestion/parsers/calendar_parser.py +21 -0
  136. topos/ingestion/parsers/chatgpt_conversation_flattener.py +266 -0
  137. topos/ingestion/parsers/chatgpt_parser.py +67 -0
  138. topos/ingestion/parsers/grok_parser.py +21 -0
  139. topos/ingestion/parsers/messenger_parser.py +97 -0
  140. topos/ingestion/progress.py +54 -0
  141. topos/ingestion/sources/__init__.py +20 -0
  142. topos/ingestion/sources/base.py +39 -0
  143. topos/ingestion/sources/calendar.py +29 -0
  144. topos/ingestion/sources/chatgpt.py +29 -0
  145. topos/ingestion/sources/contact_importers.py +274 -0
  146. topos/ingestion/sources/grok.py +29 -0
  147. topos/ingestion/sources/imessage_reader.py +479 -0
  148. topos/ingestion/sources/signal_export_parser.py +132 -0
  149. topos/ingestion/sources/signal_reader.py +491 -0
  150. topos/ingestion/state_machine.py +70 -0
  151. topos/ingestion/triggers/__init__.py +1 -0
  152. topos/ingestion/triggers/file_trigger.py +36 -0
  153. topos/ingestion/triggers/sqlite_trigger.py +18 -0
  154. topos/ingestion/validation/__init__.py +1 -0
  155. topos/ingestion/validation/base.py +27 -0
  156. topos/ingestion/validation/schema_registry.py +111 -0
  157. topos/ingestion/validation/schema_validator.py +13 -0
  158. topos/lineage/__init__.py +1 -0
  159. topos/lineage/provenance.py +9 -0
  160. topos/lineage/tracker.py +9 -0
  161. topos/mcp_stdio_proxy.py +83 -0
  162. topos/observability/__init__.py +1 -0
  163. topos/observability/alerts.py +7 -0
  164. topos/observability/metrics.py +25 -0
  165. topos/observability/tracing.py +18 -0
  166. topos/openai_client.py +69 -0
  167. topos/projections/__init__.py +1 -0
  168. topos/projections/vector_index/__init__.py +1 -0
  169. topos/projections/vector_index/base.py +21 -0
  170. topos/projections/vector_index/builders.py +11 -0
  171. topos/projections/vector_index/health_checks.py +5 -0
  172. topos/rate_limit.py +43 -0
  173. topos/sanitization/__init__.py +16 -0
  174. topos/sanitization/ollama_transforms.py +276 -0
  175. topos/scope_resolution.py +89 -0
  176. topos/services/__init__.py +1 -0
  177. topos/services/container.py +46 -0
  178. topos/services/embeddings/__init__.py +1 -0
  179. topos/services/embeddings/base.py +7 -0
  180. topos/services/embeddings/local.py +9 -0
  181. topos/services/embeddings/remote.py +9 -0
  182. topos/services/interfaces.py +40 -0
  183. topos/services/llm/__init__.py +1 -0
  184. topos/services/llm/base.py +7 -0
  185. topos/services/llm/openai.py +126 -0
  186. topos/services/local.py +123 -0
  187. topos/services/postgres.py +385 -0
  188. topos/sources/__init__.py +6 -0
  189. topos/sources/definitions.py +114 -0
  190. topos/sources/install_service.py +836 -0
  191. topos/sources/registry.py +263 -0
  192. topos/sources/runtime_install.py +427 -0
  193. topos/storage/__init__.py +1 -0
  194. topos/storage/canonical/__init__.py +18 -0
  195. topos/storage/canonical/ai_chat/__init__.py +22 -0
  196. topos/storage/canonical/ai_chat/canonicalizer.py +147 -0
  197. topos/storage/canonical/ai_chat/mapper.py +168 -0
  198. topos/storage/canonical/ai_chat/model.py +87 -0
  199. topos/storage/canonical/ai_chat/tables.py +179 -0
  200. topos/storage/canonical/canonical_store.py +24 -0
  201. topos/storage/canonical/conversations_tables.py +1020 -0
  202. topos/storage/canonical/mapping_store.py +30 -0
  203. topos/storage/canonical/postgres.py +10 -0
  204. topos/storage/db/__init__.py +1 -0
  205. topos/storage/db/client.py +8 -0
  206. topos/storage/db/migrations/__init__.py +1 -0
  207. topos/storage/db/migrations/stage9_column_renames.py +78 -0
  208. topos/storage/db/paths.py +122 -0
  209. topos/storage/db/postgres.py +240 -0
  210. topos/storage/db/schema.py +6 -0
  211. topos/storage/enrichment/__init__.py +1 -0
  212. topos/storage/enrichment/canonical_enrichment_store.py +7 -0
  213. topos/storage/enrichment/raw_enrichment_store.py +18 -0
  214. topos/storage/normalized/__init__.py +1 -0
  215. topos/storage/normalized/normalized_store.py +24 -0
  216. topos/storage/oplog/__init__.py +1 -0
  217. topos/storage/oplog/decision.py +6 -0
  218. topos/storage/oplog/oplog_store.py +17 -0
  219. topos/storage/oplog/postgres.py +10 -0
  220. topos/storage/projections/__init__.py +1 -0
  221. topos/storage/projections/index_ops_store.py +6 -0
  222. topos/storage/projections/vector_index_store.py +6 -0
  223. topos/storage/raw/__init__.py +1 -0
  224. topos/storage/raw/browser_flat_tables.py +303 -0
  225. topos/storage/raw/file_store.py +100 -0
  226. topos/storage/raw/raw_store.py +29 -0
  227. topos/storage/raw/raw_tables_manager.py +295 -0
  228. topos/storage/raw/sqlite_raw_store.py +17 -0
  229. topos/storage/security/encryption.py +21 -0
  230. topos/storage/signal_identity.py +71 -0
  231. topos/storage/source_settings.py +116 -0
  232. topos/storage/user_identity.py +69 -0
  233. topos/sync/__init__.py +5 -0
  234. topos/sync/client.py +272 -0
  235. topos/sync_handlers.py +70 -0
  236. topos/testing/__init__.py +1 -0
  237. topos/testing/lifespan.py +7 -0
  238. topos/uma_contact_enrichment.py +1032 -0
  239. topos/uma_filters.py +669 -0
  240. topos/uma_resource_id.py +24 -0
  241. topos/uma_rpt.py +69 -0
  242. topos/utils/base_object.py +61 -0
  243. topos/websocket_client.py +21 -0
  244. topos_node-0.1.0.dist-info/METADATA +199 -0
  245. topos_node-0.1.0.dist-info/RECORD +249 -0
  246. topos_node-0.1.0.dist-info/WHEEL +5 -0
  247. topos_node-0.1.0.dist-info/entry_points.txt +2 -0
  248. topos_node-0.1.0.dist-info/licenses/LICENSE +201 -0
  249. topos_node-0.1.0.dist-info/top_level.txt +2 -0
topos/core/logging.py ADDED
@@ -0,0 +1,175 @@
1
+ import json
2
+ import logging
3
+ import sys
4
+ from datetime import datetime
5
+ from time import time
6
+
7
+ from ..config.settings import settings
8
+
9
+ _LOG_FORMAT: str | None = None
10
+
11
+
12
+ def configure_logging() -> None:
13
+ """Configure logging to stdout based on environment/log settings."""
14
+ handler = logging.StreamHandler(sys.stdout)
15
+
16
+ def resolve_format() -> str:
17
+ if settings.log_format:
18
+ return settings.log_format.lower()
19
+ if settings.environment.lower() in {"prod", "production"}:
20
+ return "json"
21
+ return "color"
22
+
23
+ log_format = resolve_format()
24
+ if log_format == "json":
25
+ handler.setFormatter(JsonFormatter())
26
+ elif log_format == "color" and sys.stdout.isatty():
27
+ handler.setFormatter(ColorFormatter())
28
+ else:
29
+ handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
30
+
31
+ root = logging.getLogger()
32
+ # Set log level from settings
33
+ log_level_str = settings.log_level.upper()
34
+ log_level = getattr(logging, log_level_str, logging.INFO)
35
+ root.setLevel(log_level)
36
+ root.handlers = [handler]
37
+
38
+ # Suppress WebSocket ping/pong keepalive messages
39
+ # These are noisy debug logs from the websockets library
40
+ websockets_logger = logging.getLogger("websockets")
41
+ websockets_logger.setLevel(logging.INFO) # Only show INFO and above, suppress DEBUG
42
+
43
+ # transformers / huggingface_hub use urllib3; at DEBUG root these lines break tqdm/progress output
44
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
45
+ logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
46
+ # httpx/httpcore DEBUG emits per-request socket chatter; keep UMA transform logs readable.
47
+ logging.getLogger("httpx").setLevel(logging.WARNING)
48
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
49
+
50
+ global _LOG_FORMAT
51
+ _LOG_FORMAT = log_format
52
+
53
+
54
+ def log_request_timing(logger: logging.Logger, path: str, status_code: int, started_at: float) -> None:
55
+ elapsed_ms = int((time() - started_at) * 1000)
56
+ if _LOG_FORMAT == "json":
57
+ logger.info(
58
+ "request.complete",
59
+ extra={"extra": {"path": path, "status": status_code, "latency_ms": elapsed_ms}},
60
+ )
61
+ else:
62
+ logger.info(
63
+ "request.complete path=%s status=%s latency_ms=%s",
64
+ path,
65
+ status_code,
66
+ elapsed_ms,
67
+ )
68
+
69
+
70
+ class JsonFormatter(logging.Formatter):
71
+ def format(self, record: logging.LogRecord) -> str: # noqa: D401
72
+ payload = {
73
+ "level": record.levelname,
74
+ "logger": record.name,
75
+ "message": record.getMessage(),
76
+ "timestamp": record.created,
77
+ }
78
+ if record.exc_info:
79
+ payload["exc_info"] = self.formatException(record.exc_info)
80
+ if hasattr(record, "extra"):
81
+ payload.update(record.extra)
82
+ return json.dumps(payload)
83
+
84
+
85
+ class ColorFormatter(logging.Formatter):
86
+ """Color formatter matching Pipecat-style logs with text colors (no backgrounds)."""
87
+
88
+ _RESET = "\x1b[0m"
89
+
90
+ # Text colors for log levels (no backgrounds)
91
+ _LEVEL_COLORS = {
92
+ logging.DEBUG: "\x1b[38;5;26m", # Light blue (matching DEBUG in image)
93
+ logging.INFO: "\x1b[38;5;220m", # Light yellow/gold (matching INFO in image)
94
+ logging.WARNING: "\x1b[38;5;208m", # Orange (matching WARNING in image)
95
+ logging.ERROR: "\x1b[38;5;196m", # Red
96
+ logging.CRITICAL: "\x1b[38;5;196m", # Red
97
+ }
98
+
99
+ # Timestamp color: Forest green
100
+ _TIMESTAMP_COLOR = "\x1b[38;5;28m" # Forest green
101
+
102
+ # Separator color: Light gray
103
+ _SEPARATOR_COLOR = "\x1b[38;5;244m" # Light gray
104
+
105
+ # Class instance names: Teal/cyan-like blue-green (slightly more green/blue than debug blue)
106
+ _CLASS_NAME_COLOR = "\x1b[38;5;26m" # Teal/cyan blue-green
107
+
108
+ # Pipeline stage tags: Blue color
109
+ _PIPELINE_TAG_COLOR = "\x1b[38;5;75m" # Blue (matching user request)
110
+
111
+ # Message text: Same blue as DEBUG
112
+ _MESSAGE_COLOR = "\x1b[38;5;26m" # Light blue (same as DEBUG)
113
+
114
+ # Logger name (module path): Same teal/cyan as class names
115
+ _LOGGER_NAME_COLOR = "\x1b[38;5;75m" # Teal/cyan blue-green
116
+
117
+ _ARROW_COLOR = "\x1b[38;5;244m" # Gray for arrows
118
+
119
+ def format(self, record: logging.LogRecord) -> str: # noqa: D401
120
+ # Format timestamp: YYYY-MM-DD HH:MM:SS.ms (green)
121
+ timestamp_str = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
122
+ timestamp = f"{self._TIMESTAMP_COLOR}{timestamp_str}{self._RESET}"
123
+
124
+ # Get text color for log level (no background)
125
+ level_color = self._LEVEL_COLORS.get(record.levelno, "")
126
+ level = record.levelname
127
+ if level_color:
128
+ level = f"{level_color}{level}{self._RESET}"
129
+
130
+ # Color logger name (module path) in teal/cyan
131
+ logger_name = record.name
132
+ logger_name_colored = f"{self._LOGGER_NAME_COLOR}{logger_name}{self._RESET}"
133
+
134
+ # Get message and extract pipeline tag if present
135
+ message = record.getMessage()
136
+ import re
137
+
138
+ # Extract pipeline stage tag from message if it exists
139
+ pipeline_tag_pattern = r'\[PIPELINE:[A-Z_]+\]'
140
+ pipeline_tag_match = re.search(pipeline_tag_pattern, message)
141
+ pipeline_tag_display = ""
142
+ if pipeline_tag_match:
143
+ # Extract the tag and remove it from the message
144
+ pipeline_tag = pipeline_tag_match.group(0)
145
+ message = re.sub(pipeline_tag_pattern + r'\s*', '', message, count=1) # Remove tag and any following space
146
+ # Color the pipeline tag
147
+ pipeline_tag_display = f" {self._PIPELINE_TAG_COLOR}{pipeline_tag}{self._RESET}"
148
+
149
+ # Apply colors to message
150
+ # First, wrap the entire message in blue (default message color)
151
+ message_colored = f"{self._MESSAGE_COLOR}{message}{self._RESET}"
152
+
153
+ # Then apply special colors to specific parts (these will override the blue)
154
+ # Color class instance names (pattern: ClassName#N)
155
+ # Match class names like IngestionManager#1, Emo27Job#1, etc.
156
+ class_name_pattern = r'\b([A-Z][a-zA-Z0-9_]*#\d+)\b'
157
+ message_colored = re.sub(
158
+ class_name_pattern,
159
+ f"{self._CLASS_NAME_COLOR}\\1{self._RESET}{self._MESSAGE_COLOR}",
160
+ message_colored
161
+ )
162
+
163
+ # Color arrows (→ and ←) for pipeline flow
164
+ message_colored = re.sub(
165
+ r'([→←])',
166
+ f"{self._ARROW_COLOR}\\1{self._RESET}{self._MESSAGE_COLOR}",
167
+ message_colored
168
+ )
169
+
170
+ # Format: TIMESTAMP | LEVEL | [PIPELINE:TAG] | LOGGER: MESSAGE
171
+ separator = f"{self._SEPARATOR_COLOR}|{self._RESET}"
172
+ if pipeline_tag_display:
173
+ return f"{timestamp} {separator} {level} {separator}{pipeline_tag_display} {separator} {logger_name_colored}: {message_colored}"
174
+ else:
175
+ return f"{timestamp} {separator} {level} {separator} {logger_name_colored}: {message_colored}"
topos/core/metrics.py ADDED
@@ -0,0 +1,21 @@
1
+ """No-op metrics interface for Topos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ class MetricsClient:
9
+ """No-op metrics client for early scaffolding."""
10
+
11
+ def increment(self, name: str, value: int = 1, tags: Optional[Dict[str, str]] = None) -> None:
12
+ _ = (name, value, tags)
13
+
14
+ def gauge(self, name: str, value: float, tags: Optional[Dict[str, str]] = None) -> None:
15
+ _ = (name, value, tags)
16
+
17
+ def observe(self, name: str, value: float, tags: Optional[Dict[str, str]] = None) -> None:
18
+ _ = (name, value, tags)
19
+
20
+
21
+ metrics = MetricsClient()
@@ -0,0 +1,62 @@
1
+ """Startup banner display for Topos Control Plane."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from typing import Optional
8
+
9
+ from ..__version__ import __version__
10
+
11
+ logger = logging.getLogger("topos.core")
12
+
13
+
14
+ def print_startup_banner(
15
+ engine_mode: Optional[str] = None,
16
+ database_mode: Optional[str] = None,
17
+ sync_enabled: bool = False,
18
+ ) -> None:
19
+ """
20
+ Display a startup banner for Topos.
21
+
22
+ Args:
23
+ engine_mode: Engine mode (full, sync, etc.)
24
+ database_mode: Database mode (local, postgres, etc.)
25
+ sync_enabled: Whether sync is enabled
26
+ """
27
+ banner = """
28
+ ╔═══════════════════════════════════════════════════════════════╗
29
+ ║ ║
30
+ ║ ████████╗ ██████╗ ██████╗ ██████╗ ███████╗ ║
31
+ ║ ╚══██╔══╝██╔═══██╗██╔══██╗██╔═══██╗██╔════╝ ║
32
+ ║ ██║ ██║ ██║██████╔╝██║ ██║███████╗ ║
33
+ ║ ██║ ██║ ██║██╔═══╝ ██║ ██║╚════██║ ║
34
+ ║ ██║ ╚██████╔╝██║ ╚██████╔╝███████║ ║
35
+ ║ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ║
36
+ ║ ║
37
+ ║ Control Plane v{version:8s} ║
38
+ ║ ║
39
+ ╚═══════════════════════════════════════════════════════════════╝
40
+ """.format(version=__version__)
41
+
42
+ config_lines = []
43
+ if engine_mode:
44
+ config_lines.append(f" Mode: {engine_mode}")
45
+ if database_mode:
46
+ config_lines.append(f" Database: {database_mode}")
47
+ config_lines.append(f" Sync: {'enabled' if sync_enabled else 'disabled'}")
48
+
49
+ config_info = "\n".join(config_lines) if config_lines else ""
50
+
51
+ print(banner, file=sys.stderr)
52
+ if config_info:
53
+ print(config_info, file=sys.stderr)
54
+ print("", file=sys.stderr)
55
+
56
+ logger.info("=" * 65)
57
+ logger.info("Topos Control Plane v%s", __version__)
58
+ logger.info("=" * 65)
59
+ if config_info:
60
+ for line in config_lines:
61
+ logger.info(line)
62
+ logger.info("")