gatewaysdk 0.3.2__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.
- gatewaysdk/__init__.py +540 -0
- gatewaysdk/adapter/__init__.py +15 -0
- gatewaysdk/adapter/base.py +94 -0
- gatewaysdk/adapter/messages.py +270 -0
- gatewaysdk/adapter/triplet.py +1026 -0
- gatewaysdk/algorithm/__init__.py +39 -0
- gatewaysdk/algorithm/apo/__init__.py +5 -0
- gatewaysdk/algorithm/apo/apo.py +898 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
- gatewaysdk/algorithm/base.py +262 -0
- gatewaysdk/algorithm/decorator.py +264 -0
- gatewaysdk/algorithm/evals/__init__.py +7 -0
- gatewaysdk/algorithm/evals/evals.py +217 -0
- gatewaysdk/algorithm/fast.py +250 -0
- gatewaysdk/algorithm/gepa/__init__.py +61 -0
- gatewaysdk/algorithm/gepa/adapter.py +495 -0
- gatewaysdk/algorithm/gepa/gepa.py +570 -0
- gatewaysdk/algorithm/gepa/lib/__init__.py +18 -0
- gatewaysdk/algorithm/gepa/lib/adapters/README.md +12 -0
- gatewaysdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +268 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +699 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- gatewaysdk/algorithm/gepa/lib/api.py +382 -0
- gatewaysdk/algorithm/gepa/lib/core/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/core/adapter.py +180 -0
- gatewaysdk/algorithm/gepa/lib/core/data_loader.py +74 -0
- gatewaysdk/algorithm/gepa/lib/core/engine.py +379 -0
- gatewaysdk/algorithm/gepa/lib/core/result.py +233 -0
- gatewaysdk/algorithm/gepa/lib/core/state.py +636 -0
- gatewaysdk/algorithm/gepa/lib/examples/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/examples/aime.py +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +456 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +820 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
- gatewaysdk/algorithm/gepa/lib/gepa_utils.py +117 -0
- gatewaysdk/algorithm/gepa/lib/logging/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
- gatewaysdk/algorithm/gepa/lib/logging/logger.py +75 -0
- gatewaysdk/algorithm/gepa/lib/logging/utils.py +103 -0
- gatewaysdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/base.py +31 -0
- gatewaysdk/algorithm/gepa/lib/proposer/merge.py +357 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
- gatewaysdk/algorithm/gepa/lib/py.typed +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
- gatewaysdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
- gatewaysdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
- gatewaysdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
- gatewaysdk/algorithm/gepa/lib/strategies/instruction_proposal.py +126 -0
- gatewaysdk/algorithm/gepa/lib/utils/__init__.py +10 -0
- gatewaysdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
- gatewaysdk/algorithm/gepa/tracing.py +105 -0
- gatewaysdk/algorithm/utils.py +177 -0
- gatewaysdk/algorithm/verl/__init__.py +5 -0
- gatewaysdk/algorithm/verl/interface.py +202 -0
- gatewaysdk/automations.py +111 -0
- gatewaysdk/benchmark_hub/README.md +88 -0
- gatewaysdk/benchmark_hub/__init__.py +119 -0
- gatewaysdk/benchmark_hub/_archive.py +189 -0
- gatewaysdk/benchmark_hub/_tar.py +6 -0
- gatewaysdk/benchmark_hub/client.py +992 -0
- gatewaysdk/benchmark_hub/dispatch_shard.py +34 -0
- gatewaysdk/benchmark_hub/eval_config.py +20 -0
- gatewaysdk/benchmark_hub/evals.py +351 -0
- gatewaysdk/benchmark_hub/harbor_adapter.py +673 -0
- gatewaysdk/benchmark_hub/path_utils.py +81 -0
- gatewaysdk/benchmark_hub/save_utils.py +101 -0
- gatewaysdk/benchmark_hub/verifiers_adapter.py +327 -0
- gatewaysdk/benchmark_hub/versioning.py +72 -0
- gatewaysdk/build.py +515 -0
- gatewaysdk/cli/__init__.py +58 -0
- gatewaysdk/cli/agent_runner.py +132 -0
- gatewaysdk/cli/http_client.py +115 -0
- gatewaysdk/cli/platform.py +4086 -0
- gatewaysdk/cli/prometheus.py +115 -0
- gatewaysdk/cli/release_gate.py +215 -0
- gatewaysdk/cli/store.py +131 -0
- gatewaysdk/cli/vllm.py +29 -0
- gatewaysdk/client.py +406 -0
- gatewaysdk/config.py +348 -0
- gatewaysdk/connectors/__init__.py +25 -0
- gatewaysdk/connectors/client.py +203 -0
- gatewaysdk/connectors/skill.py +25 -0
- gatewaysdk/connectors/template.py +106 -0
- gatewaysdk/context.py +606 -0
- gatewaysdk/emitter/__init__.py +43 -0
- gatewaysdk/emitter/annotation.py +370 -0
- gatewaysdk/emitter/exception.py +54 -0
- gatewaysdk/emitter/message.py +61 -0
- gatewaysdk/emitter/object.py +117 -0
- gatewaysdk/emitter/reward.py +320 -0
- gatewaysdk/env_var.py +156 -0
- gatewaysdk/environment/__init__.py +108 -0
- gatewaysdk/environment/_bundle.py +281 -0
- gatewaysdk/environment/_harbor.py +276 -0
- gatewaysdk/environment/_materialize.py +97 -0
- gatewaysdk/environment/_tar.py +33 -0
- gatewaysdk/environment/_world.py +346 -0
- gatewaysdk/environment/core.py +527 -0
- gatewaysdk/environment/errors.py +58 -0
- gatewaysdk/environment/runtime.py +150 -0
- gatewaysdk/environment/schema/__init__.py +35 -0
- gatewaysdk/environment/schema/__main__.py +225 -0
- gatewaysdk/environment/schema/_slack_fidelity.py +116 -0
- gatewaysdk/environment/schema/_slack_scenario.py +183 -0
- gatewaysdk/environment/schema/api.py +1628 -0
- gatewaysdk/environment/schema/batch.py +755 -0
- gatewaysdk/environment/schema/compiler.py +615 -0
- gatewaysdk/environment/schema/conform.py +428 -0
- gatewaysdk/environment/schema/connector.py +777 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/handlers.py +17 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/parity.json +28 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/provenance.json +233 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/scope.toml +112 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/world.json +1380 -0
- gatewaysdk/environment/schema/connectors/base.json +28 -0
- gatewaysdk/environment/schema/connectors/custom/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/custom/world.json +123 -0
- gatewaysdk/environment/schema/connectors/github/handlers.py +81 -0
- gatewaysdk/environment/schema/connectors/github/parity.json +37 -0
- gatewaysdk/environment/schema/connectors/github/provenance.json +134 -0
- gatewaysdk/environment/schema/connectors/github/scope.toml +158 -0
- gatewaysdk/environment/schema/connectors/github/world.json +5869 -0
- gatewaysdk/environment/schema/connectors/google-calendar/handlers.py +23 -0
- gatewaysdk/environment/schema/connectors/google-calendar/world.json +209 -0
- gatewaysdk/environment/schema/connectors/google-drive/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/google-drive/world.json +286 -0
- gatewaysdk/environment/schema/connectors/jamf/handlers.py +34 -0
- gatewaysdk/environment/schema/connectors/jamf/parity.json +49 -0
- gatewaysdk/environment/schema/connectors/jamf/provenance.json +181 -0
- gatewaysdk/environment/schema/connectors/jamf/scope.toml +128 -0
- gatewaysdk/environment/schema/connectors/jamf/world.json +2218 -0
- gatewaysdk/environment/schema/connectors/jira/handlers.py +28 -0
- gatewaysdk/environment/schema/connectors/jira/world.json +389 -0
- gatewaysdk/environment/schema/connectors/kandji/handlers.py +38 -0
- gatewaysdk/environment/schema/connectors/kandji/parity.json +29 -0
- gatewaysdk/environment/schema/connectors/kandji/provenance.json +152 -0
- gatewaysdk/environment/schema/connectors/kandji/scope.toml +120 -0
- gatewaysdk/environment/schema/connectors/kandji/world.json +765 -0
- gatewaysdk/environment/schema/connectors/linear/handlers.py +20 -0
- gatewaysdk/environment/schema/connectors/linear/world.json +363 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/fidelity.json +45 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/handlers.py +1 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/provenance.json +851 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/scope.md +231 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/teams-types.json +1710 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/world.json +858 -0
- gatewaysdk/environment/schema/connectors/netsuite/handlers.py +32 -0
- gatewaysdk/environment/schema/connectors/netsuite/parity.json +50 -0
- gatewaysdk/environment/schema/connectors/netsuite/provenance.json +137 -0
- gatewaysdk/environment/schema/connectors/netsuite/scope.toml +78 -0
- gatewaysdk/environment/schema/connectors/netsuite/world.json +16243 -0
- gatewaysdk/environment/schema/connectors/salesforce/handlers.py +806 -0
- gatewaysdk/environment/schema/connectors/salesforce/parity.json +46 -0
- gatewaysdk/environment/schema/connectors/salesforce/provenance.json +143 -0
- gatewaysdk/environment/schema/connectors/salesforce/scope.toml +174 -0
- gatewaysdk/environment/schema/connectors/salesforce/world.json +4695 -0
- gatewaysdk/environment/schema/connectors/slack/MODEL.md +236 -0
- gatewaysdk/environment/schema/connectors/slack/capabilities.json +1039 -0
- gatewaysdk/environment/schema/connectors/slack/handlers.py +762 -0
- gatewaysdk/environment/schema/connectors/slack/parity.json +65 -0
- gatewaysdk/environment/schema/connectors/slack/provenance.json +294 -0
- gatewaysdk/environment/schema/connectors/slack/scope.toml +44 -0
- gatewaysdk/environment/schema/connectors/slack/world.json +4993 -0
- gatewaysdk/environment/schema/connectors/workday/handlers.py +75 -0
- gatewaysdk/environment/schema/connectors/workday/parity.json +36 -0
- gatewaysdk/environment/schema/connectors/workday/provenance.json +179 -0
- gatewaysdk/environment/schema/connectors/workday/scope.toml +180 -0
- gatewaysdk/environment/schema/connectors/workday/world.json +806 -0
- gatewaysdk/environment/schema/data.py +593 -0
- gatewaysdk/environment/schema/extract.py +263 -0
- gatewaysdk/environment/schema/host.py +2437 -0
- gatewaysdk/environment/schema/host_surfaces.py +275 -0
- gatewaysdk/environment/schema/platform.py +1272 -0
- gatewaysdk/environment/schema/scaffold.py +250 -0
- gatewaysdk/environment/schema/skill.py +118 -0
- gatewaysdk/environment/schema/skills/agents/connector-template-author.md +202 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONNECTORS.md +331 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONVENTIONS.md +100 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/SKILL.md +313 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/ROWS.md +223 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/SKILL.md +293 -0
- gatewaysdk/environment/schema/skills/worlds-getting-started/SKILL.md +390 -0
- gatewaysdk/environment/schema/slack.py +757 -0
- gatewaysdk/environment/schema/snapshot.py +479 -0
- gatewaysdk/environment/schema/store.py +904 -0
- gatewaysdk/environment/schema/tasks.py +525 -0
- gatewaysdk/environment/schema/tools_world.py +322 -0
- gatewaysdk/environment/schema/validation.py +577 -0
- gatewaysdk/environment/schema/worker.py +78 -0
- gatewaysdk/environment/schema/workers.py +500 -0
- gatewaysdk/environment/schema/world_tests.py +336 -0
- gatewaysdk/environments/__init__.py +24 -0
- gatewaysdk/environments/client.py +486 -0
- gatewaysdk/environments/types.py +318 -0
- gatewaysdk/execution/__init__.py +15 -0
- gatewaysdk/execution/base.py +64 -0
- gatewaysdk/execution/client_server.py +443 -0
- gatewaysdk/execution/events.py +69 -0
- gatewaysdk/execution/inter_process.py +16 -0
- gatewaysdk/execution/shared_memory.py +282 -0
- gatewaysdk/experiments/__init__.py +90 -0
- gatewaysdk/experiments/assignment.py +177 -0
- gatewaysdk/experiments/client.py +877 -0
- gatewaysdk/experiments/exposure.py +222 -0
- gatewaysdk/experiments/types.py +81 -0
- gatewaysdk/importers/__init__.py +49 -0
- gatewaysdk/importers/_normalize.py +91 -0
- gatewaysdk/importers/builder.py +173 -0
- gatewaysdk/importers/client.py +95 -0
- gatewaysdk/importers/langsmith.py +196 -0
- gatewaysdk/importers/recipes.py +249 -0
- gatewaysdk/instrumentation/__init__.py +300 -0
- gatewaysdk/instrumentation/agentops.py +314 -0
- gatewaysdk/instrumentation/agentops_langchain.py +45 -0
- gatewaysdk/instrumentation/base.py +119 -0
- gatewaysdk/instrumentation/litellm.py +83 -0
- gatewaysdk/instrumentation/registry.py +273 -0
- gatewaysdk/instrumentation/vllm.py +81 -0
- gatewaysdk/instrumentation/weave.py +500 -0
- gatewaysdk/integrations/__init__.py +15 -0
- gatewaysdk/integrations/gateway/__init__.py +11 -0
- gatewaysdk/integrations/gateway/client.py +171 -0
- gatewaysdk/integrations/tool_access.py +549 -0
- gatewaysdk/litagent/__init__.py +11 -0
- gatewaysdk/litagent/decorator.py +536 -0
- gatewaysdk/litagent/litagent.py +252 -0
- gatewaysdk/llm_proxy.py +1742 -0
- gatewaysdk/logging.py +370 -0
- gatewaysdk/memory.py +278 -0
- gatewaysdk/personas.py +186 -0
- gatewaysdk/platform/__init__.py +17 -0
- gatewaysdk/platform/builder.py +221 -0
- gatewaysdk/platform/compatibility.py +67 -0
- gatewaysdk/platform/manifest.py +185 -0
- gatewaysdk/platform/orchestrator.py +901 -0
- gatewaysdk/platform/registry.py +122 -0
- gatewaysdk/platform/worker.py +864 -0
- gatewaysdk/replay/__init__.py +1032 -0
- gatewaysdk/replay/pytest.py +56 -0
- gatewaysdk/reward.py +7 -0
- gatewaysdk/run.py +1781 -0
- gatewaysdk/runner/__init__.py +11 -0
- gatewaysdk/runner/agent.py +878 -0
- gatewaysdk/runner/base.py +182 -0
- gatewaysdk/runner/legacy.py +309 -0
- gatewaysdk/security.py +700 -0
- gatewaysdk/semconv.py +170 -0
- gatewaysdk/server.py +399 -0
- gatewaysdk/sessions.py +282 -0
- gatewaysdk/store/__init__.py +45 -0
- gatewaysdk/store/base.py +908 -0
- gatewaysdk/store/client_server.py +2093 -0
- gatewaysdk/store/collection/__init__.py +30 -0
- gatewaysdk/store/collection/base.py +587 -0
- gatewaysdk/store/collection/memory.py +970 -0
- gatewaysdk/store/collection/mongo.py +1412 -0
- gatewaysdk/store/collection_based.py +1823 -0
- gatewaysdk/store/gateway.py +983 -0
- gatewaysdk/store/gateway_listener.py +465 -0
- gatewaysdk/store/listener.py +58 -0
- gatewaysdk/store/memory.py +396 -0
- gatewaysdk/store/mongo.py +165 -0
- gatewaysdk/store/redis_stream.py +517 -0
- gatewaysdk/store/sqlite.py +3 -0
- gatewaysdk/store/threading.py +370 -0
- gatewaysdk/store/utils.py +142 -0
- gatewaysdk/tracer/__init__.py +14 -0
- gatewaysdk/tracer/base.py +286 -0
- gatewaysdk/tracer/dummy.py +106 -0
- gatewaysdk/tracer/otel.py +559 -0
- gatewaysdk/tracing/__init__.py +110 -0
- gatewaysdk/tracing/api.py +808 -0
- gatewaysdk/tracing/attributes.py +9 -0
- gatewaysdk/tracing/context.py +272 -0
- gatewaysdk/tracing/exporters/__init__.py +10 -0
- gatewaysdk/tracing/exporters/gateway.py +228 -0
- gatewaysdk/tracing/identity.py +288 -0
- gatewaysdk/tracing/init.py +620 -0
- gatewaysdk/tracing/instrumentors/__init__.py +15 -0
- gatewaysdk/tracing/instrumentors/claude_agent_sdk.py +766 -0
- gatewaysdk/tracing/instrumentors/instrumentation_principles.md +294 -0
- gatewaysdk/tracing/instrumentors/registry.py +352 -0
- gatewaysdk/tracing/mapping.py +729 -0
- gatewaysdk/tracing/processors.py +393 -0
- gatewaysdk/tracing/push.py +617 -0
- gatewaysdk/tracing/push_models.py +247 -0
- gatewaysdk/tracing/semconv.py +294 -0
- gatewaysdk/tracing/span_builder.py +356 -0
- gatewaysdk/trainer/__init__.py +6 -0
- gatewaysdk/trainer/init_utils.py +263 -0
- gatewaysdk/trainer/legacy.py +359 -0
- gatewaysdk/trainer/registry.py +12 -0
- gatewaysdk/trainer/trainer.py +638 -0
- gatewaysdk/types/__init__.py +63 -0
- gatewaysdk/types/core.py +556 -0
- gatewaysdk/types/resources.py +204 -0
- gatewaysdk/types/tracer.py +515 -0
- gatewaysdk/types/tracing.py +162 -0
- gatewaysdk/users.py +251 -0
- gatewaysdk/utils/__init__.py +1 -0
- gatewaysdk/utils/id.py +18 -0
- gatewaysdk/utils/metrics.py +1025 -0
- gatewaysdk/utils/otel.py +550 -0
- gatewaysdk/utils/otlp.py +556 -0
- gatewaysdk/utils/redact.py +22 -0
- gatewaysdk/utils/server_launcher.py +1045 -0
- gatewaysdk/utils/system_snapshot.py +90 -0
- gatewaysdk/verl/__init__.py +8 -0
- gatewaysdk/verl/__main__.py +6 -0
- gatewaysdk/verl/async_server.py +46 -0
- gatewaysdk/verl/config.yaml +27 -0
- gatewaysdk/verl/daemon.py +1154 -0
- gatewaysdk/verl/dataset.py +44 -0
- gatewaysdk/verl/entrypoint.py +248 -0
- gatewaysdk/verl/trainer.py +549 -0
- gatewaysdk/world_browser.py +748 -0
- gatewaysdk/world_data.py +453 -0
- gatewaysdk/world_sessions.py +978 -0
- gatewaysdk/world_tasks.py +285 -0
- gatewaysdk/world_tools.py +115 -0
- gatewaysdk/worlds.py +639 -0
- gatewaysdk-0.3.2.dist-info/METADATA +236 -0
- gatewaysdk-0.3.2.dist-info/RECORD +376 -0
- gatewaysdk-0.3.2.dist-info/WHEEL +4 -0
- gatewaysdk-0.3.2.dist-info/entry_points.txt +5 -0
- gatewaysdk-0.3.2.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
"""Schema-aware SQLite state and atomic, provider-neutral mutation batches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
import threading
|
|
8
|
+
import uuid
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Iterator
|
|
12
|
+
|
|
13
|
+
from .compiler import Contract, payload_value, quote
|
|
14
|
+
from .validation import (
|
|
15
|
+
ContractError,
|
|
16
|
+
canonical,
|
|
17
|
+
digest,
|
|
18
|
+
encoded,
|
|
19
|
+
fail,
|
|
20
|
+
members,
|
|
21
|
+
nonempty,
|
|
22
|
+
parse_json,
|
|
23
|
+
validate,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
PRIVATE_DDL = """
|
|
27
|
+
CREATE TABLE _gateway_contract (contract TEXT NOT NULL);
|
|
28
|
+
CREATE TABLE _gateway_batches (id TEXT PRIMARY KEY, digest TEXT NOT NULL, metadata TEXT NOT NULL);
|
|
29
|
+
CREATE TABLE _gateway_lineage (batch TEXT NOT NULL REFERENCES _gateway_batches(id),
|
|
30
|
+
ordinal INTEGER NOT NULL, entity TEXT NOT NULL, key TEXT NOT NULL, source TEXT NOT NULL,
|
|
31
|
+
PRIMARY KEY(batch, ordinal));
|
|
32
|
+
CREATE TABLE benchmark_runtime_context (context_key TEXT PRIMARY KEY, scenario_id TEXT NOT NULL);
|
|
33
|
+
CREATE TABLE benchmark_runtime_parameters (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
34
|
+
CREATE TABLE benchmark_conversation_events (scenario_id TEXT NOT NULL, user_turn_index INTEGER NOT NULL,
|
|
35
|
+
event_type TEXT NOT NULL, actor TEXT NOT NULL, name TEXT, payload_json TEXT NOT NULL);
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
# The host owns these tables. Schema authors cannot declare them. Tools may
|
|
39
|
+
# read parameters; context/events are available only to trusted verifier reads.
|
|
40
|
+
RUNTIME_TABLES = {
|
|
41
|
+
"benchmark_runtime_context",
|
|
42
|
+
"benchmark_runtime_parameters",
|
|
43
|
+
"benchmark_conversation_events",
|
|
44
|
+
}
|
|
45
|
+
READ_ACTIONS = {
|
|
46
|
+
sqlite3.SQLITE_SELECT,
|
|
47
|
+
sqlite3.SQLITE_READ,
|
|
48
|
+
sqlite3.SQLITE_FUNCTION,
|
|
49
|
+
sqlite3.SQLITE_RECURSIVE,
|
|
50
|
+
}
|
|
51
|
+
WRITE_ACTIONS = {sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE}
|
|
52
|
+
# Objects an ingest session may create for its own staging; they never persist.
|
|
53
|
+
TEMP_ACTIONS = {
|
|
54
|
+
sqlite3.SQLITE_CREATE_TEMP_TABLE,
|
|
55
|
+
sqlite3.SQLITE_CREATE_TEMP_INDEX,
|
|
56
|
+
sqlite3.SQLITE_CREATE_TEMP_VIEW,
|
|
57
|
+
sqlite3.SQLITE_DROP_TEMP_TABLE,
|
|
58
|
+
sqlite3.SQLITE_DROP_TEMP_INDEX,
|
|
59
|
+
sqlite3.SQLITE_DROP_TEMP_VIEW,
|
|
60
|
+
}
|
|
61
|
+
# Bookkeeping and host tables no SQL session may read; parameters stay readable.
|
|
62
|
+
PRIVATE_TABLES = {
|
|
63
|
+
"_gateway_contract",
|
|
64
|
+
"_gateway_batches",
|
|
65
|
+
"_gateway_lineage",
|
|
66
|
+
"benchmark_runtime_context",
|
|
67
|
+
"benchmark_conversation_events",
|
|
68
|
+
}
|
|
69
|
+
UNSAFE_FUNCTIONS = {"load_extension", "writefile", "readfile"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _source(source: Any, location: str) -> None:
|
|
73
|
+
members(source, {"record", "evidence"}, {"record"}, location)
|
|
74
|
+
nonempty(source["record"], f"{location}/record")
|
|
75
|
+
evidence = source.get("evidence", [])
|
|
76
|
+
if not isinstance(evidence, list):
|
|
77
|
+
fail(
|
|
78
|
+
"provenance",
|
|
79
|
+
"evidence must be an array of stable reference/digest pairs",
|
|
80
|
+
location,
|
|
81
|
+
)
|
|
82
|
+
for index, entry in enumerate(evidence):
|
|
83
|
+
members(
|
|
84
|
+
entry,
|
|
85
|
+
{"reference", "digest"},
|
|
86
|
+
{"reference", "digest"},
|
|
87
|
+
f"{location}/evidence/{index}",
|
|
88
|
+
)
|
|
89
|
+
nonempty(entry["reference"], location)
|
|
90
|
+
# Digest syntax is explicit; providers can translate a verified base64
|
|
91
|
+
# SHA-256 to hex without retaining staging credentials or locations.
|
|
92
|
+
value = entry["digest"]
|
|
93
|
+
if not isinstance(value, str) or len(value) != 64 or any(c not in "0123456789abcdef" for c in value):
|
|
94
|
+
fail("provenance", "Evidence digest must be lowercase SHA-256 hex", location)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ReadConnection:
|
|
98
|
+
"""Compatibility conn= surface: execute SELECT only; no raw writes/commit."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, store: "SQLiteStore"):
|
|
101
|
+
self._store = store
|
|
102
|
+
|
|
103
|
+
def execute(self, sql: str, params: Any = ()) -> "ReadCursor":
|
|
104
|
+
return ReadCursor(self._store.query(sql, tuple(params)))
|
|
105
|
+
|
|
106
|
+
def cursor(self):
|
|
107
|
+
fail(
|
|
108
|
+
"raw_write",
|
|
109
|
+
"Use conn.execute for reads and gateway_env.store for mutations",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def executescript(self, *args):
|
|
113
|
+
fail("raw_write", "Raw SQL scripts are unavailable in schema-backed tools")
|
|
114
|
+
|
|
115
|
+
def commit(self):
|
|
116
|
+
fail("raw_write", "The world owns the tool transaction")
|
|
117
|
+
|
|
118
|
+
rollback = commit
|
|
119
|
+
close = commit
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class IngestConnection:
|
|
123
|
+
"""Raw SQL inside one schema-gated ingest session.
|
|
124
|
+
|
|
125
|
+
Domain tables and TEMP objects are writable; the store owns the transaction
|
|
126
|
+
and commits only when every persisted row satisfies the contract.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, store: "SQLiteStore"):
|
|
130
|
+
self._store = store
|
|
131
|
+
self._open = True
|
|
132
|
+
self.result: dict | None = None
|
|
133
|
+
|
|
134
|
+
def execute(self, sql: str, params: Any = ()) -> sqlite3.Cursor:
|
|
135
|
+
return self._run(lambda conn: conn.execute(sql, params))
|
|
136
|
+
|
|
137
|
+
def executemany(self, sql: str, rows: Any) -> sqlite3.Cursor:
|
|
138
|
+
return self._run(lambda conn: conn.executemany(sql, rows))
|
|
139
|
+
|
|
140
|
+
def script(self, sql: str) -> None:
|
|
141
|
+
"""Run a multi-statement script one statement at a time.
|
|
142
|
+
|
|
143
|
+
``sqlite3.executescript`` would commit the session's transaction first,
|
|
144
|
+
so it is deliberately not exposed.
|
|
145
|
+
"""
|
|
146
|
+
from .._world import split_sql_statements
|
|
147
|
+
|
|
148
|
+
for statement in split_sql_statements(sql):
|
|
149
|
+
self.execute(statement)
|
|
150
|
+
|
|
151
|
+
def _run(self, operation):
|
|
152
|
+
if not self._open:
|
|
153
|
+
fail("transaction", "The ingest session has ended")
|
|
154
|
+
store = self._store
|
|
155
|
+
store._key_error = None
|
|
156
|
+
try:
|
|
157
|
+
return operation(store._conn)
|
|
158
|
+
except sqlite3.Error as error:
|
|
159
|
+
context = {"sqlite": str(error)}
|
|
160
|
+
if store._key_error is not None:
|
|
161
|
+
context["key"] = store._key_error
|
|
162
|
+
raise ContractError(
|
|
163
|
+
"ingest_sql",
|
|
164
|
+
"Statement rejected; only domain tables and TEMP objects are writable",
|
|
165
|
+
**context,
|
|
166
|
+
) from error
|
|
167
|
+
|
|
168
|
+
def commit(self):
|
|
169
|
+
fail("raw_write", "The store commits the ingest session when it ends")
|
|
170
|
+
|
|
171
|
+
rollback = commit
|
|
172
|
+
close = commit
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ReadCursor:
|
|
176
|
+
def __init__(self, rows: list[sqlite3.Row]):
|
|
177
|
+
self._rows = iter(rows)
|
|
178
|
+
self.rowcount = -1
|
|
179
|
+
|
|
180
|
+
def fetchone(self):
|
|
181
|
+
return next(self._rows, None)
|
|
182
|
+
|
|
183
|
+
def fetchall(self):
|
|
184
|
+
return list(self._rows)
|
|
185
|
+
|
|
186
|
+
def __iter__(self):
|
|
187
|
+
return self._rows
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class SQLiteStore:
|
|
191
|
+
"""Use create/open as context managers; every public mutation is atomic.
|
|
192
|
+
|
|
193
|
+
Payloads are JSON values, keys are dicts of exactly the primary-key fields.
|
|
194
|
+
Updates shallow-merge provided properties and validate the complete result.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, path: Path, contract: Contract, *, create: bool = False, audit: bool = True):
|
|
198
|
+
self.path = Path(path)
|
|
199
|
+
self.contract = contract
|
|
200
|
+
self._lock = threading.RLock()
|
|
201
|
+
self._depth = 0
|
|
202
|
+
self._internal = 0
|
|
203
|
+
self._touched: set[tuple[str, str]] = set()
|
|
204
|
+
self._read_only = False
|
|
205
|
+
self._private_reads = False
|
|
206
|
+
self._ingesting = False
|
|
207
|
+
self._key_error: dict | None = None
|
|
208
|
+
self._closed = False
|
|
209
|
+
if create and self.path.exists():
|
|
210
|
+
fail(
|
|
211
|
+
"state_exists",
|
|
212
|
+
"Refusing to replace existing state; reset/rebuild explicitly",
|
|
213
|
+
)
|
|
214
|
+
if not create and not self.path.is_file():
|
|
215
|
+
fail("state_missing", "Cannot open missing state")
|
|
216
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
self._conn = sqlite3.connect(
|
|
218
|
+
self.path,
|
|
219
|
+
isolation_level=None,
|
|
220
|
+
check_same_thread=False,
|
|
221
|
+
cached_statements=0,
|
|
222
|
+
)
|
|
223
|
+
self._conn.row_factory = sqlite3.Row
|
|
224
|
+
try:
|
|
225
|
+
self._conn.execute("PRAGMA foreign_keys = ON")
|
|
226
|
+
if self._conn.execute("PRAGMA foreign_keys").fetchone()[0] != 1:
|
|
227
|
+
fail("sqlite", "Foreign-key enforcement is required")
|
|
228
|
+
if create:
|
|
229
|
+
self._conn.executescript(contract.artifacts["db/schema.sql"].decode() + PRIVATE_DDL)
|
|
230
|
+
self._conn.execute(
|
|
231
|
+
"INSERT INTO _gateway_contract VALUES (?)",
|
|
232
|
+
(canonical(contract.lock),),
|
|
233
|
+
)
|
|
234
|
+
self._check_contract()
|
|
235
|
+
self._conn.create_function("gateway_key", 2, self._sql_key, deterministic=True)
|
|
236
|
+
self._conn.set_authorizer(self._authorize)
|
|
237
|
+
if audit:
|
|
238
|
+
self.audit()
|
|
239
|
+
except BaseException:
|
|
240
|
+
self._conn.close()
|
|
241
|
+
if create:
|
|
242
|
+
self.path.unlink(missing_ok=True)
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def create(cls, path: str | Path, contract: Contract) -> "SQLiteStore":
|
|
247
|
+
return cls(Path(path), contract, create=True)
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def open(cls, path: str | Path, contract: Contract, *, audit: bool = True) -> "SQLiteStore":
|
|
251
|
+
"""Open existing state. ``audit=False`` skips the full row audit: for a snapshot
|
|
252
|
+
the store itself wrote and whose bytes a sidecar digest has just verified,
|
|
253
|
+
every row was audited when it was written, so opening is then a matter of
|
|
254
|
+
milliseconds however many rows the file holds. The contract check always runs."""
|
|
255
|
+
return cls(Path(path), contract, audit=audit)
|
|
256
|
+
|
|
257
|
+
def _check_contract(self) -> None:
|
|
258
|
+
try:
|
|
259
|
+
rows = self._conn.execute("SELECT contract FROM _gateway_contract").fetchall()
|
|
260
|
+
if len(rows) != 1 or rows[0][0] != canonical(self.contract.lock):
|
|
261
|
+
fail(
|
|
262
|
+
"state_contract",
|
|
263
|
+
"State belongs to a different schema/package lock; no automatic migration",
|
|
264
|
+
)
|
|
265
|
+
# Compare actual DDL with fresh generated DDL, not merely a stored hash.
|
|
266
|
+
expected = sqlite3.connect(":memory:")
|
|
267
|
+
try:
|
|
268
|
+
expected.executescript(self.contract.artifacts["db/schema.sql"].decode() + PRIVATE_DDL)
|
|
269
|
+
expected_sql = dict(expected.execute("SELECT name, sql FROM sqlite_master WHERE sql IS NOT NULL"))
|
|
270
|
+
finally:
|
|
271
|
+
expected.close()
|
|
272
|
+
actual_sql = dict(self._conn.execute("SELECT name, sql FROM sqlite_master WHERE sql IS NOT NULL"))
|
|
273
|
+
if (
|
|
274
|
+
any(actual_sql.get(k) != v for k, v in expected_sql.items())
|
|
275
|
+
or set(actual_sql) - set(expected_sql) - RUNTIME_TABLES
|
|
276
|
+
):
|
|
277
|
+
fail(
|
|
278
|
+
"state_contract",
|
|
279
|
+
"State DDL differs from the compiled storage contract",
|
|
280
|
+
)
|
|
281
|
+
except sqlite3.Error as error:
|
|
282
|
+
raise ContractError("state_contract", "State has no compatible schema contract") from error
|
|
283
|
+
|
|
284
|
+
def _authorize(self, action, first, second, database, trigger):
|
|
285
|
+
if self._read_only:
|
|
286
|
+
if action not in READ_ACTIONS or (
|
|
287
|
+
action == sqlite3.SQLITE_READ
|
|
288
|
+
and first not in self.contract.entities
|
|
289
|
+
and first != "benchmark_runtime_parameters"
|
|
290
|
+
and not (self._private_reads and first in RUNTIME_TABLES)
|
|
291
|
+
):
|
|
292
|
+
return sqlite3.SQLITE_DENY
|
|
293
|
+
if action == sqlite3.SQLITE_FUNCTION and (second or "").lower() in UNSAFE_FUNCTIONS:
|
|
294
|
+
return sqlite3.SQLITE_DENY
|
|
295
|
+
return sqlite3.SQLITE_OK
|
|
296
|
+
if self._internal:
|
|
297
|
+
return sqlite3.SQLITE_OK
|
|
298
|
+
if self._ingesting:
|
|
299
|
+
return self._authorize_ingest(action, first, second, database)
|
|
300
|
+
if action in READ_ACTIONS:
|
|
301
|
+
return sqlite3.SQLITE_OK
|
|
302
|
+
if action in WRITE_ACTIONS and first in RUNTIME_TABLES:
|
|
303
|
+
return sqlite3.SQLITE_OK
|
|
304
|
+
return sqlite3.SQLITE_DENY
|
|
305
|
+
|
|
306
|
+
def _authorize_ingest(self, action, first, second, database):
|
|
307
|
+
"""Domain tables and TEMP staging are writable; everything else is not.
|
|
308
|
+
|
|
309
|
+
Transaction control, PRAGMA, ATTACH, persistent DDL and the private
|
|
310
|
+
bookkeeping tables stay with the store, so a session cannot commit
|
|
311
|
+
early, disable enforcement, or forge provenance.
|
|
312
|
+
"""
|
|
313
|
+
if action in READ_ACTIONS:
|
|
314
|
+
if action == sqlite3.SQLITE_READ and first in PRIVATE_TABLES:
|
|
315
|
+
return sqlite3.SQLITE_DENY
|
|
316
|
+
if action == sqlite3.SQLITE_FUNCTION and (second or "").lower() in UNSAFE_FUNCTIONS:
|
|
317
|
+
return sqlite3.SQLITE_DENY
|
|
318
|
+
return sqlite3.SQLITE_OK
|
|
319
|
+
if action in WRITE_ACTIONS:
|
|
320
|
+
if first in self.contract.entities or database == "temp":
|
|
321
|
+
return sqlite3.SQLITE_OK
|
|
322
|
+
return sqlite3.SQLITE_DENY
|
|
323
|
+
if action in TEMP_ACTIONS or action == sqlite3.SQLITE_SAVEPOINT:
|
|
324
|
+
return sqlite3.SQLITE_OK
|
|
325
|
+
return sqlite3.SQLITE_DENY
|
|
326
|
+
|
|
327
|
+
def _sql_key(self, entity: Any, payload: Any) -> str | None:
|
|
328
|
+
"""``gateway_key(entity, payload)``: the encoded ``_key`` for a payload.
|
|
329
|
+
|
|
330
|
+
Returns NULL on an invalid entity or payload so the NOT NULL constraint
|
|
331
|
+
rejects the row; the reason is attached to the resulting error.
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
value = payload
|
|
335
|
+
if isinstance(payload, (str, bytes)):
|
|
336
|
+
value = parse_json(payload)
|
|
337
|
+
if not isinstance(value, dict):
|
|
338
|
+
fail("key", "gateway_key needs a JSON object payload", entity=entity)
|
|
339
|
+
return self._key(entity, value)
|
|
340
|
+
except (ContractError, ValueError, TypeError) as error:
|
|
341
|
+
detail = getattr(error, "detail", None)
|
|
342
|
+
self._key_error = detail or {"code": "key", "message": str(error)}
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
@contextmanager
|
|
346
|
+
def _trusted(self):
|
|
347
|
+
self._internal += 1
|
|
348
|
+
try:
|
|
349
|
+
yield
|
|
350
|
+
finally:
|
|
351
|
+
self._internal -= 1
|
|
352
|
+
|
|
353
|
+
@contextmanager
|
|
354
|
+
def transaction(self) -> Iterator["SQLiteStore"]:
|
|
355
|
+
with self._lock:
|
|
356
|
+
if self._ingesting:
|
|
357
|
+
fail(
|
|
358
|
+
"transaction",
|
|
359
|
+
"Use SQL inside an ingest session; store mutations belong outside",
|
|
360
|
+
)
|
|
361
|
+
outer = self._depth == 0
|
|
362
|
+
savepoint = f"gateway_batch_{self._depth}"
|
|
363
|
+
with self._trusted():
|
|
364
|
+
self._conn.execute("BEGIN IMMEDIATE" if outer else f"SAVEPOINT {savepoint}")
|
|
365
|
+
if outer:
|
|
366
|
+
self._touched = set()
|
|
367
|
+
self._depth += 1
|
|
368
|
+
try:
|
|
369
|
+
yield self
|
|
370
|
+
if outer:
|
|
371
|
+
self._audit_touched()
|
|
372
|
+
with self._trusted():
|
|
373
|
+
self._conn.execute("COMMIT" if outer else f"RELEASE {savepoint}")
|
|
374
|
+
except BaseException:
|
|
375
|
+
with self._trusted():
|
|
376
|
+
self._conn.execute("ROLLBACK" if outer else f"ROLLBACK TO {savepoint}")
|
|
377
|
+
if not outer:
|
|
378
|
+
self._conn.execute(f"RELEASE {savepoint}")
|
|
379
|
+
raise
|
|
380
|
+
finally:
|
|
381
|
+
self._depth -= 1
|
|
382
|
+
|
|
383
|
+
def _entity(self, name: str) -> dict:
|
|
384
|
+
if not isinstance(name, str) or name not in self.contract.entities:
|
|
385
|
+
fail("entity", "Unknown entity type", entity=name)
|
|
386
|
+
return self.contract.entities[name]
|
|
387
|
+
|
|
388
|
+
def _key(self, name: str, value: dict, *, exact: bool = False) -> str:
|
|
389
|
+
entity = self._entity(name)
|
|
390
|
+
fields = entity["primary_key"]
|
|
391
|
+
if not isinstance(value, dict) or not set(fields) <= set(value) or (exact and set(value) != set(fields)):
|
|
392
|
+
fail("key", "Supply exactly the declared primary-key fields", entity=name)
|
|
393
|
+
for field in fields:
|
|
394
|
+
validate(
|
|
395
|
+
entity["schema"]["properties"][field],
|
|
396
|
+
value[field],
|
|
397
|
+
location=f"{entity['location']}/schema/properties/{field}",
|
|
398
|
+
)
|
|
399
|
+
# JSON Schema integers include 1.0; identity normalizes mathematically
|
|
400
|
+
# integral keys only, without changing the actual payload value.
|
|
401
|
+
parts = [int(value[f]) if type(value[f]) is float else value[f] for f in fields]
|
|
402
|
+
return canonical(parts)
|
|
403
|
+
|
|
404
|
+
def get(self, entity: str, key: dict) -> dict | None:
|
|
405
|
+
with self._lock:
|
|
406
|
+
encoded_key = self._key(entity, key, exact=True)
|
|
407
|
+
row = self._conn.execute(f"SELECT payload FROM {quote(entity)} WHERE _key = ?", (encoded_key,)).fetchone()
|
|
408
|
+
return json.loads(row[0]) if row else None
|
|
409
|
+
|
|
410
|
+
def list(self, entity: str) -> list[dict]:
|
|
411
|
+
with self._lock:
|
|
412
|
+
self._entity(entity)
|
|
413
|
+
return [
|
|
414
|
+
json.loads(row[0]) for row in self._conn.execute(f"SELECT payload FROM {quote(entity)} ORDER BY _key")
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
def query(self, sql: str, params: tuple = (), *, private: bool = False) -> list[sqlite3.Row]:
|
|
418
|
+
"""Read-only SQL over domain tables, enforced by SQLite's authorizer."""
|
|
419
|
+
with self._lock:
|
|
420
|
+
self._read_only = True
|
|
421
|
+
self._private_reads = private
|
|
422
|
+
try:
|
|
423
|
+
return self._conn.execute(sql, params).fetchall()
|
|
424
|
+
except sqlite3.Error as error:
|
|
425
|
+
raise ContractError("read_only", "Query failed or attempted a forbidden operation") from error
|
|
426
|
+
finally:
|
|
427
|
+
self._read_only = False
|
|
428
|
+
self._private_reads = False
|
|
429
|
+
|
|
430
|
+
def _mutate(self, mutation: dict) -> str:
|
|
431
|
+
name = mutation["entity"]
|
|
432
|
+
entity = self._entity(name)
|
|
433
|
+
operation = mutation["op"]
|
|
434
|
+
if operation == "insert":
|
|
435
|
+
value = mutation["value"]
|
|
436
|
+
validate(entity["schema"], value, location=f"{entity['location']}/schema")
|
|
437
|
+
key = self._key(name, value)
|
|
438
|
+
else:
|
|
439
|
+
key = self._key(name, mutation["key"], exact=True)
|
|
440
|
+
current = self.get(name, mutation["key"])
|
|
441
|
+
if current is None:
|
|
442
|
+
fail(
|
|
443
|
+
"not_found",
|
|
444
|
+
"No entity exists at this key",
|
|
445
|
+
entity=name,
|
|
446
|
+
key=mutation["key"],
|
|
447
|
+
)
|
|
448
|
+
if operation == "update":
|
|
449
|
+
if not isinstance(mutation["value"], dict):
|
|
450
|
+
fail("type", "An update must be an object")
|
|
451
|
+
value = {**current, **mutation["value"]}
|
|
452
|
+
validate(entity["schema"], value, location=f"{entity['location']}/schema")
|
|
453
|
+
if self._key(name, value) != key:
|
|
454
|
+
fail(
|
|
455
|
+
"key_immutable",
|
|
456
|
+
"Updates cannot change primary keys",
|
|
457
|
+
entity=name,
|
|
458
|
+
)
|
|
459
|
+
self._touched.add((name, key))
|
|
460
|
+
try:
|
|
461
|
+
with self._trusted():
|
|
462
|
+
if operation == "insert":
|
|
463
|
+
self._conn.execute(
|
|
464
|
+
f"INSERT INTO {quote(name)} (_key, payload) VALUES (?, ?)",
|
|
465
|
+
(key, canonical(value)),
|
|
466
|
+
)
|
|
467
|
+
elif operation == "update":
|
|
468
|
+
self._conn.execute(
|
|
469
|
+
f"UPDATE {quote(name)} SET payload = ? WHERE _key = ?",
|
|
470
|
+
(canonical(value), key),
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
self._conn.execute(f"DELETE FROM {quote(name)} WHERE _key = ?", (key,))
|
|
474
|
+
except sqlite3.IntegrityError as error:
|
|
475
|
+
raise ContractError(
|
|
476
|
+
"unique",
|
|
477
|
+
"Primary or unique key conflicts with existing state",
|
|
478
|
+
entity=name,
|
|
479
|
+
key=json.loads(key),
|
|
480
|
+
schema_path=f"{entity['location']}/unique",
|
|
481
|
+
) from error
|
|
482
|
+
return key
|
|
483
|
+
|
|
484
|
+
def apply_batch(self, batch: dict) -> dict:
|
|
485
|
+
"""Atomic rejection; exact retries no-op, conflicting ID reuse rejects."""
|
|
486
|
+
members(
|
|
487
|
+
batch,
|
|
488
|
+
{"id", "target", "mapping", "coverage", "mutations"},
|
|
489
|
+
{"id", "target", "mapping", "coverage", "mutations"},
|
|
490
|
+
"/batch",
|
|
491
|
+
)
|
|
492
|
+
nonempty(batch["id"], "/batch/id")
|
|
493
|
+
if batch["target"] != self.contract.target:
|
|
494
|
+
fail(
|
|
495
|
+
"batch_target",
|
|
496
|
+
"Batch targets a different domain identity/version/hash",
|
|
497
|
+
"/batch/target",
|
|
498
|
+
)
|
|
499
|
+
members(batch["mapping"], {"id", "version"}, {"id", "version"}, "/batch/mapping")
|
|
500
|
+
for key, value in batch["mapping"].items():
|
|
501
|
+
nonempty(value, f"/batch/mapping/{key}")
|
|
502
|
+
if batch["coverage"] not in ("partial", "complete"):
|
|
503
|
+
fail(
|
|
504
|
+
"coverage",
|
|
505
|
+
"coverage must be partial or complete (neither implies deletion)",
|
|
506
|
+
"/batch/coverage",
|
|
507
|
+
)
|
|
508
|
+
if not isinstance(batch["mutations"], list):
|
|
509
|
+
fail("batch", "mutations must be an array", "/batch/mutations")
|
|
510
|
+
batch_hash = digest(encoded(batch))
|
|
511
|
+
with self.transaction():
|
|
512
|
+
prior = self._conn.execute("SELECT digest FROM _gateway_batches WHERE id = ?", (batch["id"],)).fetchone()
|
|
513
|
+
if prior:
|
|
514
|
+
if prior[0] != batch_hash:
|
|
515
|
+
fail(
|
|
516
|
+
"idempotency_conflict",
|
|
517
|
+
"Batch ID was already applied with different content",
|
|
518
|
+
"/batch/id",
|
|
519
|
+
batch=batch["id"],
|
|
520
|
+
)
|
|
521
|
+
return {"status": "already_applied", "batch": batch["id"], "applied": 0}
|
|
522
|
+
with self._trusted():
|
|
523
|
+
self._conn.execute(
|
|
524
|
+
"INSERT INTO _gateway_batches VALUES (?, ?, ?)",
|
|
525
|
+
(
|
|
526
|
+
batch["id"],
|
|
527
|
+
batch_hash,
|
|
528
|
+
canonical({k: v for k, v in batch.items() if k != "mutations"}),
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
for index, mutation in enumerate(batch["mutations"]):
|
|
532
|
+
location = f"/batch/mutations/{index}"
|
|
533
|
+
try:
|
|
534
|
+
members(
|
|
535
|
+
mutation,
|
|
536
|
+
{"op", "entity", "key", "value", "source"},
|
|
537
|
+
{"op", "entity", "source"},
|
|
538
|
+
location,
|
|
539
|
+
)
|
|
540
|
+
op = mutation["op"]
|
|
541
|
+
if not isinstance(op, str):
|
|
542
|
+
fail(
|
|
543
|
+
"operation",
|
|
544
|
+
"Operation must be insert, update or delete",
|
|
545
|
+
location,
|
|
546
|
+
)
|
|
547
|
+
required = {
|
|
548
|
+
"insert": {"value"},
|
|
549
|
+
"update": {"key", "value"},
|
|
550
|
+
"delete": {"key"},
|
|
551
|
+
}.get(op)
|
|
552
|
+
if required is None:
|
|
553
|
+
fail(
|
|
554
|
+
"operation",
|
|
555
|
+
"Only insert, update and delete are supported",
|
|
556
|
+
location,
|
|
557
|
+
)
|
|
558
|
+
members(
|
|
559
|
+
mutation,
|
|
560
|
+
{"op", "entity", "source"} | required,
|
|
561
|
+
{"op", "entity", "source"} | required,
|
|
562
|
+
location,
|
|
563
|
+
)
|
|
564
|
+
_source(mutation["source"], f"{location}/source")
|
|
565
|
+
key = self._mutate(mutation)
|
|
566
|
+
with self._trusted():
|
|
567
|
+
self._conn.execute(
|
|
568
|
+
"INSERT INTO _gateway_lineage VALUES (?, ?, ?, ?, ?)",
|
|
569
|
+
(
|
|
570
|
+
batch["id"],
|
|
571
|
+
index,
|
|
572
|
+
mutation["entity"],
|
|
573
|
+
key,
|
|
574
|
+
canonical(mutation["source"]),
|
|
575
|
+
),
|
|
576
|
+
)
|
|
577
|
+
except ContractError as error:
|
|
578
|
+
# Only attach source after it has passed metadata validation.
|
|
579
|
+
context = {"batch": batch["id"], "index": index}
|
|
580
|
+
if isinstance(mutation, dict):
|
|
581
|
+
context["entity"] = mutation.get("entity")
|
|
582
|
+
if "key" in mutation:
|
|
583
|
+
context["key"] = mutation["key"]
|
|
584
|
+
try:
|
|
585
|
+
_source(mutation.get("source"), location)
|
|
586
|
+
context["source"] = mutation["source"]
|
|
587
|
+
except ContractError:
|
|
588
|
+
pass
|
|
589
|
+
raise error.contextualize(**context)
|
|
590
|
+
# Nested batches inside a tool defer references to the tool boundary.
|
|
591
|
+
return {
|
|
592
|
+
"status": "applied",
|
|
593
|
+
"batch": batch["id"],
|
|
594
|
+
"applied": len(batch["mutations"]),
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
def _single(self, op: str, entity: str, *, source: dict | None = None, **values):
|
|
598
|
+
self.apply_batch(
|
|
599
|
+
{
|
|
600
|
+
"id": uuid.uuid4().hex,
|
|
601
|
+
"target": self.contract.target,
|
|
602
|
+
"mapping": {"id": "gateway-store", "version": "1"},
|
|
603
|
+
"coverage": "partial",
|
|
604
|
+
"mutations": [
|
|
605
|
+
{
|
|
606
|
+
"op": op,
|
|
607
|
+
"entity": entity,
|
|
608
|
+
**values,
|
|
609
|
+
"source": (source if source is not None else {"record": f"store.{op}"}),
|
|
610
|
+
}
|
|
611
|
+
],
|
|
612
|
+
}
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def insert(self, entity: str, value: dict, *, source: dict | None = None) -> dict:
|
|
616
|
+
self._single("insert", entity, value=value, source=source)
|
|
617
|
+
return json.loads(canonical(value))
|
|
618
|
+
|
|
619
|
+
def update(self, entity: str, key: dict, patch: dict, *, source: dict | None = None) -> dict:
|
|
620
|
+
with self._lock:
|
|
621
|
+
self._single("update", entity, key=key, value=patch, source=source)
|
|
622
|
+
return self.get(entity, key)
|
|
623
|
+
|
|
624
|
+
def delete(self, entity: str, key: dict, *, source: dict | None = None) -> None:
|
|
625
|
+
self._single("delete", entity, key=key, source=source)
|
|
626
|
+
|
|
627
|
+
@contextmanager
|
|
628
|
+
def ingest(
|
|
629
|
+
self,
|
|
630
|
+
*,
|
|
631
|
+
source: dict,
|
|
632
|
+
mapping: dict,
|
|
633
|
+
batch_id: str | None = None,
|
|
634
|
+
limit: int = 50,
|
|
635
|
+
) -> Iterator[IngestConnection]:
|
|
636
|
+
"""Schema-gated raw SQL: write freely, commit only if every row conforms.
|
|
637
|
+
|
|
638
|
+
The session yields a connection that may INSERT/UPDATE/DELETE domain
|
|
639
|
+
tables and create TEMP staging objects. ``gateway_key(entity, payload)``
|
|
640
|
+
computes ``_key`` in SQL. On exit the store validates all persisted
|
|
641
|
+
state; any violation (up to ``limit`` are reported together) rolls the
|
|
642
|
+
whole session back. Committed changes are recorded as one batch with a
|
|
643
|
+
lineage row per touched key, so raw ingestion keeps provenance.
|
|
644
|
+
"""
|
|
645
|
+
_source(source, "/ingest/source")
|
|
646
|
+
members(mapping, {"id", "version"}, {"id", "version"}, "/ingest/mapping")
|
|
647
|
+
for key, value in mapping.items():
|
|
648
|
+
nonempty(value, f"/ingest/mapping/{key}")
|
|
649
|
+
with self._lock:
|
|
650
|
+
if self._depth or self._ingesting:
|
|
651
|
+
fail(
|
|
652
|
+
"transaction",
|
|
653
|
+
"Ingest needs no active tool, batch or ingest transaction",
|
|
654
|
+
)
|
|
655
|
+
batch = batch_id if batch_id is not None else uuid.uuid4().hex
|
|
656
|
+
nonempty(batch, "/ingest/id")
|
|
657
|
+
if self._conn.execute("SELECT 1 FROM _gateway_batches WHERE id = ?", (batch,)).fetchone():
|
|
658
|
+
fail(
|
|
659
|
+
"idempotency_conflict",
|
|
660
|
+
"Ingest ID was already applied; raw sessions are not replayable",
|
|
661
|
+
"/ingest/id",
|
|
662
|
+
batch=batch,
|
|
663
|
+
)
|
|
664
|
+
before = self._state()
|
|
665
|
+
connection = IngestConnection(self)
|
|
666
|
+
with self._trusted():
|
|
667
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
668
|
+
self._depth = 1
|
|
669
|
+
self._ingesting = True
|
|
670
|
+
try:
|
|
671
|
+
yield connection
|
|
672
|
+
violations = self._violations(limit)
|
|
673
|
+
if violations:
|
|
674
|
+
fail(
|
|
675
|
+
"ingest_rejected",
|
|
676
|
+
"Persisted rows violate the contract; nothing was committed",
|
|
677
|
+
batch=batch,
|
|
678
|
+
violations=[error.detail for error in violations],
|
|
679
|
+
truncated=len(violations) >= limit,
|
|
680
|
+
)
|
|
681
|
+
changes = self._changes(before, self._state())
|
|
682
|
+
metadata = {
|
|
683
|
+
"id": batch,
|
|
684
|
+
"kind": "ingest",
|
|
685
|
+
"target": self.contract.target,
|
|
686
|
+
"mapping": mapping,
|
|
687
|
+
"coverage": "partial",
|
|
688
|
+
}
|
|
689
|
+
with self._trusted():
|
|
690
|
+
if changes:
|
|
691
|
+
self._conn.execute(
|
|
692
|
+
"INSERT INTO _gateway_batches VALUES (?, ?, ?)",
|
|
693
|
+
(batch, digest(encoded(changes)), canonical(metadata)),
|
|
694
|
+
)
|
|
695
|
+
self._conn.executemany(
|
|
696
|
+
"INSERT INTO _gateway_lineage VALUES (?, ?, ?, ?, ?)",
|
|
697
|
+
[
|
|
698
|
+
(batch, index, entity, key, canonical(source))
|
|
699
|
+
for index, (entity, key, _) in enumerate(changes)
|
|
700
|
+
],
|
|
701
|
+
)
|
|
702
|
+
self._conn.execute("COMMIT")
|
|
703
|
+
except BaseException:
|
|
704
|
+
with self._trusted():
|
|
705
|
+
self._conn.execute("ROLLBACK")
|
|
706
|
+
raise
|
|
707
|
+
finally:
|
|
708
|
+
self._ingesting = False
|
|
709
|
+
self._depth = 0
|
|
710
|
+
connection._open = False
|
|
711
|
+
self._drop_temp_objects()
|
|
712
|
+
counts = {op: 0 for op in ("insert", "update", "delete")}
|
|
713
|
+
for _, _, op in changes:
|
|
714
|
+
counts[op] += 1
|
|
715
|
+
connection.result = {
|
|
716
|
+
"status": "applied" if changes else "unchanged",
|
|
717
|
+
"batch": batch,
|
|
718
|
+
"inserted": counts["insert"],
|
|
719
|
+
"updated": counts["update"],
|
|
720
|
+
"deleted": counts["delete"],
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
def _drop_temp_objects(self) -> None:
|
|
724
|
+
"""TEMP staging belongs to the session, not the connection."""
|
|
725
|
+
with self._trusted():
|
|
726
|
+
objects = self._conn.execute(
|
|
727
|
+
"SELECT type, name FROM sqlite_temp_master " "WHERE type IN ('view', 'table') ORDER BY type DESC"
|
|
728
|
+
).fetchall()
|
|
729
|
+
for kind, name in objects:
|
|
730
|
+
self._conn.execute(f"DROP {kind} IF EXISTS temp.{quote(name)}")
|
|
731
|
+
|
|
732
|
+
def _state(self) -> dict[str, dict[str, str]]:
|
|
733
|
+
"""Persisted ``_key -> payload`` per entity, for change attribution."""
|
|
734
|
+
with self._trusted():
|
|
735
|
+
return {
|
|
736
|
+
name: dict(self._conn.execute(f"SELECT _key, payload FROM {quote(name)}"))
|
|
737
|
+
for name in self.contract.entities
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
@staticmethod
|
|
741
|
+
def _changes(before: dict, after: dict) -> list[list[str]]:
|
|
742
|
+
changes = []
|
|
743
|
+
for name in sorted(before):
|
|
744
|
+
old, new = before[name], after[name]
|
|
745
|
+
for key in sorted(set(old) | set(new)):
|
|
746
|
+
if old.get(key) == new.get(key):
|
|
747
|
+
continue
|
|
748
|
+
if key not in old:
|
|
749
|
+
op = "insert"
|
|
750
|
+
elif key not in new:
|
|
751
|
+
op = "delete"
|
|
752
|
+
else:
|
|
753
|
+
op = "update"
|
|
754
|
+
changes.append([name, key, op])
|
|
755
|
+
return changes
|
|
756
|
+
|
|
757
|
+
def lineage(self, entity: str, key: dict) -> list[dict]:
|
|
758
|
+
with self._lock:
|
|
759
|
+
encoded_key = self._key(entity, key, exact=True)
|
|
760
|
+
rows = self._conn.execute(
|
|
761
|
+
"SELECT b.metadata, l.ordinal, l.source "
|
|
762
|
+
"FROM _gateway_lineage l "
|
|
763
|
+
"JOIN _gateway_batches b ON b.id=l.batch "
|
|
764
|
+
"WHERE l.entity=? AND l.key=? "
|
|
765
|
+
"ORDER BY b.rowid, l.ordinal",
|
|
766
|
+
(entity, encoded_key),
|
|
767
|
+
)
|
|
768
|
+
return [
|
|
769
|
+
{
|
|
770
|
+
"batch": json.loads(row[0]),
|
|
771
|
+
"index": row[1],
|
|
772
|
+
"source": json.loads(row[2]),
|
|
773
|
+
}
|
|
774
|
+
for row in rows
|
|
775
|
+
]
|
|
776
|
+
|
|
777
|
+
def _row_violation(self, name: str, entity: dict, row) -> ContractError | None:
|
|
778
|
+
"""The contract violation for one persisted row, with lineage context."""
|
|
779
|
+
try:
|
|
780
|
+
value = parse_json(row[1], f"{entity['location']}/schema")
|
|
781
|
+
except ContractError as error:
|
|
782
|
+
return error.contextualize(entity=name)
|
|
783
|
+
except (ValueError, TypeError):
|
|
784
|
+
return ContractError("state_payload", "Stored entity payload is not JSON", entity=name)
|
|
785
|
+
try:
|
|
786
|
+
validate(entity["schema"], value, location=f"{entity['location']}/schema")
|
|
787
|
+
if self._key(name, value) != row[0]:
|
|
788
|
+
fail("key", "Stored key differs from payload identity", entity=name)
|
|
789
|
+
for rel in entity["relationships"]:
|
|
790
|
+
keys = [payload_value(value, field) for field in rel["fields"]]
|
|
791
|
+
if all(k is None for k in keys):
|
|
792
|
+
continue
|
|
793
|
+
if any(k is None for k in keys):
|
|
794
|
+
fail(
|
|
795
|
+
"foreign_key",
|
|
796
|
+
"Composite references must be wholly present or wholly absent/null",
|
|
797
|
+
f"{entity['location']}/relationships",
|
|
798
|
+
)
|
|
799
|
+
target = self.contract.entities[rel["target"]]
|
|
800
|
+
target_key = dict(zip(target["primary_key"], keys))
|
|
801
|
+
if self.get(rel["target"], target_key) is None:
|
|
802
|
+
fail(
|
|
803
|
+
"foreign_key",
|
|
804
|
+
"Relationship target does not exist; incomplete evidence requires an explicit mapping decision",
|
|
805
|
+
f"{entity['location']}/relationships",
|
|
806
|
+
)
|
|
807
|
+
except ContractError as error:
|
|
808
|
+
try:
|
|
809
|
+
diagnostic_key = json.loads(row[0])
|
|
810
|
+
except (ValueError, TypeError):
|
|
811
|
+
diagnostic_key = None
|
|
812
|
+
context = {"entity": name, "key": diagnostic_key}
|
|
813
|
+
with self._trusted():
|
|
814
|
+
lineage = self._conn.execute(
|
|
815
|
+
"SELECT batch, ordinal, source FROM _gateway_lineage "
|
|
816
|
+
"WHERE entity=? AND key=? ORDER BY rowid DESC LIMIT 1",
|
|
817
|
+
(name, row[0]),
|
|
818
|
+
).fetchone()
|
|
819
|
+
if lineage:
|
|
820
|
+
context.update(batch=lineage[0], index=lineage[1], source=json.loads(lineage[2]))
|
|
821
|
+
return error.contextualize(**context)
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
def _violations(self, limit: int | None = None) -> list[ContractError]:
|
|
825
|
+
"""Contract violations in persisted state, at most ``limit`` of them."""
|
|
826
|
+
found: list[ContractError] = []
|
|
827
|
+
with self._trusted():
|
|
828
|
+
rows = [
|
|
829
|
+
(name, entity, row)
|
|
830
|
+
for name, entity in self.contract.entities.items()
|
|
831
|
+
for row in self._conn.execute(f"SELECT _key, payload FROM {quote(name)} ORDER BY _key").fetchall()
|
|
832
|
+
]
|
|
833
|
+
for name, entity, row in rows:
|
|
834
|
+
error = self._row_violation(name, entity, row)
|
|
835
|
+
if error is not None:
|
|
836
|
+
found.append(error)
|
|
837
|
+
if limit is not None and len(found) >= limit:
|
|
838
|
+
return found
|
|
839
|
+
with self._trusted():
|
|
840
|
+
if self._conn.execute("PRAGMA foreign_key_check").fetchone():
|
|
841
|
+
found.append(ContractError("foreign_key", "Persisted SQLite relationship check failed"))
|
|
842
|
+
return found
|
|
843
|
+
|
|
844
|
+
def audit(self) -> None:
|
|
845
|
+
"""Validate real persisted payloads, key identity and strict references."""
|
|
846
|
+
with self._lock:
|
|
847
|
+
violations = self._violations(limit=1)
|
|
848
|
+
if violations:
|
|
849
|
+
raise violations[0]
|
|
850
|
+
|
|
851
|
+
def _audit_touched(self) -> None:
|
|
852
|
+
"""The commit-time audit of one transaction: the rows it wrote, re-read from
|
|
853
|
+
disk and checked like a full audit, plus SQLite's own relationship check over
|
|
854
|
+
the whole state (a delete that orphans an untouched row shows up there).
|
|
855
|
+
Rows the transaction never touched cannot have changed, so a transaction's
|
|
856
|
+
cost follows its size, not the size of the world."""
|
|
857
|
+
with self._trusted():
|
|
858
|
+
for name, key in sorted(self._touched):
|
|
859
|
+
entity = self.contract.entities[name]
|
|
860
|
+
row = self._conn.execute(f"SELECT _key, payload FROM {quote(name)} WHERE _key = ?", (key,)).fetchone()
|
|
861
|
+
if row is None:
|
|
862
|
+
continue
|
|
863
|
+
error = self._row_violation(name, entity, row)
|
|
864
|
+
if error is not None:
|
|
865
|
+
raise error
|
|
866
|
+
if self._conn.execute("PRAGMA foreign_key_check").fetchone():
|
|
867
|
+
raise ContractError("foreign_key", "Persisted SQLite relationship check failed")
|
|
868
|
+
|
|
869
|
+
def counts(self) -> dict[str, int]:
|
|
870
|
+
"""Rows per entity, counted by SQLite: no payload is decoded."""
|
|
871
|
+
with self._lock, self._trusted():
|
|
872
|
+
return {
|
|
873
|
+
name: self._conn.execute(f"SELECT COUNT(*) FROM {quote(name)}").fetchone()[0]
|
|
874
|
+
for name in self.contract.entities
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
def snapshot(self, destination: str | Path) -> Path:
|
|
878
|
+
with self._lock:
|
|
879
|
+
if self._depth:
|
|
880
|
+
fail("transaction", "Cannot snapshot uncommitted state")
|
|
881
|
+
self.audit()
|
|
882
|
+
dest = Path(destination)
|
|
883
|
+
if dest.exists():
|
|
884
|
+
fail("state_exists", "Snapshot destination already exists")
|
|
885
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
886
|
+
target = sqlite3.connect(dest)
|
|
887
|
+
try:
|
|
888
|
+
self._conn.backup(target)
|
|
889
|
+
finally:
|
|
890
|
+
target.close()
|
|
891
|
+
return dest
|
|
892
|
+
|
|
893
|
+
def close(self) -> None:
|
|
894
|
+
with self._lock:
|
|
895
|
+
if self._depth:
|
|
896
|
+
fail("transaction", "Cannot close an active tool/batch transaction")
|
|
897
|
+
self._conn.close()
|
|
898
|
+
self._closed = True
|
|
899
|
+
|
|
900
|
+
def __enter__(self):
|
|
901
|
+
return self
|
|
902
|
+
|
|
903
|
+
def __exit__(self, *args):
|
|
904
|
+
self.close()
|