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
@@ -0,0 +1,175 @@
1
+ """Phase B remote compute transport endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Dict, Literal, Optional
9
+
10
+ from fastapi import APIRouter, Depends, HTTPException, status
11
+ from pydantic import BaseModel, Field
12
+
13
+ from ..auth import require_api_key
14
+ from ..core.state import get_db_connection
15
+ from ..engine.engine import Engine
16
+ from ..engine.tasks import ModelRequest, ProcessingTask
17
+
18
+ router = APIRouter(tags=["compute-remote"])
19
+
20
+
21
+ def _now_iso() -> str:
22
+ return datetime.now(timezone.utc).isoformat()
23
+
24
+
25
+ def _ensure_tables(conn) -> None:
26
+ conn.execute(
27
+ """
28
+ CREATE TABLE IF NOT EXISTS remote_compute_results (
29
+ idempotency_key TEXT PRIMARY KEY,
30
+ compute_task_id TEXT NOT NULL,
31
+ response_json TEXT NOT NULL,
32
+ requested_tier TEXT NOT NULL,
33
+ correlation_id TEXT,
34
+ created_at TEXT NOT NULL
35
+ )
36
+ """
37
+ )
38
+ conn.execute(
39
+ """
40
+ CREATE TABLE IF NOT EXISTS remote_compute_audit (
41
+ audit_id TEXT PRIMARY KEY,
42
+ compute_task_id TEXT NOT NULL,
43
+ idempotency_key TEXT,
44
+ principal TEXT NOT NULL,
45
+ requested_tier TEXT NOT NULL,
46
+ outcome TEXT NOT NULL,
47
+ created_at TEXT NOT NULL
48
+ )
49
+ """
50
+ )
51
+ conn.commit()
52
+
53
+
54
+ class RemoteComputeTaskRequest(BaseModel):
55
+ compute_task_id: str = Field(..., min_length=1)
56
+ topos_id: str = Field(..., min_length=1)
57
+ resource_id: str = Field(..., min_length=1)
58
+ operation: str = Field(..., min_length=1)
59
+ payload: Dict[str, Any] = Field(default_factory=dict)
60
+ idempotency_key: Optional[str] = Field(default=None, min_length=8, max_length=200)
61
+ correlation_id: Optional[str] = Field(default=None, min_length=8, max_length=200)
62
+ requested_tier: Literal["basic", "pro", "premium"] = "basic"
63
+
64
+
65
+ @router.post("/v1/compute/run", dependencies=[Depends(require_api_key)])
66
+ async def remote_compute_run(body: RemoteComputeTaskRequest) -> Dict[str, Any]:
67
+ conn = get_db_connection()
68
+ if conn is None:
69
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database not available")
70
+ _ensure_tables(conn)
71
+ principal = "control-plane"
72
+ if body.idempotency_key:
73
+ row = conn.execute(
74
+ "SELECT response_json FROM remote_compute_results WHERE idempotency_key = ?",
75
+ (body.idempotency_key,),
76
+ ).fetchone()
77
+ if row:
78
+ cached = json.loads(str(row["response_json"]))
79
+ conn.execute(
80
+ """
81
+ INSERT INTO remote_compute_audit
82
+ (audit_id, compute_task_id, idempotency_key, principal, requested_tier, outcome, created_at)
83
+ VALUES (?, ?, ?, ?, ?, ?, ?)
84
+ """,
85
+ (
86
+ f"rca_{uuid.uuid4().hex}",
87
+ body.compute_task_id,
88
+ body.idempotency_key,
89
+ principal,
90
+ body.requested_tier,
91
+ "idempotent_replay",
92
+ _now_iso(),
93
+ ),
94
+ )
95
+ conn.commit()
96
+ cached["idempotent_replay"] = True
97
+ return cached
98
+
99
+ try:
100
+ if body.operation == "healthcheck":
101
+ response = {
102
+ "compute_task_id": body.compute_task_id,
103
+ "status": "completed",
104
+ "result": {"status": "ok"},
105
+ "error_class": None,
106
+ "requested_tier": body.requested_tier,
107
+ "correlation_id": body.correlation_id,
108
+ "idempotent_replay": False,
109
+ }
110
+ else:
111
+ task = ProcessingTask(
112
+ id=body.compute_task_id,
113
+ type=body.operation,
114
+ input=body.payload,
115
+ model_request=ModelRequest(
116
+ provider=str(body.payload.get("provider") or "huggingface"),
117
+ model=str(body.payload.get("model") or "") or None,
118
+ ),
119
+ )
120
+ result = Engine().run(task)
121
+ response = {
122
+ "compute_task_id": body.compute_task_id,
123
+ "status": result.status,
124
+ "result": result.model_dump(mode="json"),
125
+ "error_class": None if result.status == "completed" else "runtime_failure",
126
+ "requested_tier": body.requested_tier,
127
+ "correlation_id": body.correlation_id,
128
+ "idempotent_replay": False,
129
+ }
130
+ except Exception as exc: # noqa: BLE001
131
+ response = {
132
+ "compute_task_id": body.compute_task_id,
133
+ "status": "failed",
134
+ "result": {},
135
+ "error_class": "runtime_failure",
136
+ "error_message": str(exc),
137
+ "requested_tier": body.requested_tier,
138
+ "correlation_id": body.correlation_id,
139
+ "idempotent_replay": False,
140
+ }
141
+
142
+ if body.idempotency_key:
143
+ conn.execute(
144
+ """
145
+ INSERT OR REPLACE INTO remote_compute_results
146
+ (idempotency_key, compute_task_id, response_json, requested_tier, correlation_id, created_at)
147
+ VALUES (?, ?, ?, ?, ?, ?)
148
+ """,
149
+ (
150
+ body.idempotency_key,
151
+ body.compute_task_id,
152
+ json.dumps(response),
153
+ body.requested_tier,
154
+ body.correlation_id,
155
+ _now_iso(),
156
+ ),
157
+ )
158
+ conn.execute(
159
+ """
160
+ INSERT INTO remote_compute_audit
161
+ (audit_id, compute_task_id, idempotency_key, principal, requested_tier, outcome, created_at)
162
+ VALUES (?, ?, ?, ?, ?, ?, ?)
163
+ """,
164
+ (
165
+ f"rca_{uuid.uuid4().hex}",
166
+ body.compute_task_id,
167
+ body.idempotency_key,
168
+ principal,
169
+ body.requested_tier,
170
+ str(response.get("status") or "unknown"),
171
+ _now_iso(),
172
+ ),
173
+ )
174
+ conn.commit()
175
+ return response
@@ -0,0 +1,158 @@
1
+ """Phase B authoritative data-plane commit API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Dict, Optional
9
+
10
+ from fastapi import APIRouter, Depends, HTTPException, status
11
+ from pydantic import BaseModel, Field
12
+
13
+ from ..auth import require_api_key
14
+ from ..core.state import get_db_connection
15
+
16
+ router = APIRouter(tags=["data-commit"])
17
+
18
+
19
+ def _now_iso() -> str:
20
+ return datetime.now(timezone.utc).isoformat()
21
+
22
+
23
+ def _ensure_tables(conn) -> None:
24
+ conn.execute(
25
+ """
26
+ CREATE TABLE IF NOT EXISTS data_commit_artifacts (
27
+ commit_id TEXT PRIMARY KEY,
28
+ compute_task_id TEXT NOT NULL,
29
+ topos_id TEXT NOT NULL,
30
+ resource_id TEXT NOT NULL,
31
+ artifact_schema_version TEXT NOT NULL,
32
+ artifact_json TEXT NOT NULL,
33
+ idempotency_key TEXT UNIQUE,
34
+ correlation_id TEXT,
35
+ requested_by TEXT,
36
+ created_at TEXT NOT NULL
37
+ )
38
+ """
39
+ )
40
+ conn.execute(
41
+ """
42
+ CREATE TABLE IF NOT EXISTS data_commit_audit (
43
+ audit_id TEXT PRIMARY KEY,
44
+ commit_id TEXT,
45
+ compute_task_id TEXT NOT NULL,
46
+ idempotency_key TEXT,
47
+ outcome TEXT NOT NULL,
48
+ created_at TEXT NOT NULL
49
+ )
50
+ """
51
+ )
52
+ conn.commit()
53
+
54
+
55
+ class CommitArtifactRequest(BaseModel):
56
+ compute_task_id: str = Field(..., min_length=1)
57
+ topos_id: str = Field(..., min_length=1)
58
+ resource_id: str = Field(..., min_length=1)
59
+ artifact: Dict[str, Any] = Field(default_factory=dict)
60
+ artifact_schema_version: str = Field(default="phase_b_v1", min_length=1)
61
+ idempotency_key: Optional[str] = Field(default=None, min_length=8, max_length=200)
62
+ correlation_id: Optional[str] = Field(default=None, min_length=8, max_length=200)
63
+ requested_by: Optional[str] = Field(default=None, min_length=1, max_length=200)
64
+
65
+
66
+ @router.post("/v1/data/commit-artifact", dependencies=[Depends(require_api_key)])
67
+ async def commit_artifact(body: CommitArtifactRequest) -> Dict[str, Any]:
68
+ if not isinstance(body.artifact, dict) or not body.artifact:
69
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="artifact must be a non-empty object")
70
+ conn = get_db_connection()
71
+ if conn is None:
72
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database not available")
73
+ _ensure_tables(conn)
74
+ commit_id = f"commit_{uuid.uuid4().hex}"
75
+ now = _now_iso()
76
+ try:
77
+ conn.execute("BEGIN IMMEDIATE")
78
+ if body.idempotency_key:
79
+ existing = conn.execute(
80
+ """
81
+ SELECT commit_id, correlation_id FROM data_commit_artifacts
82
+ WHERE idempotency_key = ?
83
+ LIMIT 1
84
+ """,
85
+ (body.idempotency_key,),
86
+ ).fetchone()
87
+ if existing:
88
+ existing_commit_id = str(existing["commit_id"])
89
+ conn.execute(
90
+ """
91
+ INSERT INTO data_commit_audit
92
+ (audit_id, commit_id, compute_task_id, idempotency_key, outcome, created_at)
93
+ VALUES (?, ?, ?, ?, ?, ?)
94
+ """,
95
+ (
96
+ f"dca_{uuid.uuid4().hex}",
97
+ existing_commit_id,
98
+ body.compute_task_id,
99
+ body.idempotency_key,
100
+ "idempotent_replay",
101
+ now,
102
+ ),
103
+ )
104
+ conn.commit()
105
+ return {
106
+ "commit_id": existing_commit_id,
107
+ "deduped": True,
108
+ "compute_task_id": body.compute_task_id,
109
+ "correlation_id": str(existing["correlation_id"] or body.correlation_id or ""),
110
+ }
111
+
112
+ conn.execute(
113
+ """
114
+ INSERT INTO data_commit_artifacts
115
+ (commit_id, compute_task_id, topos_id, resource_id, artifact_schema_version, artifact_json, idempotency_key, correlation_id, requested_by, created_at)
116
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
117
+ """,
118
+ (
119
+ commit_id,
120
+ body.compute_task_id,
121
+ body.topos_id,
122
+ body.resource_id,
123
+ body.artifact_schema_version,
124
+ json.dumps(body.artifact),
125
+ body.idempotency_key,
126
+ body.correlation_id,
127
+ body.requested_by,
128
+ now,
129
+ ),
130
+ )
131
+ conn.execute(
132
+ """
133
+ INSERT INTO data_commit_audit
134
+ (audit_id, commit_id, compute_task_id, idempotency_key, outcome, created_at)
135
+ VALUES (?, ?, ?, ?, ?, ?)
136
+ """,
137
+ (
138
+ f"dca_{uuid.uuid4().hex}",
139
+ commit_id,
140
+ body.compute_task_id,
141
+ body.idempotency_key,
142
+ "committed",
143
+ now,
144
+ ),
145
+ )
146
+ conn.commit()
147
+ except Exception as exc: # noqa: BLE001
148
+ conn.rollback()
149
+ raise HTTPException(
150
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
151
+ detail=f"commit_failed:{exc}",
152
+ ) from exc
153
+ return {
154
+ "commit_id": commit_id,
155
+ "deduped": False,
156
+ "compute_task_id": body.compute_task_id,
157
+ "correlation_id": body.correlation_id,
158
+ }
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
6
+
7
+ from ..auth import require_api_key
8
+ from ..core.state import get_db_connection
9
+ from ..data_explorer_table_prefs import (
10
+ delete_table_prefs,
11
+ get_table_prefs,
12
+ put_table_prefs,
13
+ )
14
+
15
+ router = APIRouter(tags=["data-explorer-table-prefs"])
16
+
17
+
18
+ def _map_validation_error(err: ValueError) -> HTTPException:
19
+ code = str(err)
20
+ mapping = {
21
+ "INVALID_PREFS": (status.HTTP_400_BAD_REQUEST, "Invalid table preferences payload."),
22
+ "INVALID_SORT": (status.HTTP_400_BAD_REQUEST, "Invalid sort state."),
23
+ "INVALID_TABLE_NAME": (status.HTTP_400_BAD_REQUEST, "Invalid table name."),
24
+ "INVALID_USER_ID": (status.HTTP_400_BAD_REQUEST, "Invalid user id."),
25
+ "PREFS_TOO_LARGE": (status.HTTP_400_BAD_REQUEST, "Table preferences exceed maximum size."),
26
+ }
27
+ status_code, message = mapping.get(code, (status.HTTP_400_BAD_REQUEST, "Invalid request."))
28
+ return HTTPException(status_code=status_code, detail={"error": message, "code": code})
29
+
30
+
31
+ @router.get("/v1/data-explorer/table-prefs/{table_name}", dependencies=[Depends(require_api_key)])
32
+ async def read_data_explorer_table_prefs(
33
+ table_name: str,
34
+ user_id: str = Query(..., min_length=1),
35
+ ) -> dict[str, Any]:
36
+ conn = get_db_connection()
37
+ if not conn:
38
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database not available")
39
+ try:
40
+ prefs = get_table_prefs(conn, user_id=user_id, table_name=table_name)
41
+ except ValueError as exc:
42
+ raise _map_validation_error(exc) from exc
43
+ return {"status": "ok", "prefs": prefs}
44
+
45
+
46
+ @router.put("/v1/data-explorer/table-prefs/{table_name}", dependencies=[Depends(require_api_key)])
47
+ async def write_data_explorer_table_prefs(
48
+ table_name: str,
49
+ body: dict[str, Any] = Body(default=None),
50
+ ) -> dict[str, Any]:
51
+ conn = get_db_connection()
52
+ if not conn:
53
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database not available")
54
+ payload = body or {}
55
+ user_id = str(payload.get("user_id") or "").strip()
56
+ prefs = payload.get("prefs")
57
+ if not user_id:
58
+ raise HTTPException(
59
+ status_code=status.HTTP_400_BAD_REQUEST,
60
+ detail={"error": "user_id is required", "code": "INVALID_USER_ID"},
61
+ )
62
+ try:
63
+ saved = put_table_prefs(conn, user_id=user_id, table_name=table_name, prefs=prefs or {})
64
+ except ValueError as exc:
65
+ raise _map_validation_error(exc) from exc
66
+ return {"status": "ok", "prefs": saved}
67
+
68
+
69
+ @router.delete("/v1/data-explorer/table-prefs/{table_name}", dependencies=[Depends(require_api_key)])
70
+ async def remove_data_explorer_table_prefs(
71
+ table_name: str,
72
+ user_id: str = Query(..., min_length=1),
73
+ ) -> dict[str, Any]:
74
+ conn = get_db_connection()
75
+ if not conn:
76
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database not available")
77
+ try:
78
+ deleted = delete_table_prefs(conn, user_id=user_id, table_name=table_name)
79
+ except ValueError as exc:
80
+ raise _map_validation_error(exc) from exc
81
+ return {"status": "ok", "deleted": deleted}
topos/api/db.py ADDED
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/db/status")
9
+ async def db_status() -> dict:
10
+ return {"status": "stub", "database": "unknown"}
topos/api/device.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, Depends
4
+
5
+ from ..auth import require_api_key
6
+ from ..core.api_models import DeviceInfoResponse, DeviceNameRequest, DeviceNameResponse
7
+ from ..services.container import Services, get_services
8
+
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.get("/device/info", response_model=DeviceInfoResponse, dependencies=[Depends(require_api_key)])
14
+ async def get_device_info(services: Services = Depends(get_services)):
15
+ return await services.device.get_device_info()
16
+
17
+
18
+ @router.get("/device_info", response_model=DeviceInfoResponse, dependencies=[Depends(require_api_key)])
19
+ async def get_device_info_alias(services: Services = Depends(get_services)):
20
+ return await services.device.get_device_info()
21
+
22
+
23
+ @router.post("/device_name", response_model=DeviceNameResponse, dependencies=[Depends(require_api_key)])
24
+ async def set_device_name(body: DeviceNameRequest, services: Services = Depends(get_services)):
25
+ return await services.device.set_device_name(body.device_name)