topos-node 0.1.8__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.8 → topos_node-0.1.9}/PKG-INFO +1 -1
- {topos_node-0.1.8 → topos_node-0.1.9}/pyproject.toml +1 -1
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/__version__.py +2 -2
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/source_install.py +30 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/app.py +9 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/cli/commands.py +30 -64
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/handlers.py +14 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/logging.py +7 -2
- topos_node-0.1.9/topos/runtime_update.py +246 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/install_service.py +95 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sync/client.py +3 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/PKG-INFO +1 -1
- {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/SOURCES.txt +1 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/LICENSE +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/README.md +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/setup.cfg +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/shared/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/shared/filtering.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/shared/schema_registry.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/duckdb_adapter.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/messenger_communities.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/messenger_graph.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/messenger_labels.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/profiles.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/query_engine.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/raw_queries.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/analytics.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/app_registry.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/backup.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/compute_remote.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/data_commit.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/data_explorer_table_prefs.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/db.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/device.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/enrichment.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/filter_lab.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/health.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ingestion_api.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ingestion_compat.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ingestion_sources.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/llm.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/local_mcp.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/messenger_analytics.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/query_api.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/sanitization_ollama_config.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/sources.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/sync.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ui_config.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/uma_data.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/usage.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/user_identity.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/auth.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/chatgpt_mapper.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/grok_mapper.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/messenger_mapper.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/models.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/resolver.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/cli/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/cli/__main__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/config/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/config/sanitization_ollama.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/config/settings.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/contacts/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/contacts/identity.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/control_plane_client.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/api_models.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/connection_resilience.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/device_helpers.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/errors.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/events.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/metrics.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/startup_banner.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/state.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/table_layers.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/types.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/data_explorer_table_prefs.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/huggingface.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/ollama.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/stub.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/engine.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/intake.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/queue_manager.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/registration.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/result_formatter.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/router.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/scoped_token.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/tasks.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/transport.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/usage_guard.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/usage_observation.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/validator.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/derived_tables.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/embeddings_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/emo_27_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/entities_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/sentiment_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/topics_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/attachments_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/language_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/time_normalization_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/tool_calls_job.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/manager.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/registry.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/versioning.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/orchestrator.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/processor.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/progress_bar.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/website_classifier.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/bundles.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/schema.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/service.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/worker.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/hosted_pool_lease.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/checkpoints/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/checkpoints/checkpoint_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/checkpoints/sqlite_checkpoint_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/ingest_helpers.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/jobs.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/local_sync.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/log_preview.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/manager.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parser.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/browser_parser.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/calendar_parser.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/chatgpt_conversation_flattener.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/chatgpt_parser.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/grok_parser.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/messenger_parser.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/progress.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/calendar.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/chatgpt.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/contact_importers.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/grok.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/imessage_reader.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/signal_export_parser.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/signal_reader.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/state_machine.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/triggers/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/triggers/file_trigger.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/triggers/sqlite_trigger.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/schema_registry.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/schema_validator.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/lineage/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/lineage/provenance.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/lineage/tracker.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/mcp_stdio_proxy.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/alerts.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/metrics.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/tracing.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/openai_client.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/builders.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/health_checks.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/rate_limit.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sanitization/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sanitization/ollama_transforms.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/scope_resolution.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/container.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/local.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/remote.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/interfaces.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/llm/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/llm/base.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/llm/openai.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/local.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/postgres.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/definitions.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/registry.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/runtime_install.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/startup_banner.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/canonicalizer.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/mapper.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/model.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/tables.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/canonical_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/conversations_tables.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/mapping_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/postgres.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/client.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/migrations/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/migrations/stage9_column_renames.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/paths.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/postgres.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/schema.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/enrichment/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/enrichment/canonical_enrichment_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/enrichment/raw_enrichment_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/normalized/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/normalized/normalized_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/decision.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/oplog_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/postgres.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/projections/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/projections/index_ops_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/projections/vector_index_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/browser_flat_tables.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/file_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/raw_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/raw_tables_manager.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/sqlite_raw_store.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/security/encryption.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/signal_identity.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/source_settings.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/user_identity.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sync/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/sync_handlers.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/testing/__init__.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/testing/lifespan.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_contact_enrichment.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_filters.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_resource_id.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_rpt.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/utils/base_object.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos/websocket_client.py +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/dependency_links.txt +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/entry_points.txt +0 -0
- {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/requires.txt +0 -0
- {topos_node-0.1.8 → 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"
|
|
@@ -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
|
|
|
@@ -1314,6 +1314,20 @@ async def handle_control_plane_request(message: Dict[str, Any]) -> Optional[Dict
|
|
|
1314
1314
|
except Exception as exc: # noqa: BLE001
|
|
1315
1315
|
return {"id": req_id, "status": "error", "error": str(exc)}
|
|
1316
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
|
+
|
|
1317
1331
|
if msg_type == "post_source_test_ingestion":
|
|
1318
1332
|
from ..api.source_install import _test_ingestion_core
|
|
1319
1333
|
|
|
@@ -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, "")
|
|
@@ -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()
|
|
@@ -782,6 +782,101 @@ def uninstall_source(
|
|
|
782
782
|
}
|
|
783
783
|
|
|
784
784
|
|
|
785
|
+
_PATCHABLE_DEFINITION_KEYS = frozenset(
|
|
786
|
+
{
|
|
787
|
+
"default_scope_id",
|
|
788
|
+
"ingestion_trigger",
|
|
789
|
+
"enrichment_trigger",
|
|
790
|
+
"raw_enrichment_jobs",
|
|
791
|
+
"canonical_enrichment_jobs",
|
|
792
|
+
}
|
|
793
|
+
)
|
|
794
|
+
_IMMUTABLE_DEFINITION_KEYS = frozenset(
|
|
795
|
+
{
|
|
796
|
+
"source_id",
|
|
797
|
+
"display_name",
|
|
798
|
+
"source_type",
|
|
799
|
+
"schema_id",
|
|
800
|
+
"parser_id",
|
|
801
|
+
"canonical_mapper_id",
|
|
802
|
+
"canonical_group_id",
|
|
803
|
+
}
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def patch_source_install(
|
|
808
|
+
*,
|
|
809
|
+
source_id: str,
|
|
810
|
+
scope: Optional[Dict[str, Any]] = None,
|
|
811
|
+
source_definition_json: Optional[Dict[str, Any]] = None,
|
|
812
|
+
) -> InstallRecord:
|
|
813
|
+
"""Update an active install's definition in place (user configuration changes)."""
|
|
814
|
+
ensure_install_schema()
|
|
815
|
+
sid = str(source_id or "").strip()
|
|
816
|
+
if not sid:
|
|
817
|
+
raise ValueError("source_id is required")
|
|
818
|
+
partial = source_definition_json if isinstance(source_definition_json, dict) else {}
|
|
819
|
+
if not partial:
|
|
820
|
+
raise ValueError("source_definition_json is required")
|
|
821
|
+
|
|
822
|
+
_validate_concrete_install_scope(scope)
|
|
823
|
+
scope_key = _scope_key(scope)
|
|
824
|
+
now = _utc_now_iso()
|
|
825
|
+
|
|
826
|
+
with _db_conn() as conn:
|
|
827
|
+
with _LOCK:
|
|
828
|
+
active = _get_active_record(conn, scope_key, sid)
|
|
829
|
+
if not active or not active.is_active:
|
|
830
|
+
raise ValueError(f"No active install found for source_id={sid}")
|
|
831
|
+
|
|
832
|
+
merged = dict(active.source_definition_json if isinstance(active.source_definition_json, dict) else {})
|
|
833
|
+
for key, value in partial.items():
|
|
834
|
+
if key in _IMMUTABLE_DEFINITION_KEYS and value != merged.get(key):
|
|
835
|
+
raise ValueError(f"Cannot change immutable field {key!r} via PATCH")
|
|
836
|
+
if key in _PATCHABLE_DEFINITION_KEYS:
|
|
837
|
+
merged[key] = value
|
|
838
|
+
|
|
839
|
+
merged = _normalize_enrichment_bindings(merged)
|
|
840
|
+
_validate_source_contract(merged)
|
|
841
|
+
if str(merged.get("source_id") or "").strip() != sid:
|
|
842
|
+
raise ValueError("source_id in definition must match install source_id")
|
|
843
|
+
|
|
844
|
+
try:
|
|
845
|
+
handle = install_source_definition(merged)
|
|
846
|
+
except Exception as exc:
|
|
847
|
+
raise RuntimeError(str(exc)) from exc
|
|
848
|
+
|
|
849
|
+
execute_query(
|
|
850
|
+
conn,
|
|
851
|
+
f"""
|
|
852
|
+
UPDATE {INSTALL_TABLE}
|
|
853
|
+
SET source_definition_json = %s, updated_at = %s
|
|
854
|
+
WHERE install_id = %s
|
|
855
|
+
""",
|
|
856
|
+
(
|
|
857
|
+
json.dumps(merged, separators=(",", ":"), ensure_ascii=True),
|
|
858
|
+
now,
|
|
859
|
+
active.install_id,
|
|
860
|
+
),
|
|
861
|
+
)
|
|
862
|
+
if settings.topos_database_mode != "postgres":
|
|
863
|
+
conn.commit()
|
|
864
|
+
_ACTIVE_HANDLES[(scope_key, sid)] = handle
|
|
865
|
+
return InstallRecord(
|
|
866
|
+
install_id=active.install_id,
|
|
867
|
+
scope=_scope_dict(scope_key),
|
|
868
|
+
source_id=sid,
|
|
869
|
+
version_id=active.version_id,
|
|
870
|
+
status="active",
|
|
871
|
+
is_active=True,
|
|
872
|
+
source_definition_json=merged,
|
|
873
|
+
source_version_row_json=active.source_version_row_json,
|
|
874
|
+
failure_reason=None,
|
|
875
|
+
created_at=active.created_at,
|
|
876
|
+
updated_at=now,
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
|
|
785
880
|
def get_active_source_definition(*, source_id: str, scope: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
|
786
881
|
try:
|
|
787
882
|
with _db_conn() as conn:
|
|
@@ -148,6 +148,9 @@ class SyncClient:
|
|
|
148
148
|
self._consecutive_failures = 0
|
|
149
149
|
self._last_connected_at = utc_now_iso()
|
|
150
150
|
logger.info("Sync client connected to relay")
|
|
151
|
+
# Let concurrent readiness waiters observe the connection before
|
|
152
|
+
# processing messages; short-lived test sockets can close immediately.
|
|
153
|
+
await asyncio.sleep(0)
|
|
151
154
|
|
|
152
155
|
await self._send_connect()
|
|
153
156
|
|