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.
- shared/__init__.py +59 -0
- shared/filtering.py +640 -0
- shared/schema_registry.py +229 -0
- topos/__init__.py +5 -0
- topos/__version__.py +6 -0
- topos/analytics/__init__.py +15 -0
- topos/analytics/duckdb_adapter.py +48 -0
- topos/analytics/messenger_communities.py +349 -0
- topos/analytics/messenger_graph.py +522 -0
- topos/analytics/messenger_labels.py +321 -0
- topos/analytics/profiles.py +22 -0
- topos/analytics/query_engine.py +64 -0
- topos/analytics/raw_queries.py +174 -0
- topos/api/__init__.py +1 -0
- topos/api/analytics.py +52 -0
- topos/api/app_registry.py +31 -0
- topos/api/backup.py +15 -0
- topos/api/compute_remote.py +175 -0
- topos/api/data_commit.py +158 -0
- topos/api/data_explorer_table_prefs.py +81 -0
- topos/api/db.py +10 -0
- topos/api/device.py +25 -0
- topos/api/enrichment.py +959 -0
- topos/api/filter_lab.py +195 -0
- topos/api/health.py +61 -0
- topos/api/ingestion_api.py +37 -0
- topos/api/ingestion_compat.py +21 -0
- topos/api/ingestion_sources.py +600 -0
- topos/api/llm.py +76 -0
- topos/api/local_mcp.py +46 -0
- topos/api/messenger_analytics.py +385 -0
- topos/api/query_api.py +13 -0
- topos/api/sanitization_ollama_config.py +64 -0
- topos/api/source_install.py +324 -0
- topos/api/sources.py +13 -0
- topos/api/sync.py +10 -0
- topos/api/ui_config.py +83 -0
- topos/api/uma_data.py +311 -0
- topos/api/usage.py +49 -0
- topos/api/user_identity.py +46 -0
- topos/app.py +239 -0
- topos/auth.py +17 -0
- topos/canonicalization/__init__.py +1 -0
- topos/canonicalization/mappers/__init__.py +22 -0
- topos/canonicalization/mappers/base.py +26 -0
- topos/canonicalization/mappers/chatgpt_mapper.py +40 -0
- topos/canonicalization/mappers/grok_mapper.py +17 -0
- topos/canonicalization/mappers/messenger_mapper.py +58 -0
- topos/canonicalization/models.py +31 -0
- topos/canonicalization/resolver.py +23 -0
- topos/cli/__init__.py +1 -0
- topos/cli/__main__.py +6 -0
- topos/cli/commands.py +132 -0
- topos/config/__init__.py +1 -0
- topos/config/sanitization_ollama.py +189 -0
- topos/config/settings.py +310 -0
- topos/contacts/__init__.py +5 -0
- topos/contacts/identity.py +24 -0
- topos/control_plane_client.py +300 -0
- topos/core/__init__.py +1 -0
- topos/core/api_models.py +128 -0
- topos/core/connection_resilience.py +99 -0
- topos/core/device_helpers.py +8 -0
- topos/core/errors.py +13 -0
- topos/core/events.py +12 -0
- topos/core/handlers.py +5625 -0
- topos/core/logging.py +175 -0
- topos/core/metrics.py +21 -0
- topos/core/startup_banner.py +62 -0
- topos/core/state.py +682 -0
- topos/core/table_layers.py +45 -0
- topos/core/types.py +13 -0
- topos/data_explorer_table_prefs.py +150 -0
- topos/engine/__init__.py +29 -0
- topos/engine/backends/__init__.py +50 -0
- topos/engine/backends/base.py +21 -0
- topos/engine/backends/huggingface.py +151 -0
- topos/engine/backends/ollama.py +181 -0
- topos/engine/backends/stub.py +22 -0
- topos/engine/engine.py +165 -0
- topos/engine/intake.py +32 -0
- topos/engine/queue_manager.py +112 -0
- topos/engine/registration.py +126 -0
- topos/engine/result_formatter.py +38 -0
- topos/engine/router.py +19 -0
- topos/engine/scoped_token.py +82 -0
- topos/engine/tasks.py +154 -0
- topos/engine/transport.py +44 -0
- topos/engine/usage_guard.py +100 -0
- topos/engine/usage_observation.py +129 -0
- topos/engine/validator.py +23 -0
- topos/enrichment/__init__.py +1 -0
- topos/enrichment/derived_tables.py +214 -0
- topos/enrichment/jobs/__init__.py +30 -0
- topos/enrichment/jobs/base.py +54 -0
- topos/enrichment/jobs/canonical/__init__.py +1 -0
- topos/enrichment/jobs/canonical/embeddings_job.py +27 -0
- topos/enrichment/jobs/canonical/emo_27_job.py +97 -0
- topos/enrichment/jobs/canonical/entities_job.py +27 -0
- topos/enrichment/jobs/canonical/sentiment_job.py +27 -0
- topos/enrichment/jobs/canonical/topics_job.py +27 -0
- topos/enrichment/jobs/raw/__init__.py +1 -0
- topos/enrichment/jobs/raw/attachments_job.py +12 -0
- topos/enrichment/jobs/raw/language_job.py +12 -0
- topos/enrichment/jobs/raw/time_normalization_job.py +12 -0
- topos/enrichment/jobs/raw/tool_calls_job.py +12 -0
- topos/enrichment/models/__init__.py +1 -0
- topos/enrichment/models/manager.py +8 -0
- topos/enrichment/models/registry.py +71 -0
- topos/enrichment/models/versioning.py +8 -0
- topos/enrichment/orchestrator.py +177 -0
- topos/enrichment/processor.py +17 -0
- topos/enrichment/progress_bar.py +122 -0
- topos/enrichment/website_classifier.py +31 -0
- topos/filter_lab/__init__.py +1 -0
- topos/filter_lab/bundles.py +300 -0
- topos/filter_lab/schema.py +86 -0
- topos/filter_lab/service.py +167 -0
- topos/filter_lab/store.py +374 -0
- topos/filter_lab/worker.py +250 -0
- topos/hosted_pool_lease.py +153 -0
- topos/ingestion/__init__.py +1 -0
- topos/ingestion/checkpoints/__init__.py +6 -0
- topos/ingestion/checkpoints/checkpoint_store.py +24 -0
- topos/ingestion/checkpoints/sqlite_checkpoint_store.py +82 -0
- topos/ingestion/ingest_helpers.py +504 -0
- topos/ingestion/jobs.py +91 -0
- topos/ingestion/local_sync.py +823 -0
- topos/ingestion/log_preview.py +21 -0
- topos/ingestion/manager.py +1100 -0
- topos/ingestion/parser.py +174 -0
- topos/ingestion/parsers/__init__.py +32 -0
- topos/ingestion/parsers/base.py +24 -0
- topos/ingestion/parsers/browser_parser.py +171 -0
- topos/ingestion/parsers/calendar_parser.py +21 -0
- topos/ingestion/parsers/chatgpt_conversation_flattener.py +266 -0
- topos/ingestion/parsers/chatgpt_parser.py +67 -0
- topos/ingestion/parsers/grok_parser.py +21 -0
- topos/ingestion/parsers/messenger_parser.py +97 -0
- topos/ingestion/progress.py +54 -0
- topos/ingestion/sources/__init__.py +20 -0
- topos/ingestion/sources/base.py +39 -0
- topos/ingestion/sources/calendar.py +29 -0
- topos/ingestion/sources/chatgpt.py +29 -0
- topos/ingestion/sources/contact_importers.py +274 -0
- topos/ingestion/sources/grok.py +29 -0
- topos/ingestion/sources/imessage_reader.py +479 -0
- topos/ingestion/sources/signal_export_parser.py +132 -0
- topos/ingestion/sources/signal_reader.py +491 -0
- topos/ingestion/state_machine.py +70 -0
- topos/ingestion/triggers/__init__.py +1 -0
- topos/ingestion/triggers/file_trigger.py +36 -0
- topos/ingestion/triggers/sqlite_trigger.py +18 -0
- topos/ingestion/validation/__init__.py +1 -0
- topos/ingestion/validation/base.py +27 -0
- topos/ingestion/validation/schema_registry.py +111 -0
- topos/ingestion/validation/schema_validator.py +13 -0
- topos/lineage/__init__.py +1 -0
- topos/lineage/provenance.py +9 -0
- topos/lineage/tracker.py +9 -0
- topos/mcp_stdio_proxy.py +83 -0
- topos/observability/__init__.py +1 -0
- topos/observability/alerts.py +7 -0
- topos/observability/metrics.py +25 -0
- topos/observability/tracing.py +18 -0
- topos/openai_client.py +69 -0
- topos/projections/__init__.py +1 -0
- topos/projections/vector_index/__init__.py +1 -0
- topos/projections/vector_index/base.py +21 -0
- topos/projections/vector_index/builders.py +11 -0
- topos/projections/vector_index/health_checks.py +5 -0
- topos/rate_limit.py +43 -0
- topos/sanitization/__init__.py +16 -0
- topos/sanitization/ollama_transforms.py +276 -0
- topos/scope_resolution.py +89 -0
- topos/services/__init__.py +1 -0
- topos/services/container.py +46 -0
- topos/services/embeddings/__init__.py +1 -0
- topos/services/embeddings/base.py +7 -0
- topos/services/embeddings/local.py +9 -0
- topos/services/embeddings/remote.py +9 -0
- topos/services/interfaces.py +40 -0
- topos/services/llm/__init__.py +1 -0
- topos/services/llm/base.py +7 -0
- topos/services/llm/openai.py +126 -0
- topos/services/local.py +123 -0
- topos/services/postgres.py +385 -0
- topos/sources/__init__.py +6 -0
- topos/sources/definitions.py +114 -0
- topos/sources/install_service.py +836 -0
- topos/sources/registry.py +263 -0
- topos/sources/runtime_install.py +427 -0
- topos/storage/__init__.py +1 -0
- topos/storage/canonical/__init__.py +18 -0
- topos/storage/canonical/ai_chat/__init__.py +22 -0
- topos/storage/canonical/ai_chat/canonicalizer.py +147 -0
- topos/storage/canonical/ai_chat/mapper.py +168 -0
- topos/storage/canonical/ai_chat/model.py +87 -0
- topos/storage/canonical/ai_chat/tables.py +179 -0
- topos/storage/canonical/canonical_store.py +24 -0
- topos/storage/canonical/conversations_tables.py +1020 -0
- topos/storage/canonical/mapping_store.py +30 -0
- topos/storage/canonical/postgres.py +10 -0
- topos/storage/db/__init__.py +1 -0
- topos/storage/db/client.py +8 -0
- topos/storage/db/migrations/__init__.py +1 -0
- topos/storage/db/migrations/stage9_column_renames.py +78 -0
- topos/storage/db/paths.py +122 -0
- topos/storage/db/postgres.py +240 -0
- topos/storage/db/schema.py +6 -0
- topos/storage/enrichment/__init__.py +1 -0
- topos/storage/enrichment/canonical_enrichment_store.py +7 -0
- topos/storage/enrichment/raw_enrichment_store.py +18 -0
- topos/storage/normalized/__init__.py +1 -0
- topos/storage/normalized/normalized_store.py +24 -0
- topos/storage/oplog/__init__.py +1 -0
- topos/storage/oplog/decision.py +6 -0
- topos/storage/oplog/oplog_store.py +17 -0
- topos/storage/oplog/postgres.py +10 -0
- topos/storage/projections/__init__.py +1 -0
- topos/storage/projections/index_ops_store.py +6 -0
- topos/storage/projections/vector_index_store.py +6 -0
- topos/storage/raw/__init__.py +1 -0
- topos/storage/raw/browser_flat_tables.py +303 -0
- topos/storage/raw/file_store.py +100 -0
- topos/storage/raw/raw_store.py +29 -0
- topos/storage/raw/raw_tables_manager.py +295 -0
- topos/storage/raw/sqlite_raw_store.py +17 -0
- topos/storage/security/encryption.py +21 -0
- topos/storage/signal_identity.py +71 -0
- topos/storage/source_settings.py +116 -0
- topos/storage/user_identity.py +69 -0
- topos/sync/__init__.py +5 -0
- topos/sync/client.py +272 -0
- topos/sync_handlers.py +70 -0
- topos/testing/__init__.py +1 -0
- topos/testing/lifespan.py +7 -0
- topos/uma_contact_enrichment.py +1032 -0
- topos/uma_filters.py +669 -0
- topos/uma_resource_id.py +24 -0
- topos/uma_rpt.py +69 -0
- topos/utils/base_object.py +61 -0
- topos/websocket_client.py +21 -0
- topos_node-0.1.0.dist-info/METADATA +199 -0
- topos_node-0.1.0.dist-info/RECORD +249 -0
- topos_node-0.1.0.dist-info/WHEEL +5 -0
- topos_node-0.1.0.dist-info/entry_points.txt +2 -0
- topos_node-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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
|
topos/api/data_commit.py
ADDED
|
@@ -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
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)
|