contextsynapse 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contextsynapse/__init__.py +107 -0
- contextsynapse/__main__.py +9 -0
- contextsynapse/a2a/__init__.py +28 -0
- contextsynapse/a2a/client.py +120 -0
- contextsynapse/a2a/discovery.py +128 -0
- contextsynapse/a2a/handlers.py +164 -0
- contextsynapse/a2a/models.py +246 -0
- contextsynapse/a2a/streaming.py +92 -0
- contextsynapse/a2a/task_manager.py +275 -0
- contextsynapse/adapters/__init__.py +98 -0
- contextsynapse/adapters/_base.py +298 -0
- contextsynapse/adapters/autogen/__init__.py +4 -0
- contextsynapse/adapters/autogen/tools.py +34 -0
- contextsynapse/adapters/crewai/__init__.py +4 -0
- contextsynapse/adapters/crewai/tools.py +63 -0
- contextsynapse/adapters/langchain/__init__.py +19 -0
- contextsynapse/adapters/langchain/message_history.py +17 -0
- contextsynapse/adapters/langchain/retriever.py +158 -0
- contextsynapse/adapters/langchain/tools.py +43 -0
- contextsynapse/adapters/langgraph/__init__.py +21 -0
- contextsynapse/adapters/langgraph/checkpoint.py +223 -0
- contextsynapse/adapters/langgraph/context_tools.py +341 -0
- contextsynapse/adapters/langgraph/message_history.py +138 -0
- contextsynapse/adapters/langgraph/tools.py +43 -0
- contextsynapse/adapters/llamaindex/__init__.py +4 -0
- contextsynapse/adapters/llamaindex/tools.py +64 -0
- contextsynapse/adapters/openai/__init__.py +24 -0
- contextsynapse/adapters/openai/tools.py +206 -0
- contextsynapse/adapters/pydantic_ai/__init__.py +4 -0
- contextsynapse/adapters/pydantic_ai/tools.py +36 -0
- contextsynapse/adapters/swarm/__init__.py +4 -0
- contextsynapse/adapters/swarm/tools.py +44 -0
- contextsynapse/agents/__init__.py +6 -0
- contextsynapse/agents/auto_capture.py +80 -0
- contextsynapse/agents/dispatcher.py +299 -0
- contextsynapse/agents/notifications.py +112 -0
- contextsynapse/aiql/__init__.py +71 -0
- contextsynapse/aiql/cache.py +341 -0
- contextsynapse/aiql/compiler/__init__.py +62 -0
- contextsynapse/aiql/compiler/optimizer.py +126 -0
- contextsynapse/aiql/compiler/planner.py +366 -0
- contextsynapse/aiql/compiler/statistics.py +235 -0
- contextsynapse/aiql/compiler/validator.py +425 -0
- contextsynapse/aiql/engine/__init__.py +11 -0
- contextsynapse/aiql/engine/chunk_storage.py +2584 -0
- contextsynapse/aiql/engine/executor.py +7737 -0
- contextsynapse/aiql/engine/extraction_integration.py +442 -0
- contextsynapse/aiql/engine/graph_builder.py +405 -0
- contextsynapse/aiql/engine/node_creation_from_normalized.py +691 -0
- contextsynapse/aiql/engine/performance_monitor.py +289 -0
- contextsynapse/aiql/engine/postprocessing.py +365 -0
- contextsynapse/aiql/engine/ray_stage_executors.py +918 -0
- contextsynapse/aiql/engine/redis_cache.py +106 -0
- contextsynapse/aiql/engine/streaming_json.py +366 -0
- contextsynapse/aiql/engine/streaming_parquet.py +568 -0
- contextsynapse/aiql/engine/streaming_ray_chunking.py +419 -0
- contextsynapse/aiql/grammar/__init__.py +14 -0
- contextsynapse/aiql/grammar/aiql_grammar.py +1822 -0
- contextsynapse/aiql/grammar/base.py +71 -0
- contextsynapse/aiql/parser/__init__.py +46 -0
- contextsynapse/aiql/parser/aiql_parser.py +11330 -0
- contextsynapse/aiql/parser/base_parser.py +47 -0
- contextsynapse/api/__init__.py +5 -0
- contextsynapse/api/a2a_router.py +233 -0
- contextsynapse/api/admin_router.py +140 -0
- contextsynapse/api/agent_worker_router.py +978 -0
- contextsynapse/api/algorithms_router.py +317 -0
- contextsynapse/api/api.py +4144 -0
- contextsynapse/api/audit.py +204 -0
- contextsynapse/api/auth.py +441 -0
- contextsynapse/api/auth_router.py +537 -0
- contextsynapse/api/billing.py +261 -0
- contextsynapse/api/billing_router.py +275 -0
- contextsynapse/api/boundary_router.py +452 -0
- contextsynapse/api/cognition_router.py +116 -0
- contextsynapse/api/connector_router.py +237 -0
- contextsynapse/api/context_router.py +1619 -0
- contextsynapse/api/dashboard_router.py +11367 -0
- contextsynapse/api/errors.py +183 -0
- contextsynapse/api/events.py +88 -0
- contextsynapse/api/experiment_bootstrap.py +321 -0
- contextsynapse/api/experiment_orchestrator.py +1078 -0
- contextsynapse/api/experiment_router.py +2089 -0
- contextsynapse/api/feature_gate.py +89 -0
- contextsynapse/api/federation_router.py +108 -0
- contextsynapse/api/integrations.py +567 -0
- contextsynapse/api/integrations_router.py +461 -0
- contextsynapse/api/intelligence_router.py +1028 -0
- contextsynapse/api/metering.py +155 -0
- contextsynapse/api/metrics.py +112 -0
- contextsynapse/api/models.py +38 -0
- contextsynapse/api/monitoring_router.py +174 -0
- contextsynapse/api/pipeline_router.py +1437 -0
- contextsynapse/api/playground_conn.py +116 -0
- contextsynapse/api/projects_router.py +131 -0
- contextsynapse/api/queries_router.py +282 -0
- contextsynapse/api/quota.py +95 -0
- contextsynapse/api/rate_limit.py +64 -0
- contextsynapse/api/search_router.py +261 -0
- contextsynapse/api/shield_router.py +447 -0
- contextsynapse/api/tenant_guard.py +103 -0
- contextsynapse/api/tenant_quotas.py +231 -0
- contextsynapse/api/tenants.py +305 -0
- contextsynapse/api/users.py +646 -0
- contextsynapse/api/v1.py +167 -0
- contextsynapse/api/vertical_builder_router.py +387 -0
- contextsynapse/benchmark/__init__.py +15 -0
- contextsynapse/benchmark/__main__.py +64 -0
- contextsynapse/benchmark/agents.py +338 -0
- contextsynapse/benchmark/live_ab.py +338 -0
- contextsynapse/benchmark/reporter.py +149 -0
- contextsynapse/benchmark/runner.py +264 -0
- contextsynapse/benchmark/savings.py +266 -0
- contextsynapse/benchmark/token_counter.py +212 -0
- contextsynapse/benchmark/workloads.py +289 -0
- contextsynapse/buffer/__init__.py +154 -0
- contextsynapse/buffer/base.py +257 -0
- contextsynapse/buffer/duckdb_buffer.py +246 -0
- contextsynapse/buffer/file_buffer.py +229 -0
- contextsynapse/buffer/lmdb_buffer.py +286 -0
- contextsynapse/buffer/local_buffer.py +154 -0
- contextsynapse/buffer/redis_buffer.py +211 -0
- contextsynapse/buffer/sqlite_buffer.py +266 -0
- contextsynapse/cache.py +2 -0
- contextsynapse/cli/__init__.py +40 -0
- contextsynapse/cli/cli.py +475 -0
- contextsynapse/cli/config_cli.py +177 -0
- contextsynapse/cli/scaffold.py +465 -0
- contextsynapse/cognition/__init__.py +31 -0
- contextsynapse/cognition/derivation.py +55 -0
- contextsynapse/cognition/events.py +135 -0
- contextsynapse/cognition/feedback.py +52 -0
- contextsynapse/cognition/hallucination.py +90 -0
- contextsynapse/cognition/invalidation.py +70 -0
- contextsynapse/cognition/read_tracker.py +50 -0
- contextsynapse/collaboration.py +2 -0
- contextsynapse/comparison.py +2 -0
- contextsynapse/config/__init__.py +10 -0
- contextsynapse/config/connectors/amfi_nav.yaml +18 -0
- contextsynapse/config/connectors/auto_sector.yaml +20 -0
- contextsynapse/config/connectors/banking_sector.yaml +20 -0
- contextsynapse/config/connectors/energy_sector.yaml +20 -0
- contextsynapse/config/connectors/fmcg_sector.yaml +20 -0
- contextsynapse/config/connectors/nsdl_fpi.yaml +9 -0
- contextsynapse/config/connectors/nse_price.yaml +61 -0
- contextsynapse/config/connectors/pharma_sector.yaml +20 -0
- contextsynapse/config/connectors/rbi_dbie.yaml +12 -0
- contextsynapse/config/context_presets/ai_research.json +25 -0
- contextsynapse/config/context_presets/chatgpt_conversations.json +19 -0
- contextsynapse/config/context_presets/claude_conversations.json +19 -0
- contextsynapse/config/context_presets/legal_compliance.json +21 -0
- contextsynapse/config/context_presets/meeting_notes.json +21 -0
- contextsynapse/config/context_presets/slack_engineering.json +21 -0
- contextsynapse/config/context_presets/tech_climate.json +24 -0
- contextsynapse/config/context_presets/toi_politics.json +25 -0
- contextsynapse/config/database_config.py +453 -0
- contextsynapse/config/deployment.py +221 -0
- contextsynapse/config/gpt_action_schema.json +164 -0
- contextsynapse/config/indicators/india_macro.yaml +213 -0
- contextsynapse/config/logging_config.py +88 -0
- contextsynapse/config/mode_config.py +223 -0
- contextsynapse/config/schemas/api_spec.yaml +48 -0
- contextsynapse/config/schemas/architecture_doc.yaml +51 -0
- contextsynapse/config/schemas/chat_history.yaml +90 -0
- contextsynapse/config/schemas/chatgpt_conversation.yaml +101 -0
- contextsynapse/config/schemas/claude_conversation.yaml +148 -0
- contextsynapse/config/schemas/contract.yaml +48 -0
- contextsynapse/config/schemas/conversation.yaml +167 -0
- contextsynapse/config/schemas/database.yaml +74 -0
- contextsynapse/config/schemas/decision.yaml +72 -0
- contextsynapse/config/schemas/ecommerce.yaml +56 -0
- contextsynapse/config/schemas/education.yaml +42 -0
- contextsynapse/config/schemas/healthcare.yaml +133 -0
- contextsynapse/config/schemas/hr_document.yaml +48 -0
- contextsynapse/config/schemas/invoice.yaml +40 -0
- contextsynapse/config/schemas/knowledge_base.yaml +75 -0
- contextsynapse/config/schemas/legal.yaml +42 -0
- contextsynapse/config/schemas/marketing_content.yaml +48 -0
- contextsynapse/config/schemas/meeting_notes.yaml +51 -0
- contextsynapse/config/schemas/news_article.yaml +61 -0
- contextsynapse/config/schemas/real_estate.yaml +49 -0
- contextsynapse/config/schemas/requirements_doc.yaml +48 -0
- contextsynapse/config/schemas/research_paper.yaml +56 -0
- contextsynapse/config/schemas/resume.yaml +49 -0
- contextsynapse/config/schemas/retail.yaml +42 -0
- contextsynapse/config/schemas/rule_meta.yaml +173 -0
- contextsynapse/config/schemas/rules.yaml +72 -0
- contextsynapse/config/schemas/screener_meta.yaml +236 -0
- contextsynapse/config/schemas/sdlc.yaml +103 -0
- contextsynapse/config/schemas/session_graph.yaml +172 -0
- contextsynapse/config/schemas/support_ticket.yaml +48 -0
- contextsynapse/config/schemas/web.yaml +64 -0
- contextsynapse/config/sensors/earnings_calendar.yaml +52 -0
- contextsynapse/config/sensors/global_risk.yaml +53 -0
- contextsynapse/config/sensors/government_policy.yaml +54 -0
- contextsynapse/config/sensors/supply_chain.yaml +50 -0
- contextsynapse/config/sensors/templates/behavioral.yaml +56 -0
- contextsynapse/config/sensors/templates/equity_sentiment.yaml +48 -0
- contextsynapse/config/sensors/templates/global_macro.yaml +47 -0
- contextsynapse/config/sensors/templates/market_data.yaml +118 -0
- contextsynapse/config/sensors/templates/sector_technical.yaml +47 -0
- contextsynapse/config/sensors/weather.yaml +52 -0
- contextsynapse/config/storage_strategy.py +202 -0
- contextsynapse/config/templates/competitive_intel.yaml +39 -0
- contextsynapse/config/templates/geopolitical_risk.yaml +46 -0
- contextsynapse/config/templates/market_analysis.yaml +58 -0
- contextsynapse/connectors/__init__.py +15 -0
- contextsynapse/connectors/api_router.py +121 -0
- contextsynapse/connectors/base.py +357 -0
- contextsynapse/connectors/financial/audio.py +170 -0
- contextsynapse/connectors/financial/youtube.py +272 -0
- contextsynapse/connectors/github.py +164 -0
- contextsynapse/connectors/jira.py +120 -0
- contextsynapse/connectors/kafka_connector.py +124 -0
- contextsynapse/connectors/pipeline_connector.py +747 -0
- contextsynapse/connectors/registry.py +116 -0
- contextsynapse/connectors/scheduler.py +87 -0
- contextsynapse/connectors/universal.py +360 -0
- contextsynapse/context/__init__.py +96 -0
- contextsynapse/context/__main__.py +4 -0
- contextsynapse/context/acl.py +125 -0
- contextsynapse/context/agent_card.py +201 -0
- contextsynapse/context/agent_memory.py +814 -0
- contextsynapse/context/agents.py +472 -0
- contextsynapse/context/agents_redis.py +391 -0
- contextsynapse/context/assembled.py +204 -0
- contextsynapse/context/attribution.py +113 -0
- contextsynapse/context/blob.py +184 -0
- contextsynapse/context/boundaries.py +187 -0
- contextsynapse/context/boundary.py +507 -0
- contextsynapse/context/bundle.py +155 -0
- contextsynapse/context/cli.py +519 -0
- contextsynapse/context/collaboration.py +165 -0
- contextsynapse/context/compiler.py +604 -0
- contextsynapse/context/composite.py +536 -0
- contextsynapse/context/compression.py +147 -0
- contextsynapse/context/context_manager.py +888 -0
- contextsynapse/context/context_schema.py +90 -0
- contextsynapse/context/context_schema.yaml +105 -0
- contextsynapse/context/context_units.py +891 -0
- contextsynapse/context/conversation.py +488 -0
- contextsynapse/context/dedup.py +207 -0
- contextsynapse/context/document_processor.py +698 -0
- contextsynapse/context/embedding_hooks.py +338 -0
- contextsynapse/context/execution_schema.py +274 -0
- contextsynapse/context/fan_out.py +138 -0
- contextsynapse/context/frozen.py +80 -0
- contextsynapse/context/gravity.py +227 -0
- contextsynapse/context/hub.py +1401 -0
- contextsynapse/context/ingest.py +391 -0
- contextsynapse/context/injection.py +226 -0
- contextsynapse/context/interaction_graphifier.py +562 -0
- contextsynapse/context/layers.py +574 -0
- contextsynapse/context/projection.py +1195 -0
- contextsynapse/context/promotion.py +634 -0
- contextsynapse/context/proof.py +131 -0
- contextsynapse/context/propagation.py +577 -0
- contextsynapse/context/pubsub.py +135 -0
- contextsynapse/context/quality.py +1054 -0
- contextsynapse/context/quality_tracker.py +270 -0
- contextsynapse/context/scoping.py +370 -0
- contextsynapse/context/seed.py +992 -0
- contextsynapse/context/session.py +1142 -0
- contextsynapse/context/session_graph.py +593 -0
- contextsynapse/context/session_memory.py +309 -0
- contextsynapse/context/session_resolver.py +111 -0
- contextsynapse/context/skill_learning.py +237 -0
- contextsynapse/context/smart_budget.py +129 -0
- contextsynapse/context/source_policy.py +242 -0
- contextsynapse/context/store_factory.py +53 -0
- contextsynapse/context/sync.py +648 -0
- contextsynapse/context/token.py +236 -0
- contextsynapse/context/vector_integration.py +297 -0
- contextsynapse/context/webhooks.py +194 -0
- contextsynapse/context/working_memory.py +224 -0
- contextsynapse/core/__init__.py +18 -0
- contextsynapse/core/archiver.py +237 -0
- contextsynapse/core/change_stream.py +282 -0
- contextsynapse/core/checkpoint.py +310 -0
- contextsynapse/core/cloud_storage.py +387 -0
- contextsynapse/core/context_state.py +651 -0
- contextsynapse/core/cron.py +324 -0
- contextsynapse/core/datasource.py +643 -0
- contextsynapse/core/db.py +289 -0
- contextsynapse/core/federation.py +283 -0
- contextsynapse/core/graph_coordinator.py +201 -0
- contextsynapse/core/graph_intelligence.py +388 -0
- contextsynapse/core/graph_structures.py +106 -0
- contextsynapse/core/graph_sync.py +157 -0
- contextsynapse/core/hybrid_graph_storage.py +2998 -0
- contextsynapse/core/lifecycle.py +691 -0
- contextsynapse/core/multiworker.py +378 -0
- contextsynapse/core/pruning.py +312 -0
- contextsynapse/core/redis_registry.py +524 -0
- contextsynapse/core/registry.py +1043 -0
- contextsynapse/core/registry_factory.py +51 -0
- contextsynapse/core/registry_metadata.py +294 -0
- contextsynapse/core/replay.py +354 -0
- contextsynapse/core/replication.py +339 -0
- contextsynapse/core/time_travel.py +260 -0
- contextsynapse/core/write_behind.py +152 -0
- contextsynapse/core/write_context.py +39 -0
- contextsynapse/db/__init__.py +1 -0
- contextsynapse/db/postgres.py +267 -0
- contextsynapse/db/profile.py +236 -0
- contextsynapse/db/rules.py +328 -0
- contextsynapse/demo/__init__.py +0 -0
- contextsynapse/demo/report.py +147 -0
- contextsynapse/demo/runner.py +520 -0
- contextsynapse/demo/scenario.py +165 -0
- contextsynapse/demo/session_demo.py +280 -0
- contextsynapse/demo/session_report.py +143 -0
- contextsynapse/demo/universal_runner.py +257 -0
- contextsynapse/engine/__init__.py +15 -0
- contextsynapse/engine/core.py +555 -0
- contextsynapse/extraction/__init__.py +69 -0
- contextsynapse/extraction/cli.py +274 -0
- contextsynapse/extraction/config.py +107 -0
- contextsynapse/extraction/examples/example_usage.py +118 -0
- contextsynapse/extraction/examples/sample_manifest.json +53 -0
- contextsynapse/extraction/examples/sample_normalized.json +185 -0
- contextsynapse/extraction/extract_engine.py +440 -0
- contextsynapse/extraction/extractors/__init__.py +37 -0
- contextsynapse/extraction/extractors/chat_export_extractor.py +189 -0
- contextsynapse/extraction/extractors/csv_extractor.py +83 -0
- contextsynapse/extraction/extractors/docx_extractor.py +129 -0
- contextsynapse/extraction/extractors/excel_extractor.py +152 -0
- contextsynapse/extraction/extractors/html_extractor.py +146 -0
- contextsynapse/extraction/extractors/pdf_extractor.py +635 -0
- contextsynapse/extraction/extractors/pdf_extractor_layoutparser.py +408 -0
- contextsynapse/extraction/extractors/register_extractors.py +63 -0
- contextsynapse/extraction/extractors/text_extractor.py +65 -0
- contextsynapse/extraction/extractors/txt_extractor.py +120 -0
- contextsynapse/extraction/extractors/website_extractor.py +246 -0
- contextsynapse/extraction/fact_extractor.py +236 -0
- contextsynapse/extraction/file_manager.py +353 -0
- contextsynapse/extraction/hierarchy.py +239 -0
- contextsynapse/extraction/id_generator.py +450 -0
- contextsynapse/extraction/layout_detector.py +409 -0
- contextsynapse/extraction/llm_entity_extractor.py +301 -0
- contextsynapse/extraction/normalized_store.py +480 -0
- contextsynapse/extraction/normalizer.py +594 -0
- contextsynapse/extraction/parquet_writer.py +598 -0
- contextsynapse/extraction/post_processors/__init__.py +40 -0
- contextsynapse/extraction/post_processors/column_reorganizer.py +507 -0
- contextsynapse/extraction/ray_normalization.py +233 -0
- contextsynapse/extraction/ray_runner.py +207 -0
- contextsynapse/extraction/redis_schema_registry.py +222 -0
- contextsynapse/extraction/registry.py +204 -0
- contextsynapse/extraction/schema_loader.py +663 -0
- contextsynapse/extraction/schema_registry.py +230 -0
- contextsynapse/extraction/semantic_normalizer.py +245 -0
- contextsynapse/extraction/version_manager.py +314 -0
- contextsynapse/gateway/__init__.py +24 -0
- contextsynapse/gateway/action_emitter.py +212 -0
- contextsynapse/gateway/cost_tracker.py +508 -0
- contextsynapse/gateway/gateway.py +377 -0
- contextsynapse/gateway/policy.py +303 -0
- contextsynapse/governance/__init__.py +13 -0
- contextsynapse/governance/layer.py +513 -0
- contextsynapse/ingestion/__init__.py +15 -0
- contextsynapse/ingestion/amplifier.py +288 -0
- contextsynapse/ingestion/async_ingest.py +463 -0
- contextsynapse/ingestion/chunker.py +303 -0
- contextsynapse/ingestion/chunking.py +58 -0
- contextsynapse/ingestion/cleaner.py +190 -0
- contextsynapse/ingestion/cleanse.py +252 -0
- contextsynapse/ingestion/connectors/__init__.py +38 -0
- contextsynapse/ingestion/connectors/base.py +300 -0
- contextsynapse/ingestion/connectors/chatgpt_memory.py +247 -0
- contextsynapse/ingestion/connectors/claude_memory.py +274 -0
- contextsynapse/ingestion/connectors/sap_ingestor.py +208 -0
- contextsynapse/ingestion/content_detector.py +80 -0
- contextsynapse/ingestion/content_monitor.py +502 -0
- contextsynapse/ingestion/dedup.py +106 -0
- contextsynapse/ingestion/feed.py +640 -0
- contextsynapse/ingestion/filters.py +461 -0
- contextsynapse/ingestion/graph_builder.py +705 -0
- contextsynapse/ingestion/input_classifier.py +307 -0
- contextsynapse/ingestion/job_manager.py +571 -0
- contextsynapse/ingestion/llm_extractor.py +610 -0
- contextsynapse/ingestion/orchestrator.py +116 -0
- contextsynapse/ingestion/parsers/__init__.py +19 -0
- contextsynapse/ingestion/parsers/graph_parser_registry.py +103 -0
- contextsynapse/ingestion/parsers/graphml_parser.py +141 -0
- contextsynapse/ingestion/parsers/jsonld_parser.py +171 -0
- contextsynapse/ingestion/parsers/rdf_parser.py +150 -0
- contextsynapse/ingestion/pipeline_context.py +228 -0
- contextsynapse/ingestion/pipeline_store.py +1195 -0
- contextsynapse/ingestion/queue.py +133 -0
- contextsynapse/ingestion/reprocess.py +179 -0
- contextsynapse/ingestion/scenario_router.py +224 -0
- contextsynapse/ingestion/schema_extractor.py +759 -0
- contextsynapse/ingestion/schema_validator.py +274 -0
- contextsynapse/ingestion/sdlc_ingest.py +87 -0
- contextsynapse/ingestion/smart_ingest.py +2451 -0
- contextsynapse/ingestion/source_connector.py +171 -0
- contextsynapse/ingestion/stage_executor.py +2095 -0
- contextsynapse/ingestion/stages/__init__.py +9 -0
- contextsynapse/ingestion/stages/tabular_to_graph.py +289 -0
- contextsynapse/ingestion/strategies.py +386 -0
- contextsynapse/ingestion/universal/__init__.py +4 -0
- contextsynapse/ingestion/universal/_operator_registry.py +59 -0
- contextsynapse/ingestion/universal/ingest_content.py +32 -0
- contextsynapse/ingestion/universal/operators/__init__.py +1 -0
- contextsynapse/ingestion/universal/operators/base.py +19 -0
- contextsynapse/ingestion/universal/operators/build_edges.py +75 -0
- contextsynapse/ingestion/universal/operators/cluster_topics.py +397 -0
- contextsynapse/ingestion/universal/operators/deduplicate.py +45 -0
- contextsynapse/ingestion/universal/operators/detect_signals.py +90 -0
- contextsynapse/ingestion/universal/operators/embed.py +80 -0
- contextsynapse/ingestion/universal/operators/extract_entities.py +253 -0
- contextsynapse/ingestion/universal/operators/extract_preferences.py +117 -0
- contextsynapse/ingestion/universal/operators/filter_content.py +69 -0
- contextsynapse/ingestion/universal/operators/index_bm25.py +52 -0
- contextsynapse/ingestion/universal/operators/infer_domains.py +69 -0
- contextsynapse/ingestion/universal/operators/link_cross_reference.py +210 -0
- contextsynapse/ingestion/universal/operators/parse_records.py +66 -0
- contextsynapse/ingestion/universal/operators/resolve_entities.py +261 -0
- contextsynapse/ingestion/universal/operators/scanners/__init__.py +16 -0
- contextsynapse/ingestion/universal/operators/scanners/edges.py +173 -0
- contextsynapse/ingestion/universal/operators/scanners/git.py +187 -0
- contextsynapse/ingestion/universal/operators/scanners/github.py +135 -0
- contextsynapse/ingestion/universal/operators/scanners/github_api.py +509 -0
- contextsynapse/ingestion/universal/operators/scanners/helpers.py +67 -0
- contextsynapse/ingestion/universal/operators/scanners/jira.py +65 -0
- contextsynapse/ingestion/universal/operators/scanners/llm_enrichment.py +82 -0
- contextsynapse/ingestion/universal/operators/scanners/repo.py +149 -0
- contextsynapse/ingestion/universal/operators/sdlc_scan.py +213 -0
- contextsynapse/ingestion/universal/operators/spec_scanner.py +149 -0
- contextsynapse/ingestion/universal/operators/store_documents.py +72 -0
- contextsynapse/ingestion/universal/operators/synthesize_cu.py +180 -0
- contextsynapse/ingestion/universal/operators/validate_gate.py +65 -0
- contextsynapse/ingestion/universal/parsers/__init__.py +5 -0
- contextsynapse/ingestion/universal/parsers/record_parser.py +203 -0
- contextsynapse/ingestion/universal/parsers/turn_parser.py +329 -0
- contextsynapse/ingestion/universal/shared_signals.py +161 -0
- contextsynapse/ingestion/universal/stage_executor.py +219 -0
- contextsynapse/ingestion/web_crawler.py +265 -0
- contextsynapse/intelligence/__init__.py +53 -0
- contextsynapse/intelligence/alerts.py +198 -0
- contextsynapse/intelligence/collector.py +655 -0
- contextsynapse/intelligence/comparison.py +198 -0
- contextsynapse/intelligence/config.py +56 -0
- contextsynapse/intelligence/conflict_detector.py +182 -0
- contextsynapse/intelligence/context_gaps.py +192 -0
- contextsynapse/intelligence/context_radar.py +135 -0
- contextsynapse/intelligence/correlation_engine.py +416 -0
- contextsynapse/intelligence/correlation_template.py +120 -0
- contextsynapse/intelligence/dedup.py +296 -0
- contextsynapse/intelligence/event_bus.py +91 -0
- contextsynapse/intelligence/feedback_loop.py +101 -0
- contextsynapse/intelligence/freshness.py +307 -0
- contextsynapse/intelligence/fundamental_feed.py +551 -0
- contextsynapse/intelligence/geo_data.py +170 -0
- contextsynapse/intelligence/geo_reference.py +112 -0
- contextsynapse/intelligence/graph_fusion.py +465 -0
- contextsynapse/intelligence/heuristics.py +176 -0
- contextsynapse/intelligence/impact_tracker.py +336 -0
- contextsynapse/intelligence/macro_indicators.py +319 -0
- contextsynapse/intelligence/ocr.py +104 -0
- contextsynapse/intelligence/persistence.py +267 -0
- contextsynapse/intelligence/pipeline_health.py +130 -0
- contextsynapse/intelligence/price_feed.py +221 -0
- contextsynapse/intelligence/rate_limiter.py +130 -0
- contextsynapse/intelligence/reactive.py +411 -0
- contextsynapse/intelligence/runtime_context.py +390 -0
- contextsynapse/intelligence/sentiment_decay.py +426 -0
- contextsynapse/intelligence/session.py +277 -0
- contextsynapse/intelligence/signal_hierarchy.py +438 -0
- contextsynapse/intelligence/signals.py +185 -0
- contextsynapse/intelligence/source_watcher.py +132 -0
- contextsynapse/intelligence/table_enricher.py +256 -0
- contextsynapse/intelligence/tagger.py +319 -0
- contextsynapse/intelligence/timeseries.py +209 -0
- contextsynapse/intelligence/tracker.py +318 -0
- contextsynapse/intelligence/watchdog.py +392 -0
- contextsynapse/intelligence/watchdog_manager.py +263 -0
- contextsynapse/intelligence/webhook_receiver.py +165 -0
- contextsynapse/intelligence/weight_learner.py +234 -0
- contextsynapse/llm/__init__.py +8 -0
- contextsynapse/llm/client.py +348 -0
- contextsynapse/llm/rate_limiter.py +166 -0
- contextsynapse/logging_config.py +2 -0
- contextsynapse/marketplace/__init__.py +7 -0
- contextsynapse/marketplace/registry.py +173 -0
- contextsynapse/mcp/__init__.py +11 -0
- contextsynapse/mcp/__main__.py +4 -0
- contextsynapse/mcp/auth_middleware.py +212 -0
- contextsynapse/mcp/connection_pool.py +128 -0
- contextsynapse/mcp/server.py +986 -0
- contextsynapse/mcp/session_context.py +67 -0
- contextsynapse/memory/__init__.py +34 -0
- contextsynapse/memory/engine.py +475 -0
- contextsynapse/memory/temporal.py +327 -0
- contextsynapse/metadata/__init__.py +12 -0
- contextsynapse/metadata/metadata_db.py +272 -0
- contextsynapse/metadata/metadata_query_tool.py +300 -0
- contextsynapse/metadata/metadata_tracker.py +425 -0
- contextsynapse/models/__init__.py +27 -0
- contextsynapse/models/embedding_service.py +360 -0
- contextsynapse/models/model_registry.py +228 -0
- contextsynapse/multiworker.py +2 -0
- contextsynapse/pipelines/__init__.py +1 -0
- contextsynapse/pipelines/executor.py +851 -0
- contextsynapse/pipelines/filters.py +189 -0
- contextsynapse/pipelines/models.py +176 -0
- contextsynapse/pipelines/scheduler.py +268 -0
- contextsynapse/playground/__init__.py +1 -0
- contextsynapse/playground/templates.py +85 -0
- contextsynapse/plugins/__init__.py +43 -0
- contextsynapse/plugins/api.py +66 -0
- contextsynapse/plugins/base.py +350 -0
- contextsynapse/plugins/domain_interface.py +508 -0
- contextsynapse/plugins/domain_loader.py +241 -0
- contextsynapse/plugins/loader.py +148 -0
- contextsynapse/plugins/registry.py +265 -0
- contextsynapse/project/__init__.py +11 -0
- contextsynapse/project/code_context.py +807 -0
- contextsynapse/project/connectors/__init__.py +4 -0
- contextsynapse/project/connectors/github_connector.py +113 -0
- contextsynapse/project/connectors/jira_connector.py +160 -0
- contextsynapse/project/graph.py +1023 -0
- contextsynapse/project/project_context.py +545 -0
- contextsynapse/project/scanner_registry.py +61 -0
- contextsynapse/project/schema_manager.py +322 -0
- contextsynapse/project/schemas/knowledge.yaml +59 -0
- contextsynapse/project/schemas/sdlc.yaml +369 -0
- contextsynapse/project/sdlc_schema.py +172 -0
- contextsynapse/project/spec_parser.py +85 -0
- contextsynapse/project/task_lock.py +181 -0
- contextsynapse/project/task_stream.py +203 -0
- contextsynapse/project/team_tools.py +537 -0
- contextsynapse/py.typed +0 -0
- contextsynapse/realtime/__init__.py +6 -0
- contextsynapse/realtime/collaboration.py +147 -0
- contextsynapse/rules/__init__.py +10 -0
- contextsynapse/rules/engine.py +116 -0
- contextsynapse/rules/evaluators/__init__.py +6 -0
- contextsynapse/rules/evaluators/aggregate.py +205 -0
- contextsynapse/rules/evaluators/custom.py +170 -0
- contextsynapse/rules/evaluators/pre_action.py +125 -0
- contextsynapse/rules/evaluators/temporal.py +129 -0
- contextsynapse/rules/loader.py +52 -0
- contextsynapse/rules/models.py +124 -0
- contextsynapse/rules/universal.py +393 -0
- contextsynapse/scheduler.py +229 -0
- contextsynapse/schema/__init__.py +42 -0
- contextsynapse/schema/compiler.py +252 -0
- contextsynapse/schema/composer.py +158 -0
- contextsynapse/schema/dedup.py +73 -0
- contextsynapse/schema/derivation.py +172 -0
- contextsynapse/schema/sdl.py +326 -0
- contextsynapse/schema/validation_gate.py +124 -0
- contextsynapse/sdk/__init__.py +63 -0
- contextsynapse/sdk/agent.py +214 -0
- contextsynapse/sdk/client.py +269 -0
- contextsynapse/sdk/exceptions.py +23 -0
- contextsynapse/sdk/models.py +78 -0
- contextsynapse/sdk/session.py +408 -0
- contextsynapse/sdk/tracked.py +218 -0
- contextsynapse/sdk/worker.py +266 -0
- contextsynapse/sdk/workspace_client.py +98 -0
- contextsynapse/sdk/ws.py +41 -0
- contextsynapse/search/__init__.py +20 -0
- contextsynapse/search/embedding_cache.py +140 -0
- contextsynapse/search/enhanced_search.py +152 -0
- contextsynapse/search/fulltext.py +130 -0
- contextsynapse/search/graph_search.py +634 -0
- contextsynapse/search/lmdb_index.py +1218 -0
- contextsynapse/search/nl_to_aiql.py +199 -0
- contextsynapse/search/rag.py +1346 -0
- contextsynapse/search/rag_cache.py +167 -0
- contextsynapse/search/reasoning_chain.py +231 -0
- contextsynapse/search/redis_search.py +207 -0
- contextsynapse/search/retrieval_quality.py +314 -0
- contextsynapse/search/semantic_query.py +455 -0
- contextsynapse/search/text_resolver.py +159 -0
- contextsynapse/search/whoosh_search.py +373 -0
- contextsynapse/security/__init__.py +17 -0
- contextsynapse/security/audit.py +297 -0
- contextsynapse/security/audit_logger.py +97 -0
- contextsynapse/security/audit_trail.py +270 -0
- contextsynapse/security/auth_unified.py +284 -0
- contextsynapse/security/auto_tagger.py +230 -0
- contextsynapse/security/data_security.py +218 -0
- contextsynapse/security/encryption.py +207 -0
- contextsynapse/security/identity.py +447 -0
- contextsynapse/security/jwt_identity.py +59 -0
- contextsynapse/security/middleware.py +336 -0
- contextsynapse/security/pii.py +233 -0
- contextsynapse/security/rbac.py +96 -0
- contextsynapse/security/rls.py +205 -0
- contextsynapse/security/sanitize.py +233 -0
- contextsynapse/security/scoped_encryption.py +131 -0
- contextsynapse/security/tenant.py +401 -0
- contextsynapse/shield/__init__.py +21 -0
- contextsynapse/shield/anomaly.py +56 -0
- contextsynapse/shield/permissions.py +99 -0
- contextsynapse/shield/profile.py +103 -0
- contextsynapse/shield/shield.py +108 -0
- contextsynapse/shield/trust_engine.py +69 -0
- contextsynapse/skills/__init__.py +14 -0
- contextsynapse/skills/engine.py +233 -0
- contextsynapse/skills/loader.py +59 -0
- contextsynapse/skills/models.py +139 -0
- contextsynapse/storage/__init__.py +7 -0
- contextsynapse/storage/cache.py +208 -0
- contextsynapse/storage/columnar_store_v2.py +178 -0
- contextsynapse/storage/csr_graph_storage.py +587 -0
- contextsynapse/storage/csr_store.py +388 -0
- contextsynapse/storage/document_store.py +491 -0
- contextsynapse/storage/hnsw_index.py +294 -0
- contextsynapse/storage/hybrid_store_v2.py +278 -0
- contextsynapse/storage/lmdb_graph_storage.py +366 -0
- contextsynapse/storage/migration.py +279 -0
- contextsynapse/storage/namespace_store.py +346 -0
- contextsynapse/storage/pure_graph_storage.py +185 -0
- contextsynapse/storage/redis_graph_adapter.py +186 -0
- contextsynapse/storage/redis_graph_storage.py +375 -0
- contextsynapse/storage/router/__init__.py +29 -0
- contextsynapse/storage/router/duckdb_store.py +398 -0
- contextsynapse/storage/router/factory.py +83 -0
- contextsynapse/storage/router/interface.py +125 -0
- contextsynapse/storage/router/postgres_store.py +339 -0
- contextsynapse/storage/router/resolver.py +192 -0
- contextsynapse/storage/router/router.py +300 -0
- contextsynapse/storage/single_file_storage.py +391 -0
- contextsynapse/storage/storage_manager.py +214 -0
- contextsynapse/storage/storage_query_optimizer.py +314 -0
- contextsynapse/storage/wal.py +359 -0
- contextsynapse/temporal/__init__.py +0 -0
- contextsynapse/temporal/storage.py +106 -0
- contextsynapse/tests/__init__.py +11 -0
- contextsynapse/tests/regression/__init__.py +1 -0
- contextsynapse/tests/regression/add_metadata.py +395 -0
- contextsynapse/tests/regression/add_skip_flags.py +85 -0
- contextsynapse/tests/regression/cases/test_basic_create_node.yaml +20 -0
- contextsynapse/tests/regression/cases/test_basic_select.yaml +21 -0
- contextsynapse/tests/regression/cases/test_blockchain_audit_trail.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_audit_trail_filtered.yaml +39 -0
- contextsynapse/tests/regression/cases/test_blockchain_blocks_range.yaml +39 -0
- contextsynapse/tests/regression/cases/test_blockchain_get_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_latest_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_length.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_merkle_root.yaml +37 -0
- contextsynapse/tests/regression/cases/test_blockchain_verify.yaml +35 -0
- contextsynapse/tests/regression/cases/test_blockchain_verify_block.yaml +35 -0
- contextsynapse/tests/regression/cases/test_create_edge.yaml +28 -0
- contextsynapse/tests/regression/cases/test_delete_edge_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_delete_node_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_evaluation_mrr.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_ndcg.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_precision.yaml +40 -0
- contextsynapse/tests/regression/cases/test_evaluation_recall.yaml +40 -0
- contextsynapse/tests/regression/cases/test_group_by_basic.yaml +56 -0
- contextsynapse/tests/regression/cases/test_match_node_basic.yaml +29 -0
- contextsynapse/tests/regression/cases/test_match_node_with_order_limit.yaml +31 -0
- contextsynapse/tests/regression/cases/test_namespace_switch.yaml +18 -0
- contextsynapse/tests/regression/cases/test_pipeline_cascaded.yaml +61 -0
- contextsynapse/tests/regression/cases/test_pipeline_fixed_size_chunking.yaml +48 -0
- contextsynapse/tests/regression/cases/test_pipeline_gpt4_chunking.yaml +54 -0
- contextsynapse/tests/regression/cases/test_pipeline_hybrid_indexing.yaml +57 -0
- contextsynapse/tests/regression/cases/test_pipeline_large_embedding.yaml +52 -0
- contextsynapse/tests/regression/cases/test_pipeline_paragraph_chunking.yaml +49 -0
- contextsynapse/tests/regression/cases/test_pipeline_recursive_chunking.yaml +47 -0
- contextsynapse/tests/regression/cases/test_pipeline_run_pipeline.yaml +56 -0
- contextsynapse/tests/regression/cases/test_pipeline_section_based_chunking.yaml +53 -0
- contextsynapse/tests/regression/cases/test_pipeline_semantic_chunking.yaml +51 -0
- contextsynapse/tests/regression/cases/test_pipeline_with_entity_extraction.yaml +57 -0
- contextsynapse/tests/regression/cases/test_pipeline_with_relationships.yaml +62 -0
- contextsynapse/tests/regression/cases/test_rag_evaluation.yaml +38 -0
- contextsynapse/tests/regression/cases/test_rag_graph_bfs_with_eval.yaml +48 -0
- contextsynapse/tests/regression/cases/test_rag_graph_centrality.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_centrality_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_community.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_community_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_dfs_with_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_multi_hop_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_path.yaml +40 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_basic.yaml +38 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_bfs.yaml +52 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_dfs.yaml +41 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_multi_hop.yaml +43 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_shortest_path.yaml +41 -0
- contextsynapse/tests/regression/cases/test_rag_graph_search_weighted.yaml +43 -0
- contextsynapse/tests/regression/cases/test_rag_graph_shortest_path_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_graph_traversal.yaml +30 -0
- contextsynapse/tests/regression/cases/test_rag_graph_weighted_eval.yaml +35 -0
- contextsynapse/tests/regression/cases/test_rag_hybrid_search.yaml +33 -0
- contextsynapse/tests/regression/cases/test_rag_hybrid_with_prompt_embedding.yaml +44 -0
- contextsynapse/tests/regression/cases/test_rag_keyword_search.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_multi_hop_graph.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_path_based.yaml +34 -0
- contextsynapse/tests/regression/cases/test_rag_profile_based.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_vector_search.yaml +32 -0
- contextsynapse/tests/regression/cases/test_rag_weighted_graph.yaml +34 -0
- contextsynapse/tests/regression/cases/test_rag_weighted_hybrid.yaml +31 -0
- contextsynapse/tests/regression/cases/test_semantic_boundary_chunking.yaml +57 -0
- contextsynapse/tests/regression/cases/test_semantic_boundary_detection.yaml +33 -0
- contextsynapse/tests/regression/cases/test_semantic_hash_function.yaml +30 -0
- contextsynapse/tests/regression/cases/test_semantic_hash_with_model.yaml +31 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_fixed_size.yaml +44 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_recursive.yaml +46 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_section.yaml +46 -0
- contextsynapse/tests/regression/cases/test_tcs_ingestion_semantic.yaml +44 -0
- contextsynapse/tests/regression/cases/test_time_travel_as_of.yaml +35 -0
- contextsynapse/tests/regression/cases/test_time_travel_at_commit.yaml +33 -0
- contextsynapse/tests/regression/cases/test_time_travel_at_timestamp.yaml +33 -0
- contextsynapse/tests/regression/cases/test_time_travel_for_system_time_all.yaml +37 -0
- contextsynapse/tests/regression/cases/test_time_travel_versions_between.yaml +39 -0
- contextsynapse/tests/regression/cases/test_traverse_basic.yaml +31 -0
- contextsynapse/tests/regression/cases/test_traverse_max_depth.yaml +34 -0
- contextsynapse/tests/regression/cases/test_update_edge_basic.yaml +29 -0
- contextsynapse/tests/regression/evaluation_metrics.py +227 -0
- contextsynapse/tests/regression/regression_runner.py +1081 -0
- contextsynapse/tests/regression/setup_regression_data.py +212 -0
- contextsynapse/tests/regression/test_basic_only.py +84 -0
- contextsynapse/tests/regression/test_namespace_creation.py +29 -0
- contextsynapse/tests/regression/test_single_fix.py +61 -0
- contextsynapse/tests/regression/test_single_query.py +20 -0
- contextsynapse/tests/regression/validate_test_syntax.py +179 -0
- contextsynapse/tests/test_advanced_features.py +347 -0
- contextsynapse/tests/test_aiql_summary.py +283 -0
- contextsynapse/tests/test_all_aiql_queries.py +948 -0
- contextsynapse/tests/test_all_create_queries.py +213 -0
- contextsynapse/tests/test_api_vs_native.py +280 -0
- contextsynapse/tests/test_chunking_dedup.py +199 -0
- contextsynapse/tests/test_connectors.py +171 -0
- contextsynapse/tests/test_edges_and_match.py +202 -0
- contextsynapse/tests/test_extraction_pipeline_integration.py +581 -0
- contextsynapse/tests/test_name_property.py +224 -0
- contextsynapse/tests/test_node_edge_metadata.py +250 -0
- contextsynapse/tests/test_playground_conn.py +114 -0
- contextsynapse/tests/test_query_cache.py +213 -0
- contextsynapse/tests/test_source_policy_auth.py +348 -0
- contextsynapse/tests/test_temporal_and_domain.py +224 -0
- contextsynapse/tests/test_time_travel_e2e.py +571 -0
- contextsynapse/tests/test_traversal_and_aggregation.py +275 -0
- contextsynapse/tools/__init__.py +36 -0
- contextsynapse/tools/a2a_tools.py +106 -0
- contextsynapse/tools/cognition_tools.py +118 -0
- contextsynapse/tools/context.py +905 -0
- contextsynapse/tools/formatting.py +92 -0
- contextsynapse/tools/gateway_tools.py +186 -0
- contextsynapse/tools/graph.py +1700 -0
- contextsynapse/tools/intelligence.py +235 -0
- contextsynapse/tools/memory.py +118 -0
- contextsynapse/tools/registry.py +373 -0
- contextsynapse/tools/shield_tools.py +159 -0
- contextsynapse/tools/tasks.py +341 -0
- contextsynapse/tools/workspace_tools.py +124 -0
- contextsynapse/utils/__init__.py +15 -0
- contextsynapse/utils/config_loader.py +198 -0
- contextsynapse/utils/env_loader.py +156 -0
- contextsynapse/utils/file_utils.py +288 -0
- contextsynapse/utils/logging_optimizer.py +119 -0
- contextsynapse/vector/__init__.py +30 -0
- contextsynapse/vector/chroma_vector_store.py +152 -0
- contextsynapse/vector/custom_vector_store.py +537 -0
- contextsynapse/vector/faiss_vector_store.py +296 -0
- contextsynapse/vector/qdrant_vector_store.py +163 -0
- contextsynapse/vector/vector_db.py +953 -0
- contextsynapse/vector/vector_db_manager.py +197 -0
- contextsynapse/workspace/__init__.py +27 -0
- contextsynapse/workspace/base.py +94 -0
- contextsynapse/workspace/git.py +228 -0
- contextsynapse/workspace/github.py +78 -0
- contextsynapse/workspace/local.py +73 -0
- contextsynapse/workspace/tools.py +222 -0
- contextsynapse-1.0.0.dist-info/METADATA +847 -0
- contextsynapse-1.0.0.dist-info/RECORD +776 -0
- contextsynapse-1.0.0.dist-info/WHEEL +5 -0
- contextsynapse-1.0.0.dist-info/entry_points.txt +4 -0
- contextsynapse-1.0.0.dist-info/licenses/LICENSE +200 -0
- contextsynapse-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Registry
|
|
3
|
+
=============
|
|
4
|
+
Self-service user accounts for the AIContextDB SaaS layer.
|
|
5
|
+
|
|
6
|
+
Each user belongs to one or more tenants (organisations) via the
|
|
7
|
+
``user_tenants`` join table. On signup a personal tenant is auto-created
|
|
8
|
+
and the user is set as its owner.
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
registry = UserRegistry()
|
|
13
|
+
user, jwt = registry.signup("alice@example.com", "s3cret", "Alice")
|
|
14
|
+
user = registry.authenticate("alice@example.com", "s3cret")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import secrets
|
|
24
|
+
from dataclasses import dataclass, field, asdict
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
from contextsynapse.core.db import (
|
|
30
|
+
IS_POSTGRES, PH,
|
|
31
|
+
connect, dict_cursor, run_ddl,
|
|
32
|
+
integrity_error, column_exists, row_to_dict, serial_pk,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# bcrypt password hashing (direct, no passlib needed)
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
try:
|
|
41
|
+
import bcrypt as _bcrypt
|
|
42
|
+
except ImportError:
|
|
43
|
+
_bcrypt = None
|
|
44
|
+
logger.warning("bcrypt not installed — falling back to SHA-256 password hashing")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _hash_password(password: str) -> str:
|
|
48
|
+
if _bcrypt:
|
|
49
|
+
return _bcrypt.hashpw(password.encode("utf-8")[:72], _bcrypt.gensalt()).decode("utf-8")
|
|
50
|
+
return hashlib.sha256(password.encode()).hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _verify_password(password: str, hashed: str) -> bool:
|
|
54
|
+
if _bcrypt and hashed.startswith("$2"):
|
|
55
|
+
return _bcrypt.checkpw(password.encode("utf-8")[:72], hashed.encode("utf-8"))
|
|
56
|
+
return hashlib.sha256(password.encode()).hexdigest() == hashed
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Data classes
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class UserIdentity:
|
|
65
|
+
"""A registered user."""
|
|
66
|
+
user_id: str
|
|
67
|
+
email: str
|
|
68
|
+
display_name: str
|
|
69
|
+
status: str = "active"
|
|
70
|
+
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
71
|
+
last_login_at: Optional[str] = None
|
|
72
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
is_super_admin: bool = False
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
76
|
+
return asdict(self)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class UserTenantRole:
|
|
81
|
+
"""A user's membership in a tenant."""
|
|
82
|
+
user_id: str
|
|
83
|
+
tenant_id: str
|
|
84
|
+
role: str # owner, admin, contributor, reader
|
|
85
|
+
joined_at: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Registry
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
_EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$")
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Password strength validation
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
_COMMON_PASSWORDS = {
|
|
99
|
+
"password", "123456", "12345678", "qwerty", "abc123", "letmein",
|
|
100
|
+
"admin", "welcome", "monkey", "dragon", "master", "login",
|
|
101
|
+
"password1", "123456789", "1234567890",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def validate_password(password: str) -> Optional[str]:
|
|
106
|
+
"""Return error message if password is weak, None if acceptable.
|
|
107
|
+
|
|
108
|
+
Rules (production):
|
|
109
|
+
- Minimum 8 characters
|
|
110
|
+
- At least 1 uppercase, 1 lowercase, 1 digit
|
|
111
|
+
- Not in common password list
|
|
112
|
+
|
|
113
|
+
For testing/dev (AICONTEXTDB_ENV=dev), relaxes to 6 chars min.
|
|
114
|
+
"""
|
|
115
|
+
import os
|
|
116
|
+
is_dev = os.environ.get("CONTEXTSYNAPSE_ENV") or os.environ.get("AICONTEXTDB_ENV", "").lower() in ("dev", "development", "test")
|
|
117
|
+
min_len = 6 if is_dev else 8
|
|
118
|
+
|
|
119
|
+
if len(password) < min_len:
|
|
120
|
+
return f"Password must be at least {min_len} characters"
|
|
121
|
+
if password.lower() in _COMMON_PASSWORDS:
|
|
122
|
+
return "Password is too common — choose something more unique"
|
|
123
|
+
if not is_dev:
|
|
124
|
+
if not re.search(r"[A-Z]", password):
|
|
125
|
+
return "Password must contain at least one uppercase letter"
|
|
126
|
+
if not re.search(r"[a-z]", password):
|
|
127
|
+
return "Password must contain at least one lowercase letter"
|
|
128
|
+
if not re.search(r"[0-9]", password):
|
|
129
|
+
return "Password must contain at least one digit"
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class UserRegistry:
|
|
134
|
+
"""Persistent user registry backed by SQLite or PostgreSQL."""
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _schema() -> str:
|
|
138
|
+
return f"""
|
|
139
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
140
|
+
user_id TEXT PRIMARY KEY,
|
|
141
|
+
email TEXT NOT NULL UNIQUE,
|
|
142
|
+
password_hash TEXT NOT NULL,
|
|
143
|
+
display_name TEXT NOT NULL,
|
|
144
|
+
status TEXT DEFAULT 'active',
|
|
145
|
+
created_at TEXT NOT NULL,
|
|
146
|
+
last_login_at TEXT,
|
|
147
|
+
config TEXT DEFAULT '{{}}',
|
|
148
|
+
is_super_admin INTEGER DEFAULT 0
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
|
|
153
|
+
|
|
154
|
+
CREATE TABLE IF NOT EXISTS user_tenants (
|
|
155
|
+
user_id TEXT NOT NULL,
|
|
156
|
+
tenant_id TEXT NOT NULL,
|
|
157
|
+
role TEXT DEFAULT 'owner',
|
|
158
|
+
joined_at TEXT NOT NULL,
|
|
159
|
+
PRIMARY KEY (user_id, tenant_id)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_ut_user ON user_tenants(user_id);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_ut_tenant ON user_tenants(tenant_id);
|
|
164
|
+
|
|
165
|
+
CREATE TABLE IF NOT EXISTS team_invitations (
|
|
166
|
+
invite_id TEXT PRIMARY KEY,
|
|
167
|
+
tenant_id TEXT NOT NULL,
|
|
168
|
+
email TEXT NOT NULL,
|
|
169
|
+
role TEXT DEFAULT 'member',
|
|
170
|
+
invited_by TEXT NOT NULL,
|
|
171
|
+
status TEXT DEFAULT 'pending',
|
|
172
|
+
created_at TEXT NOT NULL,
|
|
173
|
+
UNIQUE(tenant_id, email)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_inv_tenant ON team_invitations(tenant_id);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_inv_email ON team_invitations(email);
|
|
178
|
+
|
|
179
|
+
CREATE TABLE IF NOT EXISTS login_attempts (
|
|
180
|
+
id {serial_pk()},
|
|
181
|
+
email TEXT NOT NULL,
|
|
182
|
+
ip_address TEXT,
|
|
183
|
+
success INTEGER DEFAULT 0,
|
|
184
|
+
created_at TEXT NOT NULL
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_la_email ON login_attempts(email);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_la_created ON login_attempts(created_at);
|
|
189
|
+
|
|
190
|
+
CREATE TABLE IF NOT EXISTS email_verifications (
|
|
191
|
+
token TEXT PRIMARY KEY,
|
|
192
|
+
user_id TEXT NOT NULL,
|
|
193
|
+
created_at TEXT NOT NULL,
|
|
194
|
+
expires_at TEXT NOT NULL,
|
|
195
|
+
verified_at TEXT
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_ev_user ON email_verifications(user_id);
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(self, db_path: str = "contextcore_data/users.db"):
|
|
202
|
+
self._db_path = Path(db_path)
|
|
203
|
+
if not IS_POSTGRES:
|
|
204
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
self._conn = connect(str(self._db_path))
|
|
206
|
+
run_ddl(self._conn, self._schema())
|
|
207
|
+
self._migrate()
|
|
208
|
+
|
|
209
|
+
def _exec(self, sql: str, params: tuple = ()):
|
|
210
|
+
"""Execute SQL and return cursor (works for both SQLite and Postgres)."""
|
|
211
|
+
cur = dict_cursor(self._conn)
|
|
212
|
+
cur.execute(sql, params)
|
|
213
|
+
return cur
|
|
214
|
+
|
|
215
|
+
def _commit(self):
|
|
216
|
+
self._conn.commit()
|
|
217
|
+
|
|
218
|
+
def _migrate(self):
|
|
219
|
+
"""Run lightweight schema migrations for existing DBs."""
|
|
220
|
+
if not column_exists(self._conn, "users", "is_super_admin"):
|
|
221
|
+
self._exec(f"ALTER TABLE users ADD COLUMN is_super_admin INTEGER DEFAULT 0")
|
|
222
|
+
self._commit()
|
|
223
|
+
# Normalize legacy role names: viewer→reader, member→contributor
|
|
224
|
+
self._exec("UPDATE user_tenants SET role = 'reader' WHERE role = 'viewer'")
|
|
225
|
+
self._exec("UPDATE user_tenants SET role = 'contributor' WHERE role = 'member'")
|
|
226
|
+
self._commit()
|
|
227
|
+
|
|
228
|
+
def ensure_admin(
|
|
229
|
+
self,
|
|
230
|
+
email: str = "admin@contextsynapse.local",
|
|
231
|
+
password: str = "Admin123!",
|
|
232
|
+
display_name: str = "Admin",
|
|
233
|
+
) -> Optional[UserIdentity]:
|
|
234
|
+
"""Ensure a default admin user exists. Creates one if not found.
|
|
235
|
+
|
|
236
|
+
Called on first startup so there's always a way to log in.
|
|
237
|
+
Returns the admin user, or None if already exists.
|
|
238
|
+
"""
|
|
239
|
+
import os
|
|
240
|
+
email = os.environ.get("CONTEXTCORE_ADMIN_EMAIL", email)
|
|
241
|
+
password = os.environ.get("CONTEXTCORE_ADMIN_PASSWORD", password)
|
|
242
|
+
display_name = os.environ.get("CONTEXTCORE_ADMIN_NAME", display_name)
|
|
243
|
+
|
|
244
|
+
cur = self._exec(f"SELECT user_id FROM users WHERE email = {PH}", (email,))
|
|
245
|
+
row = cur.fetchone()
|
|
246
|
+
if row:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
user_id = secrets.token_hex(12)
|
|
250
|
+
password_hash = _hash_password(password)
|
|
251
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
self._exec(
|
|
255
|
+
f"INSERT INTO users (user_id, email, password_hash, display_name, status, created_at, is_super_admin) "
|
|
256
|
+
f"VALUES ({PH}, {PH}, {PH}, {PH}, 'active', {PH}, 1)",
|
|
257
|
+
(user_id, email, password_hash, display_name, now),
|
|
258
|
+
)
|
|
259
|
+
self._commit()
|
|
260
|
+
logger.info("Default admin created: %s (change password after first login)", email)
|
|
261
|
+
return UserIdentity(
|
|
262
|
+
user_id=user_id, email=email, display_name=display_name,
|
|
263
|
+
created_at=now, is_super_admin=True,
|
|
264
|
+
)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.debug("Admin user creation skipped: %s", e)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# User CRUD
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def create(
|
|
274
|
+
self,
|
|
275
|
+
email: str,
|
|
276
|
+
password: str,
|
|
277
|
+
display_name: str,
|
|
278
|
+
) -> UserIdentity:
|
|
279
|
+
"""Create a new user. Raises ValueError on invalid input or duplicate email."""
|
|
280
|
+
email = email.strip().lower()
|
|
281
|
+
if not _EMAIL_RE.match(email):
|
|
282
|
+
raise ValueError("Invalid email address")
|
|
283
|
+
pwd_err = validate_password(password)
|
|
284
|
+
if pwd_err:
|
|
285
|
+
raise ValueError(pwd_err)
|
|
286
|
+
display_name = display_name.strip()
|
|
287
|
+
if not display_name:
|
|
288
|
+
raise ValueError("Display name is required")
|
|
289
|
+
|
|
290
|
+
user_id = secrets.token_hex(12)
|
|
291
|
+
password_hash = _hash_password(password)
|
|
292
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
self._exec(
|
|
296
|
+
f"INSERT INTO users (user_id, email, password_hash, display_name, status, created_at) "
|
|
297
|
+
f"VALUES ({PH}, {PH}, {PH}, {PH}, 'active', {PH})",
|
|
298
|
+
(user_id, email, password_hash, display_name, now),
|
|
299
|
+
)
|
|
300
|
+
self._commit()
|
|
301
|
+
except integrity_error():
|
|
302
|
+
raise ValueError("A user with this email already exists")
|
|
303
|
+
|
|
304
|
+
return UserIdentity(
|
|
305
|
+
user_id=user_id,
|
|
306
|
+
email=email,
|
|
307
|
+
display_name=display_name,
|
|
308
|
+
created_at=now,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Max failed login attempts before lockout (within window)
|
|
312
|
+
MAX_FAILED_ATTEMPTS = 5
|
|
313
|
+
LOCKOUT_WINDOW_MINUTES = 15
|
|
314
|
+
|
|
315
|
+
def authenticate(self, email: str, password: str,
|
|
316
|
+
ip_address: str = None) -> Optional[UserIdentity]:
|
|
317
|
+
"""Validate email + password with brute-force protection.
|
|
318
|
+
|
|
319
|
+
Returns user if valid, None if bad credentials.
|
|
320
|
+
Raises ValueError if account is locked out.
|
|
321
|
+
"""
|
|
322
|
+
email = email.strip().lower()
|
|
323
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
324
|
+
|
|
325
|
+
# Check for lockout (too many failed attempts)
|
|
326
|
+
if self._is_locked_out(email):
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"Account temporarily locked — too many failed attempts. "
|
|
329
|
+
f"Try again in {self.LOCKOUT_WINDOW_MINUTES} minutes."
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
row = row_to_dict(self._exec(
|
|
333
|
+
f"SELECT * FROM users WHERE email = {PH} AND status = 'active'",
|
|
334
|
+
(email,),
|
|
335
|
+
).fetchone())
|
|
336
|
+
|
|
337
|
+
if not row or not _verify_password(password, row["password_hash"]):
|
|
338
|
+
# Record failed attempt
|
|
339
|
+
self._record_login_attempt(email, ip_address, success=False)
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
# Success — record and clear lockout
|
|
343
|
+
self._record_login_attempt(email, ip_address, success=True)
|
|
344
|
+
|
|
345
|
+
# Update last_login_at
|
|
346
|
+
self._exec(
|
|
347
|
+
f"UPDATE users SET last_login_at = {PH} WHERE user_id = {PH}",
|
|
348
|
+
(now, row["user_id"]),
|
|
349
|
+
)
|
|
350
|
+
self._commit()
|
|
351
|
+
|
|
352
|
+
user = self._row_to_user(row)
|
|
353
|
+
user.last_login_at = now
|
|
354
|
+
return user
|
|
355
|
+
|
|
356
|
+
def _is_locked_out(self, email: str) -> bool:
|
|
357
|
+
"""Check if too many failed login attempts in the lockout window."""
|
|
358
|
+
from datetime import timedelta
|
|
359
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(minutes=self.LOCKOUT_WINDOW_MINUTES)).isoformat()
|
|
360
|
+
row = row_to_dict(self._exec(
|
|
361
|
+
f"SELECT COUNT(*) as cnt FROM login_attempts "
|
|
362
|
+
f"WHERE email = {PH} AND success = 0 AND created_at > {PH}",
|
|
363
|
+
(email, cutoff),
|
|
364
|
+
).fetchone())
|
|
365
|
+
return (row["cnt"] >= self.MAX_FAILED_ATTEMPTS) if row else False
|
|
366
|
+
|
|
367
|
+
def _record_login_attempt(self, email: str, ip_address: str = None,
|
|
368
|
+
success: bool = False):
|
|
369
|
+
"""Record a login attempt for rate limiting."""
|
|
370
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
371
|
+
try:
|
|
372
|
+
self._exec(
|
|
373
|
+
f"INSERT INTO login_attempts (email, ip_address, success, created_at) "
|
|
374
|
+
f"VALUES ({PH}, {PH}, {PH}, {PH})",
|
|
375
|
+
(email, ip_address, 1 if success else 0, now),
|
|
376
|
+
)
|
|
377
|
+
# Clean up old attempts (older than 24h)
|
|
378
|
+
from datetime import timedelta
|
|
379
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
|
|
380
|
+
self._exec(f"DELETE FROM login_attempts WHERE created_at < {PH}", (cutoff,))
|
|
381
|
+
self._commit()
|
|
382
|
+
except Exception:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
# ------------------------------------------------------------------
|
|
386
|
+
# Email verification
|
|
387
|
+
# ------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
def create_verification_token(self, user_id: str, expires_hours: int = 24) -> str:
|
|
390
|
+
"""Create an email verification token. Returns the raw token."""
|
|
391
|
+
from datetime import timedelta
|
|
392
|
+
token = secrets.token_urlsafe(32)
|
|
393
|
+
now = datetime.now(timezone.utc)
|
|
394
|
+
expires = now + timedelta(hours=expires_hours)
|
|
395
|
+
self._exec(
|
|
396
|
+
f"INSERT INTO email_verifications (token, user_id, created_at, expires_at) "
|
|
397
|
+
f"VALUES ({PH}, {PH}, {PH}, {PH})",
|
|
398
|
+
(token, user_id, now.isoformat(), expires.isoformat()),
|
|
399
|
+
)
|
|
400
|
+
self._commit()
|
|
401
|
+
return token
|
|
402
|
+
|
|
403
|
+
def verify_email(self, token: str) -> Optional[str]:
|
|
404
|
+
"""Verify an email token. Returns user_id if valid, None otherwise."""
|
|
405
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
406
|
+
row = row_to_dict(self._exec(
|
|
407
|
+
f"SELECT * FROM email_verifications "
|
|
408
|
+
f"WHERE token = {PH} AND verified_at IS NULL AND expires_at > {PH}",
|
|
409
|
+
(token, now),
|
|
410
|
+
).fetchone())
|
|
411
|
+
if not row:
|
|
412
|
+
return None
|
|
413
|
+
user_id = row["user_id"]
|
|
414
|
+
self._exec(
|
|
415
|
+
f"UPDATE email_verifications SET verified_at = {PH} WHERE token = {PH}",
|
|
416
|
+
(now, token),
|
|
417
|
+
)
|
|
418
|
+
# Mark user as email-verified in config
|
|
419
|
+
user = self.get(user_id)
|
|
420
|
+
if user:
|
|
421
|
+
config = dict(user.config)
|
|
422
|
+
config["email_verified"] = True
|
|
423
|
+
config["email_verified_at"] = now
|
|
424
|
+
self._exec(
|
|
425
|
+
f"UPDATE users SET config = {PH} WHERE user_id = {PH}",
|
|
426
|
+
(json.dumps(config), user_id),
|
|
427
|
+
)
|
|
428
|
+
self._commit()
|
|
429
|
+
return user_id
|
|
430
|
+
|
|
431
|
+
def is_email_verified(self, user_id: str) -> bool:
|
|
432
|
+
"""Check if a user's email is verified."""
|
|
433
|
+
user = self.get(user_id)
|
|
434
|
+
if not user:
|
|
435
|
+
return False
|
|
436
|
+
return user.config.get("email_verified", False)
|
|
437
|
+
|
|
438
|
+
def get(self, user_id: str) -> Optional[UserIdentity]:
|
|
439
|
+
row = row_to_dict(self._exec(
|
|
440
|
+
f"SELECT * FROM users WHERE user_id = {PH} AND status != 'deleted'",
|
|
441
|
+
(user_id,),
|
|
442
|
+
).fetchone())
|
|
443
|
+
return self._row_to_user(row) if row else None
|
|
444
|
+
|
|
445
|
+
def get_by_email(self, email: str) -> Optional[UserIdentity]:
|
|
446
|
+
email = email.strip().lower()
|
|
447
|
+
row = row_to_dict(self._exec(
|
|
448
|
+
f"SELECT * FROM users WHERE email = {PH} AND status != 'deleted'",
|
|
449
|
+
(email,),
|
|
450
|
+
).fetchone())
|
|
451
|
+
return self._row_to_user(row) if row else None
|
|
452
|
+
|
|
453
|
+
def update(self, user_id: str, display_name: Optional[str] = None,
|
|
454
|
+
password: Optional[str] = None) -> Optional[UserIdentity]:
|
|
455
|
+
"""Update user profile fields."""
|
|
456
|
+
user = self.get(user_id)
|
|
457
|
+
if not user:
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
if display_name:
|
|
461
|
+
self._exec(
|
|
462
|
+
f"UPDATE users SET display_name = {PH} WHERE user_id = {PH}",
|
|
463
|
+
(display_name.strip(), user_id),
|
|
464
|
+
)
|
|
465
|
+
if password:
|
|
466
|
+
if len(password) < 6:
|
|
467
|
+
raise ValueError("Password must be at least 6 characters")
|
|
468
|
+
self._exec(
|
|
469
|
+
f"UPDATE users SET password_hash = {PH} WHERE user_id = {PH}",
|
|
470
|
+
(_hash_password(password), user_id),
|
|
471
|
+
)
|
|
472
|
+
self._commit()
|
|
473
|
+
return self.get(user_id)
|
|
474
|
+
|
|
475
|
+
# ------------------------------------------------------------------
|
|
476
|
+
# User ↔ Tenant membership
|
|
477
|
+
# ------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
def link_tenant(self, user_id: str, tenant_id: str, role: str = "owner") -> None:
|
|
480
|
+
"""Associate a user with a tenant."""
|
|
481
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
482
|
+
if IS_POSTGRES:
|
|
483
|
+
self._exec(
|
|
484
|
+
f"INSERT INTO user_tenants (user_id, tenant_id, role, joined_at) "
|
|
485
|
+
f"VALUES ({PH}, {PH}, {PH}, {PH}) "
|
|
486
|
+
f"ON CONFLICT (user_id, tenant_id) DO UPDATE SET role = EXCLUDED.role, joined_at = EXCLUDED.joined_at",
|
|
487
|
+
(user_id, tenant_id, role, now),
|
|
488
|
+
)
|
|
489
|
+
else:
|
|
490
|
+
self._exec(
|
|
491
|
+
f"INSERT OR REPLACE INTO user_tenants (user_id, tenant_id, role, joined_at) "
|
|
492
|
+
f"VALUES ({PH}, {PH}, {PH}, {PH})",
|
|
493
|
+
(user_id, tenant_id, role, now),
|
|
494
|
+
)
|
|
495
|
+
self._commit()
|
|
496
|
+
|
|
497
|
+
def get_user_tenants(self, user_id: str) -> List[UserTenantRole]:
|
|
498
|
+
"""Get all tenants a user belongs to."""
|
|
499
|
+
rows = self._exec(
|
|
500
|
+
f"SELECT * FROM user_tenants WHERE user_id = {PH}",
|
|
501
|
+
(user_id,),
|
|
502
|
+
).fetchall()
|
|
503
|
+
return [
|
|
504
|
+
UserTenantRole(
|
|
505
|
+
user_id=r["user_id"],
|
|
506
|
+
tenant_id=r["tenant_id"],
|
|
507
|
+
role=r["role"],
|
|
508
|
+
joined_at=r["joined_at"],
|
|
509
|
+
)
|
|
510
|
+
for r in [row_to_dict(r) for r in rows]
|
|
511
|
+
]
|
|
512
|
+
|
|
513
|
+
def get_tenant_members(self, tenant_id: str) -> List[UserTenantRole]:
|
|
514
|
+
"""Get all users in a tenant."""
|
|
515
|
+
rows = self._exec(
|
|
516
|
+
f"SELECT * FROM user_tenants WHERE tenant_id = {PH}",
|
|
517
|
+
(tenant_id,),
|
|
518
|
+
).fetchall()
|
|
519
|
+
return [
|
|
520
|
+
UserTenantRole(
|
|
521
|
+
user_id=r["user_id"],
|
|
522
|
+
tenant_id=r["tenant_id"],
|
|
523
|
+
role=r["role"],
|
|
524
|
+
joined_at=r["joined_at"],
|
|
525
|
+
)
|
|
526
|
+
for r in [row_to_dict(r) for r in rows]
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
def get_primary_tenant_id(self, user_id: str) -> Optional[str]:
|
|
530
|
+
"""Get the first (owner) tenant for a user."""
|
|
531
|
+
row = row_to_dict(self._exec(
|
|
532
|
+
f"SELECT tenant_id FROM user_tenants WHERE user_id = {PH} AND role = 'owner' LIMIT 1",
|
|
533
|
+
(user_id,),
|
|
534
|
+
).fetchone())
|
|
535
|
+
if row:
|
|
536
|
+
return row["tenant_id"]
|
|
537
|
+
# Fallback to any tenant
|
|
538
|
+
row = row_to_dict(self._exec(
|
|
539
|
+
f"SELECT tenant_id FROM user_tenants WHERE user_id = {PH} LIMIT 1",
|
|
540
|
+
(user_id,),
|
|
541
|
+
).fetchone())
|
|
542
|
+
return row["tenant_id"] if row else None
|
|
543
|
+
|
|
544
|
+
# ------------------------------------------------------------------
|
|
545
|
+
# Team invitations
|
|
546
|
+
# ------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
def create_invitation(self, tenant_id: str, email: str, role: str, invited_by: str) -> Dict[str, Any]:
|
|
549
|
+
"""Create a team invitation. Returns the invitation dict."""
|
|
550
|
+
email = email.strip().lower()
|
|
551
|
+
invite_id = secrets.token_hex(12)
|
|
552
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
self._exec(
|
|
556
|
+
f"INSERT INTO team_invitations (invite_id, tenant_id, email, role, invited_by, status, created_at) "
|
|
557
|
+
f"VALUES ({PH}, {PH}, {PH}, {PH}, {PH}, 'pending', {PH})",
|
|
558
|
+
(invite_id, tenant_id, email, role, invited_by, now),
|
|
559
|
+
)
|
|
560
|
+
self._commit()
|
|
561
|
+
except integrity_error():
|
|
562
|
+
raise ValueError("An invitation for this email already exists")
|
|
563
|
+
|
|
564
|
+
return {"invite_id": invite_id, "tenant_id": tenant_id, "email": email, "role": role, "status": "pending", "created_at": now}
|
|
565
|
+
|
|
566
|
+
def list_invitations(self, tenant_id: str) -> List[Dict[str, Any]]:
|
|
567
|
+
"""List all pending invitations for a tenant."""
|
|
568
|
+
rows = self._exec(
|
|
569
|
+
f"SELECT * FROM team_invitations WHERE tenant_id = {PH} AND status = 'pending' ORDER BY created_at DESC",
|
|
570
|
+
(tenant_id,),
|
|
571
|
+
).fetchall()
|
|
572
|
+
return [row_to_dict(r) for r in rows]
|
|
573
|
+
|
|
574
|
+
def accept_invitation(self, invite_id: str, user_id: str) -> bool:
|
|
575
|
+
"""Accept an invitation — link user to tenant."""
|
|
576
|
+
row = row_to_dict(self._exec(
|
|
577
|
+
f"SELECT * FROM team_invitations WHERE invite_id = {PH} AND status = 'pending'",
|
|
578
|
+
(invite_id,),
|
|
579
|
+
).fetchone())
|
|
580
|
+
if not row:
|
|
581
|
+
return False
|
|
582
|
+
|
|
583
|
+
self.link_tenant(user_id, row["tenant_id"], role=row["role"])
|
|
584
|
+
self._exec(
|
|
585
|
+
f"UPDATE team_invitations SET status = 'accepted' WHERE invite_id = {PH}",
|
|
586
|
+
(invite_id,),
|
|
587
|
+
)
|
|
588
|
+
self._commit()
|
|
589
|
+
return True
|
|
590
|
+
|
|
591
|
+
def revoke_invitation(self, invite_id: str) -> bool:
|
|
592
|
+
"""Revoke a pending invitation."""
|
|
593
|
+
cur = self._exec(
|
|
594
|
+
f"UPDATE team_invitations SET status = 'revoked' WHERE invite_id = {PH} AND status = 'pending'",
|
|
595
|
+
(invite_id,),
|
|
596
|
+
)
|
|
597
|
+
self._commit()
|
|
598
|
+
return cur.rowcount > 0
|
|
599
|
+
|
|
600
|
+
def remove_member(self, tenant_id: str, user_id: str) -> bool:
|
|
601
|
+
"""Remove a user from a tenant."""
|
|
602
|
+
cur = self._exec(
|
|
603
|
+
f"DELETE FROM user_tenants WHERE tenant_id = {PH} AND user_id = {PH} AND role != 'owner'",
|
|
604
|
+
(tenant_id, user_id),
|
|
605
|
+
)
|
|
606
|
+
self._commit()
|
|
607
|
+
return cur.rowcount > 0
|
|
608
|
+
|
|
609
|
+
def update_member_role(self, tenant_id: str, user_id: str, role: str) -> bool:
|
|
610
|
+
"""Update a member's role (cannot change owner)."""
|
|
611
|
+
cur = self._exec(
|
|
612
|
+
f"UPDATE user_tenants SET role = {PH} WHERE tenant_id = {PH} AND user_id = {PH} AND role != 'owner'",
|
|
613
|
+
(role, tenant_id, user_id),
|
|
614
|
+
)
|
|
615
|
+
self._commit()
|
|
616
|
+
return cur.rowcount > 0
|
|
617
|
+
|
|
618
|
+
# ------------------------------------------------------------------
|
|
619
|
+
# Helpers
|
|
620
|
+
# ------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
def _row_to_user(self, row) -> UserIdentity:
|
|
623
|
+
d = row_to_dict(row) if not isinstance(row, dict) else row
|
|
624
|
+
return UserIdentity(
|
|
625
|
+
user_id=d["user_id"],
|
|
626
|
+
email=d["email"],
|
|
627
|
+
display_name=d["display_name"],
|
|
628
|
+
status=d["status"],
|
|
629
|
+
created_at=d["created_at"],
|
|
630
|
+
last_login_at=d.get("last_login_at"),
|
|
631
|
+
config=json.loads(d["config"]) if d.get("config") else {},
|
|
632
|
+
is_super_admin=bool(d.get("is_super_admin", 0)),
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
def set_super_admin(self, user_id: str, is_super_admin: bool = True) -> bool:
|
|
636
|
+
"""Promote or demote a user to/from super_admin (platform-wide)."""
|
|
637
|
+
cur = self._exec(
|
|
638
|
+
f"UPDATE users SET is_super_admin = {PH} WHERE user_id = {PH}",
|
|
639
|
+
(1 if is_super_admin else 0, user_id),
|
|
640
|
+
)
|
|
641
|
+
self._commit()
|
|
642
|
+
return cur.rowcount > 0
|
|
643
|
+
|
|
644
|
+
def close(self):
|
|
645
|
+
if self._conn:
|
|
646
|
+
self._conn.close()
|