topos-node 0.1.7__tar.gz → 0.1.9__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.
- {topos_node-0.1.7 → topos_node-0.1.9}/PKG-INFO +1 -1
- {topos_node-0.1.7 → topos_node-0.1.9}/pyproject.toml +1 -1
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/__version__.py +2 -2
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/device.py +2 -2
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/source_install.py +30 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/app.py +9 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/cli/commands.py +30 -64
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/handlers.py +26 -1
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/logging.py +7 -2
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/state.py +58 -0
- topos_node-0.1.9/topos/runtime_update.py +246 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/interfaces.py +1 -1
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/local.py +2 -1
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/postgres.py +68 -5
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sources/install_service.py +95 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sync/client.py +3 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos_node.egg-info/PKG-INFO +1 -1
- {topos_node-0.1.7 → topos_node-0.1.9}/topos_node.egg-info/SOURCES.txt +1 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/LICENSE +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/README.md +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/setup.cfg +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/shared/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/shared/filtering.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/shared/schema_registry.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/duckdb_adapter.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/messenger_communities.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/messenger_graph.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/messenger_labels.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/profiles.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/query_engine.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/analytics/raw_queries.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/analytics.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/app_registry.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/backup.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/compute_remote.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/data_commit.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/data_explorer_table_prefs.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/db.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/enrichment.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/filter_lab.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/health.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/ingestion_api.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/ingestion_compat.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/ingestion_sources.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/llm.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/local_mcp.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/messenger_analytics.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/query_api.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/sanitization_ollama_config.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/sources.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/sync.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/ui_config.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/uma_data.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/usage.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/api/user_identity.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/auth.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/mappers/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/mappers/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/mappers/chatgpt_mapper.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/mappers/grok_mapper.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/mappers/messenger_mapper.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/models.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/canonicalization/resolver.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/cli/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/cli/__main__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/config/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/config/sanitization_ollama.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/config/settings.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/contacts/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/contacts/identity.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/control_plane_client.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/api_models.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/connection_resilience.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/device_helpers.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/errors.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/events.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/metrics.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/startup_banner.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/table_layers.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/core/types.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/data_explorer_table_prefs.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/backends/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/backends/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/backends/huggingface.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/backends/ollama.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/backends/stub.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/engine.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/intake.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/queue_manager.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/registration.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/result_formatter.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/router.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/scoped_token.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/tasks.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/transport.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/usage_guard.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/usage_observation.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/engine/validator.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/derived_tables.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/embeddings_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/emo_27_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/entities_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/sentiment_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/topics_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/raw/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/raw/attachments_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/raw/language_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/raw/time_normalization_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/jobs/raw/tool_calls_job.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/models/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/models/manager.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/models/registry.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/models/versioning.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/orchestrator.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/processor.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/progress_bar.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/enrichment/website_classifier.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/filter_lab/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/filter_lab/bundles.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/filter_lab/schema.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/filter_lab/service.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/filter_lab/store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/filter_lab/worker.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/hosted_pool_lease.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/checkpoints/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/checkpoints/checkpoint_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/checkpoints/sqlite_checkpoint_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/ingest_helpers.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/jobs.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/local_sync.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/log_preview.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/manager.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parser.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/browser_parser.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/calendar_parser.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/chatgpt_conversation_flattener.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/chatgpt_parser.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/grok_parser.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/parsers/messenger_parser.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/progress.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/calendar.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/chatgpt.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/contact_importers.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/grok.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/imessage_reader.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/signal_export_parser.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/sources/signal_reader.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/state_machine.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/triggers/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/triggers/file_trigger.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/triggers/sqlite_trigger.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/validation/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/validation/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/validation/schema_registry.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/ingestion/validation/schema_validator.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/lineage/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/lineage/provenance.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/lineage/tracker.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/mcp_stdio_proxy.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/observability/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/observability/alerts.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/observability/metrics.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/observability/tracing.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/openai_client.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/projections/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/projections/vector_index/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/projections/vector_index/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/projections/vector_index/builders.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/projections/vector_index/health_checks.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/rate_limit.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sanitization/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sanitization/ollama_transforms.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/scope_resolution.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/container.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/embeddings/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/embeddings/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/embeddings/local.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/embeddings/remote.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/llm/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/llm/base.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/services/llm/openai.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sources/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sources/definitions.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sources/registry.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sources/runtime_install.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/startup_banner.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/canonicalizer.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/mapper.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/model.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/tables.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/canonical_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/conversations_tables.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/mapping_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/canonical/postgres.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/db/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/db/client.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/db/migrations/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/db/migrations/stage9_column_renames.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/db/paths.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/db/postgres.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/db/schema.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/enrichment/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/enrichment/canonical_enrichment_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/enrichment/raw_enrichment_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/normalized/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/normalized/normalized_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/oplog/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/oplog/decision.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/oplog/oplog_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/oplog/postgres.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/projections/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/projections/index_ops_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/projections/vector_index_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/raw/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/raw/browser_flat_tables.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/raw/file_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/raw/raw_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/raw/raw_tables_manager.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/raw/sqlite_raw_store.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/security/encryption.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/signal_identity.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/source_settings.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/storage/user_identity.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sync/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/sync_handlers.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/testing/__init__.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/testing/lifespan.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/uma_contact_enrichment.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/uma_filters.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/uma_resource_id.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/uma_rpt.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/utils/base_object.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos/websocket_client.py +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos_node.egg-info/dependency_links.txt +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos_node.egg-info/entry_points.txt +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos_node.egg-info/requires.txt +0 -0
- {topos_node-0.1.7 → topos_node-0.1.9}/topos_node.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "topos-node"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "Topos personal AI engine (FastAPI): local data, sync, control plane WebSocket client"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -12,12 +12,12 @@ router = APIRouter()
|
|
|
12
12
|
|
|
13
13
|
@router.get("/device/info", response_model=DeviceInfoResponse, dependencies=[Depends(require_api_key)])
|
|
14
14
|
async def get_device_info(services: Services = Depends(get_services)):
|
|
15
|
-
return await services.device.get_device_info()
|
|
15
|
+
return await services.device.get_device_info(context=None)
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@router.get("/device_info", response_model=DeviceInfoResponse, dependencies=[Depends(require_api_key)])
|
|
19
19
|
async def get_device_info_alias(services: Services = Depends(get_services)):
|
|
20
|
-
return await services.device.get_device_info()
|
|
20
|
+
return await services.device.get_device_info(context=None)
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@router.post("/device_name", response_model=DeviceNameResponse, dependencies=[Depends(require_api_key)])
|
|
@@ -145,6 +145,21 @@ async def _list_sources_core(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
145
145
|
return {"status": "ok", "sources": sources}
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
async def _patch_source_install_core(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
149
|
+
source_id = str(payload.get("source_id") or "").strip()
|
|
150
|
+
if not source_id:
|
|
151
|
+
raise ValueError("source_id is required")
|
|
152
|
+
partial = payload.get("source_definition_json")
|
|
153
|
+
if not isinstance(partial, dict):
|
|
154
|
+
raise ValueError("source_definition_json object is required")
|
|
155
|
+
record = install_service.patch_source_install(
|
|
156
|
+
source_id=source_id,
|
|
157
|
+
scope=_scope_from_payload(payload),
|
|
158
|
+
source_definition_json=partial,
|
|
159
|
+
)
|
|
160
|
+
return {"status": "ok", "install": record.to_dict()}
|
|
161
|
+
|
|
162
|
+
|
|
148
163
|
async def _uninstall_source_core(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
149
164
|
source_id = str(payload.get("source_id") or "").strip()
|
|
150
165
|
if not source_id:
|
|
@@ -266,6 +281,21 @@ async def source_install_status(
|
|
|
266
281
|
return _ok_envelope(request_id, result)
|
|
267
282
|
|
|
268
283
|
|
|
284
|
+
@router.patch("/source-install", dependencies=[Depends(require_api_key)])
|
|
285
|
+
async def patch_source_install(payload: Dict[str, Any] = Body(default_factory=dict)) -> Dict[str, Any]:
|
|
286
|
+
request_id = str(uuid.uuid4())
|
|
287
|
+
_log_request("patch_source_install", request_id, payload)
|
|
288
|
+
try:
|
|
289
|
+
result = await _patch_source_install_core(payload)
|
|
290
|
+
return _ok_envelope(request_id, result)
|
|
291
|
+
except ValueError as exc:
|
|
292
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
293
|
+
except RuntimeError as exc:
|
|
294
|
+
raise HTTPException(status_code=503, detail=str(exc))
|
|
295
|
+
except Exception as exc: # noqa: BLE001
|
|
296
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
|
297
|
+
|
|
298
|
+
|
|
269
299
|
@router.delete("/source-install", dependencies=[Depends(require_api_key)])
|
|
270
300
|
async def uninstall_source(payload: Dict[str, Any] = Body(default_factory=dict)) -> Dict[str, Any]:
|
|
271
301
|
request_id = str(uuid.uuid4())
|
|
@@ -46,6 +46,11 @@ from .control_plane_client import ControlPlaneClient
|
|
|
46
46
|
from .engine.registration import build_engine_heartbeat_message, build_engine_register_message
|
|
47
47
|
from .hosted_pool_lease import HostedPoolLeaseClient
|
|
48
48
|
from .services.container import get_services
|
|
49
|
+
from .runtime_update import (
|
|
50
|
+
start_runtime_update_monitor,
|
|
51
|
+
start_update_hotkey_listener,
|
|
52
|
+
stop_runtime_update_monitor,
|
|
53
|
+
)
|
|
49
54
|
from .startup_banner import emit_startup_banner
|
|
50
55
|
from .sync import SyncClient
|
|
51
56
|
from .sync_handlers import handle_sync_op
|
|
@@ -206,6 +211,9 @@ async def startup_event() -> None:
|
|
|
206
211
|
await state.control_plane_client.send_message(build_engine_heartbeat_message())
|
|
207
212
|
|
|
208
213
|
state.engine_presence_task = asyncio.create_task(_presence_loop())
|
|
214
|
+
start_runtime_update_monitor()
|
|
215
|
+
start_update_hotkey_listener()
|
|
216
|
+
|
|
209
217
|
if settings.enable_sync and settings.topos_user_id:
|
|
210
218
|
state.sync_client = SyncClient(
|
|
211
219
|
sync_url=settings.get_sync_url(),
|
|
@@ -229,6 +237,7 @@ async def startup_event() -> None:
|
|
|
229
237
|
|
|
230
238
|
@app.on_event("shutdown")
|
|
231
239
|
async def shutdown_event() -> None:
|
|
240
|
+
await stop_runtime_update_monitor()
|
|
232
241
|
if state.engine_presence_task:
|
|
233
242
|
state.engine_presence_task.cancel()
|
|
234
243
|
try:
|
|
@@ -2,28 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
import os
|
|
7
|
-
import subprocess
|
|
8
6
|
import sys
|
|
9
|
-
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
10
7
|
from pathlib import Path
|
|
11
|
-
from urllib.error import URLError
|
|
12
|
-
from urllib.request import urlopen
|
|
13
8
|
|
|
14
9
|
import click
|
|
15
10
|
import uvicorn
|
|
16
|
-
from packaging.version import InvalidVersion, Version
|
|
17
11
|
|
|
18
12
|
# Add parent directory to path for imports
|
|
19
13
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
20
14
|
|
|
15
|
+
from topos.runtime_update import (
|
|
16
|
+
DEFAULT_PACKAGE_NAME,
|
|
17
|
+
apply_package_update,
|
|
18
|
+
can_prompt_for_input,
|
|
19
|
+
check_for_update,
|
|
20
|
+
get_installed_package_version,
|
|
21
|
+
get_latest_pypi_version,
|
|
22
|
+
get_module_version,
|
|
23
|
+
should_skip_update_check,
|
|
24
|
+
)
|
|
21
25
|
from topos.storage.db.paths import discover_databases
|
|
22
26
|
from topos.startup_banner import emit_startup_banner
|
|
23
27
|
|
|
24
28
|
USER_ENV_PATH = Path.home() / ".topos" / ".env"
|
|
25
29
|
LEGACY_ENV_PATH = Path(__file__).resolve().parent.parent / ".env"
|
|
26
30
|
|
|
31
|
+
# Re-exported for tests that monkeypatch commands.*
|
|
32
|
+
_get_installed_package_version = get_installed_package_version
|
|
33
|
+
_get_latest_pypi_version = get_latest_pypi_version
|
|
34
|
+
_get_module_version = get_module_version
|
|
35
|
+
_can_prompt_for_input = can_prompt_for_input
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_runtime_version(package_name: str = DEFAULT_PACKAGE_NAME) -> str:
|
|
39
|
+
return _get_module_version() or _get_installed_package_version(package_name) or "unknown"
|
|
40
|
+
|
|
27
41
|
|
|
28
42
|
def _load_env_file(env_path: Path) -> None:
|
|
29
43
|
if not env_path.exists():
|
|
@@ -69,10 +83,6 @@ def _save_topos_key(topos_key: str, env_path: Path = USER_ENV_PATH) -> Path:
|
|
|
69
83
|
return env_path
|
|
70
84
|
|
|
71
85
|
|
|
72
|
-
def _can_prompt_for_input() -> bool:
|
|
73
|
-
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
74
|
-
|
|
75
|
-
|
|
76
86
|
def _prompt_for_topos_key() -> str:
|
|
77
87
|
click.echo("TOPOS_KEY is required to connect Topos Node.")
|
|
78
88
|
click.echo("Enter your TOPOS_KEY to save it locally and continue.")
|
|
@@ -107,26 +117,7 @@ def _resolve_topos_key(cli_topos_key: str | None, env_path: Path = USER_ENV_PATH
|
|
|
107
117
|
)
|
|
108
118
|
|
|
109
119
|
|
|
110
|
-
def
|
|
111
|
-
try:
|
|
112
|
-
return package_version(package_name)
|
|
113
|
-
except PackageNotFoundError:
|
|
114
|
-
return None
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _get_module_version() -> str | None:
|
|
118
|
-
try:
|
|
119
|
-
from topos.__version__ import __version__
|
|
120
|
-
except Exception:
|
|
121
|
-
return None
|
|
122
|
-
return __version__ or None
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _get_runtime_version(package_name: str = "topos-node") -> str:
|
|
126
|
-
return _get_module_version() or _get_installed_package_version(package_name) or "unknown"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _emit_startup_banner(host: str, port: int, package_name: str = "topos-node") -> None:
|
|
120
|
+
def _emit_startup_banner(host: str, port: int, package_name: str = DEFAULT_PACKAGE_NAME) -> None:
|
|
130
121
|
runtime_version = _get_runtime_version(package_name=package_name)
|
|
131
122
|
emit_startup_banner(
|
|
132
123
|
click.echo,
|
|
@@ -138,45 +129,18 @@ def _emit_startup_banner(host: str, port: int, package_name: str = "topos-node")
|
|
|
138
129
|
os.environ["TOPOS_STARTUP_BANNER_EMITTED"] = "1"
|
|
139
130
|
|
|
140
131
|
|
|
141
|
-
def _get_latest_pypi_version(package_name: str, timeout_seconds: float = 2.0) -> str | None:
|
|
142
|
-
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
143
|
-
try:
|
|
144
|
-
with urlopen(url, timeout=timeout_seconds) as response:
|
|
145
|
-
payload = json.loads(response.read().decode("utf-8"))
|
|
146
|
-
except (OSError, URLError, ValueError):
|
|
147
|
-
return None
|
|
148
|
-
return str(payload.get("info", {}).get("version") or "").strip() or None
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _should_skip_update_check(skip_update_check: bool) -> bool:
|
|
152
|
-
if skip_update_check:
|
|
153
|
-
return True
|
|
154
|
-
env_value = (os.getenv("TOPOS_SKIP_UPDATE_CHECK") or "").strip().lower()
|
|
155
|
-
return env_value in {"1", "true", "yes", "on"}
|
|
156
|
-
|
|
157
|
-
|
|
158
132
|
def _maybe_offer_self_update(
|
|
159
133
|
skip_update_check: bool,
|
|
160
|
-
package_name: str =
|
|
134
|
+
package_name: str = DEFAULT_PACKAGE_NAME,
|
|
161
135
|
) -> bool:
|
|
162
|
-
if
|
|
136
|
+
if should_skip_update_check(cli_skip=skip_update_check):
|
|
163
137
|
return False
|
|
164
138
|
|
|
165
|
-
|
|
166
|
-
if not
|
|
139
|
+
info = check_for_update(package_name)
|
|
140
|
+
if not info:
|
|
167
141
|
return False
|
|
168
142
|
|
|
169
|
-
|
|
170
|
-
if not latest:
|
|
171
|
-
return False
|
|
172
|
-
|
|
173
|
-
try:
|
|
174
|
-
if Version(latest) <= Version(installed):
|
|
175
|
-
return False
|
|
176
|
-
except InvalidVersion:
|
|
177
|
-
return False
|
|
178
|
-
|
|
179
|
-
click.echo(f"Update available for {package_name}: {installed} -> {latest}")
|
|
143
|
+
click.echo(f"Update available for {package_name}: {info.installed} -> {info.latest}")
|
|
180
144
|
if not _can_prompt_for_input():
|
|
181
145
|
click.echo(f"Run `uv tool upgrade {package_name}` to update.")
|
|
182
146
|
return False
|
|
@@ -185,8 +149,7 @@ def _maybe_offer_self_update(
|
|
|
185
149
|
return False
|
|
186
150
|
|
|
187
151
|
click.echo(f"Updating {package_name}...")
|
|
188
|
-
|
|
189
|
-
if result.returncode != 0:
|
|
152
|
+
if not apply_package_update(package_name):
|
|
190
153
|
click.echo("Update failed. Continuing with current version.")
|
|
191
154
|
return False
|
|
192
155
|
|
|
@@ -246,6 +209,9 @@ def main(db_path, topos_key, set_topos_key, discover, port, host, skip_update_ch
|
|
|
246
209
|
click.echo("No existing databases found")
|
|
247
210
|
return
|
|
248
211
|
|
|
212
|
+
if skip_update_check:
|
|
213
|
+
os.environ["TOPOS_SKIP_UPDATE_CHECK"] = "1"
|
|
214
|
+
|
|
249
215
|
if _maybe_offer_self_update(skip_update_check=skip_update_check):
|
|
250
216
|
return
|
|
251
217
|
|
|
@@ -1139,6 +1139,15 @@ async def handle_control_plane_request(message: Dict[str, Any]) -> Optional[Dict
|
|
|
1139
1139
|
_payload = message.get("payload") or {}
|
|
1140
1140
|
_mcp_source = _payload.get("mcp_source")
|
|
1141
1141
|
_mcp_requester_id = _payload.get("mcp_requester_id")
|
|
1142
|
+
if msg_type == "migrate_browser_plugin_app_id":
|
|
1143
|
+
from .state import _migrate_legacy_browser_plugin_app_ids
|
|
1144
|
+
|
|
1145
|
+
conn = get_db_connection()
|
|
1146
|
+
if not conn:
|
|
1147
|
+
return {"id": req_id, "status": "error", "error": "Database not available"}
|
|
1148
|
+
updated = _migrate_legacy_browser_plugin_app_ids(conn)
|
|
1149
|
+
return {"id": req_id, "status": "ok", "payload": {"updated_rows": updated}}
|
|
1150
|
+
|
|
1142
1151
|
if msg_type == "get_request_counts":
|
|
1143
1152
|
"""Return UMA + MCP request counts from engine DB (for CP proxy or direct frontend)."""
|
|
1144
1153
|
payload = message.get("payload") or {}
|
|
@@ -1305,6 +1314,20 @@ async def handle_control_plane_request(message: Dict[str, Any]) -> Optional[Dict
|
|
|
1305
1314
|
except Exception as exc: # noqa: BLE001
|
|
1306
1315
|
return {"id": req_id, "status": "error", "error": str(exc)}
|
|
1307
1316
|
|
|
1317
|
+
if msg_type == "patch_source_install":
|
|
1318
|
+
from ..api.source_install import _patch_source_install_core
|
|
1319
|
+
|
|
1320
|
+
payload = message.get("payload") if isinstance(message.get("payload"), dict) else {}
|
|
1321
|
+
try:
|
|
1322
|
+
result = await _patch_source_install_core(payload)
|
|
1323
|
+
return {"id": req_id, "status": "ok", "payload": result}
|
|
1324
|
+
except ValueError as exc:
|
|
1325
|
+
return {"id": req_id, "status": "error", "error": str(exc)}
|
|
1326
|
+
except RuntimeError as exc:
|
|
1327
|
+
return {"id": req_id, "status": "error", "error": str(exc)}
|
|
1328
|
+
except Exception as exc: # noqa: BLE001
|
|
1329
|
+
return {"id": req_id, "status": "error", "error": str(exc)}
|
|
1330
|
+
|
|
1308
1331
|
if msg_type == "post_source_test_ingestion":
|
|
1309
1332
|
from ..api.source_install import _test_ingestion_core
|
|
1310
1333
|
|
|
@@ -1759,8 +1782,10 @@ async def handle_control_plane_request(message: Dict[str, Any]) -> Optional[Dict
|
|
|
1759
1782
|
except Exception as exc: # noqa: BLE001
|
|
1760
1783
|
return {"id": req_id, "status": "error", "error": str(exc)}
|
|
1761
1784
|
if msg_type == "get_device_info":
|
|
1785
|
+
payload = message.get("payload") or {}
|
|
1786
|
+
context = payload if isinstance(payload, dict) else {}
|
|
1762
1787
|
try:
|
|
1763
|
-
result = await get_services().device.get_device_info()
|
|
1788
|
+
result = await get_services().device.get_device_info(context=context)
|
|
1764
1789
|
return {"id": req_id, "status": "ok", "payload": result.model_dump()}
|
|
1765
1790
|
except Exception as exc: # noqa: BLE001
|
|
1766
1791
|
return {"id": req_id, "status": "error", "error": str(exc)}
|
|
@@ -5,6 +5,7 @@ from datetime import datetime
|
|
|
5
5
|
from time import time
|
|
6
6
|
|
|
7
7
|
from ..config.settings import settings
|
|
8
|
+
from ..runtime_update import is_update_available
|
|
8
9
|
|
|
9
10
|
_LOG_FORMAT: str | None = None
|
|
10
11
|
|
|
@@ -96,8 +97,9 @@ class ColorFormatter(logging.Formatter):
|
|
|
96
97
|
logging.CRITICAL: "\x1b[38;5;196m", # Red
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
# Timestamp color: Forest green
|
|
100
|
+
# Timestamp color: Forest green (amber when a newer Topos release is on PyPI)
|
|
100
101
|
_TIMESTAMP_COLOR = "\x1b[38;5;28m" # Forest green
|
|
102
|
+
_TIMESTAMP_UPDATE_COLOR = "\x1b[38;5;214m" # Amber
|
|
101
103
|
|
|
102
104
|
# Separator color: Light gray
|
|
103
105
|
_SEPARATOR_COLOR = "\x1b[38;5;244m" # Light gray
|
|
@@ -119,7 +121,10 @@ class ColorFormatter(logging.Formatter):
|
|
|
119
121
|
def format(self, record: logging.LogRecord) -> str: # noqa: D401
|
|
120
122
|
# Format timestamp: YYYY-MM-DD HH:MM:SS.ms (green)
|
|
121
123
|
timestamp_str = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
122
|
-
|
|
124
|
+
timestamp_color = (
|
|
125
|
+
self._TIMESTAMP_UPDATE_COLOR if is_update_available() else self._TIMESTAMP_COLOR
|
|
126
|
+
)
|
|
127
|
+
timestamp = f"{timestamp_color}{timestamp_str}{self._RESET}"
|
|
123
128
|
|
|
124
129
|
# Get text color for log level (no background)
|
|
125
130
|
level_color = self._LEVEL_COLORS.get(record.levelno, "")
|
|
@@ -14,6 +14,8 @@ from typing import Any, Dict, Optional
|
|
|
14
14
|
|
|
15
15
|
MCP_REQUEST_LOG_TABLE = "mcp_request_log"
|
|
16
16
|
UMA_ACCESS_REQUESTS_TABLE = "uma_access_requests"
|
|
17
|
+
LEGACY_BROWSER_PLUGIN_APP_ID = "browser-plugin"
|
|
18
|
+
BROWSER_HISTORY_PLUGIN_APP_ID = "browser-history-plugin"
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def derive_uma_access_context(owner_user_id: str, requesting_user_id: Optional[str]) -> str:
|
|
@@ -401,6 +403,29 @@ def _ensure_uma_access_requests_table(conn: sqlite3.Connection) -> None:
|
|
|
401
403
|
conn.commit()
|
|
402
404
|
except Exception as exc:
|
|
403
405
|
logger.warning("Failed to ensure uma_access_requests table exists: %s", exc)
|
|
406
|
+
else:
|
|
407
|
+
_migrate_legacy_browser_plugin_app_ids(conn)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _migrate_legacy_browser_plugin_app_ids(conn: sqlite3.Connection) -> int:
|
|
411
|
+
"""Rewrite engine Activity counts from browser-plugin → browser-history-plugin (idempotent)."""
|
|
412
|
+
try:
|
|
413
|
+
cursor = conn.execute(
|
|
414
|
+
f"UPDATE {UMA_ACCESS_REQUESTS_TABLE} SET app_id = ? WHERE app_id = ?",
|
|
415
|
+
(BROWSER_HISTORY_PLUGIN_APP_ID, LEGACY_BROWSER_PLUGIN_APP_ID),
|
|
416
|
+
)
|
|
417
|
+
if cursor.rowcount:
|
|
418
|
+
conn.commit()
|
|
419
|
+
logger.info(
|
|
420
|
+
"Migrated %s uma_access_requests rows: %s → %s",
|
|
421
|
+
cursor.rowcount,
|
|
422
|
+
LEGACY_BROWSER_PLUGIN_APP_ID,
|
|
423
|
+
BROWSER_HISTORY_PLUGIN_APP_ID,
|
|
424
|
+
)
|
|
425
|
+
return int(cursor.rowcount or 0)
|
|
426
|
+
except Exception as exc:
|
|
427
|
+
logger.warning("browser-plugin app_id migration on engine failed: %s", exc)
|
|
428
|
+
return 0
|
|
404
429
|
|
|
405
430
|
|
|
406
431
|
def record_uma_request(
|
|
@@ -463,6 +488,7 @@ def get_uma_request_counts(
|
|
|
463
488
|
"total_read_requests": 0,
|
|
464
489
|
"total_write_requests": 0,
|
|
465
490
|
"by_app": [],
|
|
491
|
+
"by_app_requester": [],
|
|
466
492
|
"by_requesting_user": [],
|
|
467
493
|
"access_attribution": {
|
|
468
494
|
"window_days": since_days or 0,
|
|
@@ -510,6 +536,38 @@ def get_uma_request_counts(
|
|
|
510
536
|
for aid, d in sorted(by_app.items())
|
|
511
537
|
]
|
|
512
538
|
|
|
539
|
+
cursor = conn.execute(
|
|
540
|
+
f"""SELECT app_id, requesting_user_id, requesting_user_email, request_type
|
|
541
|
+
FROM {UMA_ACCESS_REQUESTS_TABLE}
|
|
542
|
+
WHERE owner_user_id = ? AND created_at >= ?""",
|
|
543
|
+
(owner_user_id, since_ts),
|
|
544
|
+
)
|
|
545
|
+
by_app_req: Dict[str, Dict[str, Any]] = {}
|
|
546
|
+
for row in cursor.fetchall():
|
|
547
|
+
app_id_val = (row[0] or "").strip() if len(row) > 0 else ""
|
|
548
|
+
rid = (row[1] or "").strip() if len(row) > 1 else ""
|
|
549
|
+
remail = (row[2] or "").strip() if len(row) > 2 else ""
|
|
550
|
+
rt = (row[3] or "").strip().lower() if len(row) > 3 else ""
|
|
551
|
+
key = f"{app_id_val}\0{rid}\0{remail}"
|
|
552
|
+
if key not in by_app_req:
|
|
553
|
+
by_app_req[key] = {
|
|
554
|
+
"app_id": app_id_val or None,
|
|
555
|
+
"requesting_user_id": rid or None,
|
|
556
|
+
"requesting_user_email": remail or None,
|
|
557
|
+
"read_requests": 0,
|
|
558
|
+
"write_requests": 0,
|
|
559
|
+
}
|
|
560
|
+
if rt == "read":
|
|
561
|
+
by_app_req[key]["read_requests"] += 1
|
|
562
|
+
elif rt == "write":
|
|
563
|
+
by_app_req[key]["write_requests"] += 1
|
|
564
|
+
app_req_ranked = []
|
|
565
|
+
for d in by_app_req.values():
|
|
566
|
+
tr = int(d["read_requests"]) + int(d["write_requests"])
|
|
567
|
+
app_req_ranked.append({**d, "total_requests": tr})
|
|
568
|
+
app_req_ranked.sort(key=lambda x: -x["total_requests"])
|
|
569
|
+
out["by_app_requester"] = app_req_ranked[:50]
|
|
570
|
+
|
|
513
571
|
cursor = conn.execute(
|
|
514
572
|
f"""SELECT requesting_user_id, requesting_user_email, request_type, access_context
|
|
515
573
|
FROM {UMA_ACCESS_REQUESTS_TABLE}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Runtime PyPI update checks for the Topos node (non-blocking while server runs)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
14
|
+
from urllib.error import URLError
|
|
15
|
+
from urllib.request import urlopen
|
|
16
|
+
|
|
17
|
+
from packaging.version import InvalidVersion, Version
|
|
18
|
+
|
|
19
|
+
DEFAULT_PACKAGE_NAME = "topos-node"
|
|
20
|
+
DEFAULT_CHECK_INTERVAL_SECONDS = 6 * 60 * 60
|
|
21
|
+
INITIAL_CHECK_DELAY_SECONDS = 10.0
|
|
22
|
+
PYPI_TIMEOUT_SECONDS = 2.0
|
|
23
|
+
|
|
24
|
+
_logger = logging.getLogger("topos.runtime_update")
|
|
25
|
+
_lock = threading.Lock()
|
|
26
|
+
_update_info: UpdateInfo | None = None
|
|
27
|
+
_announcement_logged = False
|
|
28
|
+
_monitor_task: asyncio.Task[None] | None = None
|
|
29
|
+
_hotkey_thread: threading.Thread | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class UpdateInfo:
|
|
34
|
+
package_name: str
|
|
35
|
+
installed: str
|
|
36
|
+
latest: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def should_skip_update_check(*, cli_skip: bool = False) -> bool:
|
|
40
|
+
if cli_skip:
|
|
41
|
+
return True
|
|
42
|
+
env_value = (os.getenv("TOPOS_SKIP_UPDATE_CHECK") or "").strip().lower()
|
|
43
|
+
return env_value in {"1", "true", "yes", "on"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_installed_package_version(package_name: str = DEFAULT_PACKAGE_NAME) -> str | None:
|
|
47
|
+
try:
|
|
48
|
+
return package_version(package_name)
|
|
49
|
+
except PackageNotFoundError:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_module_version() -> str | None:
|
|
54
|
+
try:
|
|
55
|
+
from topos.__version__ import __version__
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
return __version__ or None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_runtime_version(package_name: str = DEFAULT_PACKAGE_NAME) -> str:
|
|
62
|
+
return get_module_version() or get_installed_package_version(package_name) or "unknown"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_latest_pypi_version(
|
|
66
|
+
package_name: str = DEFAULT_PACKAGE_NAME,
|
|
67
|
+
timeout_seconds: float = PYPI_TIMEOUT_SECONDS,
|
|
68
|
+
) -> str | None:
|
|
69
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
70
|
+
try:
|
|
71
|
+
with urlopen(url, timeout=timeout_seconds) as response:
|
|
72
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
73
|
+
except (OSError, URLError, ValueError):
|
|
74
|
+
return None
|
|
75
|
+
return str(payload.get("info", {}).get("version") or "").strip() or None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_update_available() -> bool:
|
|
79
|
+
with _lock:
|
|
80
|
+
return _update_info is not None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_update_info() -> UpdateInfo | None:
|
|
84
|
+
with _lock:
|
|
85
|
+
return _update_info
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _set_update_info(info: UpdateInfo | None) -> None:
|
|
89
|
+
global _update_info
|
|
90
|
+
with _lock:
|
|
91
|
+
_update_info = info
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_newer_version(installed: str, latest: str) -> bool:
|
|
95
|
+
try:
|
|
96
|
+
return Version(latest) > Version(installed)
|
|
97
|
+
except InvalidVersion:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_for_update(package_name: str = DEFAULT_PACKAGE_NAME) -> UpdateInfo | None:
|
|
102
|
+
"""Return update details when PyPI has a newer release, else None."""
|
|
103
|
+
if should_skip_update_check():
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
installed = get_installed_package_version(package_name) or get_module_version()
|
|
107
|
+
if not installed or installed == "unknown":
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
latest = get_latest_pypi_version(package_name)
|
|
111
|
+
if not latest or not _is_newer_version(installed, latest):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
return UpdateInfo(package_name=package_name, installed=installed, latest=latest)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def apply_package_update(package_name: str = DEFAULT_PACKAGE_NAME) -> bool:
|
|
118
|
+
"""Install the latest PyPI release via `uv tool upgrade`. Returns True on success."""
|
|
119
|
+
result = subprocess.run(["uv", "tool", "upgrade", package_name], check=False)
|
|
120
|
+
return result.returncode == 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _log_update_available_once(info: UpdateInfo) -> None:
|
|
124
|
+
global _announcement_logged
|
|
125
|
+
with _lock:
|
|
126
|
+
if _announcement_logged:
|
|
127
|
+
return
|
|
128
|
+
_announcement_logged = True
|
|
129
|
+
|
|
130
|
+
_logger.warning(
|
|
131
|
+
"New Topos version available: %s -> %s. "
|
|
132
|
+
"Timestamps will show in amber until you restart. "
|
|
133
|
+
"Update with `uv tool upgrade %s` then stop (Ctrl+C) and re-run `topos-node`. "
|
|
134
|
+
"In an interactive terminal, type `:update` and press Enter to install now.",
|
|
135
|
+
info.installed,
|
|
136
|
+
info.latest,
|
|
137
|
+
info.package_name,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _monitor_loop(
|
|
142
|
+
*,
|
|
143
|
+
package_name: str = DEFAULT_PACKAGE_NAME,
|
|
144
|
+
initial_delay_seconds: float = INITIAL_CHECK_DELAY_SECONDS,
|
|
145
|
+
interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
|
|
146
|
+
) -> None:
|
|
147
|
+
await asyncio.sleep(initial_delay_seconds)
|
|
148
|
+
while True:
|
|
149
|
+
try:
|
|
150
|
+
info = await asyncio.to_thread(check_for_update, package_name)
|
|
151
|
+
if info:
|
|
152
|
+
_set_update_info(info)
|
|
153
|
+
_log_update_available_once(info)
|
|
154
|
+
except asyncio.CancelledError:
|
|
155
|
+
raise
|
|
156
|
+
except Exception as exc: # noqa: BLE001
|
|
157
|
+
_logger.debug("Runtime update check failed (non-fatal): %s", exc)
|
|
158
|
+
await asyncio.sleep(interval_seconds)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def start_runtime_update_monitor(
|
|
162
|
+
*,
|
|
163
|
+
cli_skip: bool = False,
|
|
164
|
+
package_name: str = DEFAULT_PACKAGE_NAME,
|
|
165
|
+
) -> asyncio.Task[None] | None:
|
|
166
|
+
"""Schedule periodic PyPI checks; safe to call once at app startup."""
|
|
167
|
+
global _monitor_task
|
|
168
|
+
if should_skip_update_check(cli_skip=cli_skip):
|
|
169
|
+
return None
|
|
170
|
+
if _monitor_task is not None and not _monitor_task.done():
|
|
171
|
+
return _monitor_task
|
|
172
|
+
|
|
173
|
+
loop = asyncio.get_running_loop()
|
|
174
|
+
_monitor_task = loop.create_task(
|
|
175
|
+
_monitor_loop(package_name=package_name),
|
|
176
|
+
name="topos-runtime-update-monitor",
|
|
177
|
+
)
|
|
178
|
+
return _monitor_task
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def stop_runtime_update_monitor() -> None:
|
|
182
|
+
global _monitor_task
|
|
183
|
+
task = _monitor_task
|
|
184
|
+
_monitor_task = None
|
|
185
|
+
if task is None:
|
|
186
|
+
return
|
|
187
|
+
task.cancel()
|
|
188
|
+
try:
|
|
189
|
+
await task
|
|
190
|
+
except asyncio.CancelledError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _handle_hotkey_line(line: str, package_name: str = DEFAULT_PACKAGE_NAME) -> None:
|
|
195
|
+
command = line.strip().lower()
|
|
196
|
+
if command not in {":update", ":u"}:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
info = get_update_info() or check_for_update(package_name)
|
|
200
|
+
if not info:
|
|
201
|
+
_logger.info("Topos is already on the latest published version.")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
_logger.info("Updating %s (%s -> %s)...", package_name, info.installed, info.latest)
|
|
205
|
+
if apply_package_update(package_name):
|
|
206
|
+
_logger.warning(
|
|
207
|
+
"Update installed. Stop Topos (Ctrl+C) and re-run `topos-node` to use %s.",
|
|
208
|
+
info.latest,
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
_logger.error("Update failed. Run `uv tool upgrade %s` manually.", package_name)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _hotkey_listener_loop(package_name: str = DEFAULT_PACKAGE_NAME) -> None:
|
|
215
|
+
try:
|
|
216
|
+
for line in sys.stdin:
|
|
217
|
+
_handle_hotkey_line(line, package_name=package_name)
|
|
218
|
+
except Exception as exc: # noqa: BLE001
|
|
219
|
+
_logger.debug("Update hotkey listener stopped: %s", exc)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def start_update_hotkey_listener(
|
|
223
|
+
*,
|
|
224
|
+
cli_skip: bool = False,
|
|
225
|
+
package_name: str = DEFAULT_PACKAGE_NAME,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Listen for `:update` on stdin (interactive terminals only)."""
|
|
228
|
+
global _hotkey_thread
|
|
229
|
+
if should_skip_update_check(cli_skip=cli_skip):
|
|
230
|
+
return
|
|
231
|
+
if not sys.stdin.isatty():
|
|
232
|
+
return
|
|
233
|
+
if _hotkey_thread is not None and _hotkey_thread.is_alive():
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
_hotkey_thread = threading.Thread(
|
|
237
|
+
target=_hotkey_listener_loop,
|
|
238
|
+
kwargs={"package_name": package_name},
|
|
239
|
+
name="topos-update-hotkey",
|
|
240
|
+
daemon=True,
|
|
241
|
+
)
|
|
242
|
+
_hotkey_thread.start()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def can_prompt_for_input() -> bool:
|
|
246
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|