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,755 @@
|
|
|
1
|
+
"""Rows in bulk: streamed from files, cut into chunks, checked and applied as a batch.
|
|
2
|
+
|
|
3
|
+
The runtime side of world data ingestion at scale. :func:`iter_rows` streams the
|
|
4
|
+
three rows-file shapes (JSON object of entity to rows, tagged JSONL, bare rows with
|
|
5
|
+
an entity) without loading a file; :func:`chunk_rows` cuts a row stream into gzipped
|
|
6
|
+
JSONL chunks on row boundaries; :func:`validate_chunks` checks every row's shape
|
|
7
|
+
against the contract; :func:`build_batch` applies chunks onto a parent snapshot in
|
|
8
|
+
dependency order and saves the new snapshot. Refusals everywhere are the one record
|
|
9
|
+
of the contract (section 2)::
|
|
10
|
+
|
|
11
|
+
{"chunk": 0, "index": 17, "entity": "users", "primaryKey": {"id": "u17"},
|
|
12
|
+
"field": "email", "pointer": "/users/17/email", "detail": "..."}
|
|
13
|
+
|
|
14
|
+
Standard library only, so the bridge and the api bundle can load it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import gzip
|
|
20
|
+
import hashlib
|
|
21
|
+
import io
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import IO, Any, Iterable, Iterator
|
|
27
|
+
|
|
28
|
+
from .compiler import Contract, quote
|
|
29
|
+
from .store import SQLiteStore
|
|
30
|
+
from .validation import ContractError, fail, validate
|
|
31
|
+
|
|
32
|
+
CHUNK_BYTES = 64 * 1024 * 1024
|
|
33
|
+
REPORT_CAP = 10_000
|
|
34
|
+
_READ = 256 * 1024
|
|
35
|
+
#: A first line longer than this is a one-line document, never a JSONL row.
|
|
36
|
+
_LINE_CAP = 1024 * 1024
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def encode_row(value: Any) -> str:
|
|
40
|
+
"""One JSONL line, the exact serialisation every client must match: compact
|
|
41
|
+
separators, non-ASCII kept as is, key order kept as given."""
|
|
42
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------- streaming rows
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _Reader:
|
|
49
|
+
"""A text stream with a growing buffer, for decoding JSON values one at a time.
|
|
50
|
+
|
|
51
|
+
The buffer only shrinks on ``compact()``, so a caller may note a position,
|
|
52
|
+
read ahead, and come back to it."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, stream: IO[str]):
|
|
55
|
+
self._stream = stream
|
|
56
|
+
self.buffer = ""
|
|
57
|
+
self.pos = 0
|
|
58
|
+
self.eof = False
|
|
59
|
+
|
|
60
|
+
def fill(self) -> bool:
|
|
61
|
+
if self.eof:
|
|
62
|
+
return False
|
|
63
|
+
more = self._stream.read(_READ)
|
|
64
|
+
if not more:
|
|
65
|
+
self.eof = True
|
|
66
|
+
return False
|
|
67
|
+
self.buffer += more
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def compact(self) -> None:
|
|
71
|
+
self.buffer = self.buffer[self.pos :]
|
|
72
|
+
self.pos = 0
|
|
73
|
+
|
|
74
|
+
def peek(self) -> str | None:
|
|
75
|
+
"""The next non-whitespace character, or None at the end."""
|
|
76
|
+
while True:
|
|
77
|
+
while self.pos < len(self.buffer) and self.buffer[self.pos] in " \t\r\n\ufeff":
|
|
78
|
+
self.pos += 1
|
|
79
|
+
if self.pos < len(self.buffer):
|
|
80
|
+
return self.buffer[self.pos]
|
|
81
|
+
if not self.fill():
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def take(self, char: str, where: str) -> None:
|
|
85
|
+
if self.peek() != char:
|
|
86
|
+
fail("rows", f"{where}: expected {char!r} in the JSON document", where)
|
|
87
|
+
self.pos += 1
|
|
88
|
+
|
|
89
|
+
def value(self, where: str) -> Any:
|
|
90
|
+
"""Decode the next JSON value, reading more of the stream as needed."""
|
|
91
|
+
decoder = json.JSONDecoder()
|
|
92
|
+
self.peek()
|
|
93
|
+
while True:
|
|
94
|
+
try:
|
|
95
|
+
parsed, end = decoder.raw_decode(self.buffer, self.pos)
|
|
96
|
+
except ValueError:
|
|
97
|
+
if self.fill():
|
|
98
|
+
continue
|
|
99
|
+
fail("rows", f"{where}: not valid JSON", where)
|
|
100
|
+
if end == len(self.buffer) and self.fill():
|
|
101
|
+
# The value may continue (a number, a literal) past the buffer's edge.
|
|
102
|
+
continue
|
|
103
|
+
self.pos = end
|
|
104
|
+
return parsed
|
|
105
|
+
|
|
106
|
+
def line(self, cap: int | None = None) -> str | None:
|
|
107
|
+
"""The next line without its newline, or None at the end. With ``cap``, a line
|
|
108
|
+
longer than it is handed back unconsumed (the caller treats it as a document)."""
|
|
109
|
+
while True:
|
|
110
|
+
rest = self.buffer[self.pos :]
|
|
111
|
+
newline = rest.find("\n")
|
|
112
|
+
if newline >= 0:
|
|
113
|
+
self.pos += newline + 1
|
|
114
|
+
return rest[:newline]
|
|
115
|
+
if cap is not None and len(rest) > cap:
|
|
116
|
+
return rest
|
|
117
|
+
if not self.fill():
|
|
118
|
+
break
|
|
119
|
+
rest = self.buffer[self.pos :]
|
|
120
|
+
if not rest:
|
|
121
|
+
return None
|
|
122
|
+
self.pos = len(self.buffer)
|
|
123
|
+
return rest
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _open_text(source: str | Path) -> IO[str]:
|
|
127
|
+
if str(source) == "-":
|
|
128
|
+
return io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8")
|
|
129
|
+
path = Path(source)
|
|
130
|
+
if path.suffix == ".gz":
|
|
131
|
+
return io.TextIOWrapper(gzip.open(path, "rb"), encoding="utf-8")
|
|
132
|
+
return open(path, "r", encoding="utf-8")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _row(value: Any, where: str) -> dict:
|
|
136
|
+
if not isinstance(value, dict):
|
|
137
|
+
fail("rows", f"{where}: a row must be a JSON object", where)
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _stream_array(reader: _Reader, where: str) -> Iterator[dict]:
|
|
142
|
+
reader.take("[", where)
|
|
143
|
+
index = 0
|
|
144
|
+
while True:
|
|
145
|
+
if reader.peek() == "]":
|
|
146
|
+
reader.pos += 1
|
|
147
|
+
return
|
|
148
|
+
yield _row(reader.value(f"{where}[{index}]"), f"{where}[{index}]")
|
|
149
|
+
reader.compact()
|
|
150
|
+
index += 1
|
|
151
|
+
if reader.peek() == ",":
|
|
152
|
+
reader.pos += 1
|
|
153
|
+
elif reader.peek() != "]":
|
|
154
|
+
fail("rows", f"{where}: expected ',' or ']' after row {index - 1}", where)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _stream_document(
|
|
158
|
+
reader: _Reader, where: str, entity: str | None, seen: dict[str, int] | None = None
|
|
159
|
+
) -> Iterator[tuple[str, dict]]:
|
|
160
|
+
"""``{"<entity>": [row, ...], ...}`` streamed one row at a time. ``seen`` collects
|
|
161
|
+
every entity the document names, an empty array included (a replace names it)."""
|
|
162
|
+
reader.take("{", where)
|
|
163
|
+
while True:
|
|
164
|
+
if reader.peek() == "}":
|
|
165
|
+
reader.pos += 1
|
|
166
|
+
return
|
|
167
|
+
name = reader.value(where)
|
|
168
|
+
if not isinstance(name, str):
|
|
169
|
+
fail("rows", f"{where}: object keys must be entity names", where)
|
|
170
|
+
reader.take(":", where)
|
|
171
|
+
if reader.peek() != "[":
|
|
172
|
+
fail("rows", f'{where}: {name} must map to an array of rows (JSONL lines are {{"entity", "row"}})', where)
|
|
173
|
+
if entity is not None and name != entity:
|
|
174
|
+
fail("rows", f"{where} maps entities; drop --entity or make the file a bare array", where)
|
|
175
|
+
if seen is not None:
|
|
176
|
+
seen.setdefault(name, 0)
|
|
177
|
+
for row in _stream_array(reader, f"{where}:{name}"):
|
|
178
|
+
if seen is not None:
|
|
179
|
+
seen[name] += 1
|
|
180
|
+
yield name, row
|
|
181
|
+
if reader.peek() == ",":
|
|
182
|
+
reader.pos += 1
|
|
183
|
+
elif reader.peek() != "}":
|
|
184
|
+
fail("rows", f"{where}: expected ',' or '}}' after {name}", where)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _stream_jsonl(reader: _Reader, first: str, where: str, entity: str | None) -> Iterator[tuple[str, dict]]:
|
|
188
|
+
number = 0
|
|
189
|
+
line: str | None = first
|
|
190
|
+
while line is not None:
|
|
191
|
+
number += 1
|
|
192
|
+
if line.strip():
|
|
193
|
+
try:
|
|
194
|
+
item = json.loads(line)
|
|
195
|
+
except ValueError:
|
|
196
|
+
fail("rows", f"{where}:{number} is not JSON", where)
|
|
197
|
+
if entity is not None:
|
|
198
|
+
yield entity, _row(item, f"{where}:{number}")
|
|
199
|
+
elif not isinstance(item, dict) or set(item) != {"entity", "row"} or not isinstance(item["entity"], str):
|
|
200
|
+
fail(
|
|
201
|
+
"rows",
|
|
202
|
+
f'{where}:{number}: expected {{"entity": name, "row": {{…}}}} (or pass --entity for bare rows)',
|
|
203
|
+
where,
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
yield item["entity"], _row(item["row"], f"{where}:{number}")
|
|
207
|
+
reader.compact()
|
|
208
|
+
line = reader.line()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def iter_file_rows(
|
|
212
|
+
stream: IO[str], where: str, *, entity: str | None = None, seen: dict[str, int] | None = None
|
|
213
|
+
) -> Iterator[tuple[str, dict]]:
|
|
214
|
+
"""Stream ``(entity, row)`` from one rows file of any of the three shapes. ``seen``,
|
|
215
|
+
when given, collects entity names to row counts, an entity with no rows included."""
|
|
216
|
+
reader = _Reader(stream)
|
|
217
|
+
head = reader.peek()
|
|
218
|
+
if head is None:
|
|
219
|
+
return
|
|
220
|
+
if head == "[":
|
|
221
|
+
if entity is None:
|
|
222
|
+
fail("rows", f"{where} is a bare array; say which entity it holds with --entity", where)
|
|
223
|
+
if seen is not None:
|
|
224
|
+
seen.setdefault(entity, 0)
|
|
225
|
+
for row in _stream_array(reader, where):
|
|
226
|
+
if seen is not None:
|
|
227
|
+
seen[entity] += 1
|
|
228
|
+
yield entity, row
|
|
229
|
+
return
|
|
230
|
+
if head != "{":
|
|
231
|
+
fail("rows", f"{where} is neither a JSON object of entity -> rows nor an array of rows", where)
|
|
232
|
+
start = reader.pos
|
|
233
|
+
first = reader.line(cap=_LINE_CAP)
|
|
234
|
+
parsed: Any = None
|
|
235
|
+
complete = False
|
|
236
|
+
if first is not None and len(first) <= _LINE_CAP:
|
|
237
|
+
try:
|
|
238
|
+
parsed = json.loads(first)
|
|
239
|
+
complete = True
|
|
240
|
+
except ValueError:
|
|
241
|
+
complete = False
|
|
242
|
+
if complete and isinstance(parsed, dict):
|
|
243
|
+
is_row = (entity is None and set(parsed) == {"entity", "row"}) or (
|
|
244
|
+
entity is not None and not (set(parsed) == {entity} and isinstance(parsed[entity], list))
|
|
245
|
+
)
|
|
246
|
+
if is_row:
|
|
247
|
+
for name, row in _stream_jsonl(reader, first, where, entity):
|
|
248
|
+
if seen is not None:
|
|
249
|
+
seen[name] = seen.get(name, 0) + 1
|
|
250
|
+
yield name, row
|
|
251
|
+
return
|
|
252
|
+
# A document: rewind to its first character and stream it.
|
|
253
|
+
reader.pos = start
|
|
254
|
+
yield from _stream_document(reader, where, entity, seen)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def iter_rows(
|
|
258
|
+
sources: Iterable[str | Path], *, entity: str | None = None, seen: dict[str, int] | None = None
|
|
259
|
+
) -> Iterator[tuple[str, dict]]:
|
|
260
|
+
"""Stream ``(entity, row)`` from rows files (``-`` is stdin, ``.gz`` is gunzipped)
|
|
261
|
+
in order, never holding a whole file."""
|
|
262
|
+
for source in sources:
|
|
263
|
+
where = str(source)
|
|
264
|
+
try:
|
|
265
|
+
stream = _open_text(source)
|
|
266
|
+
except OSError:
|
|
267
|
+
fail("rows", f"Cannot read {where}", where)
|
|
268
|
+
with stream:
|
|
269
|
+
try:
|
|
270
|
+
yield from iter_file_rows(stream, where, entity=entity, seen=seen)
|
|
271
|
+
except UnicodeError:
|
|
272
|
+
fail("rows", f"Cannot read {where} as UTF-8 text", where)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def count_rows(sources: Iterable[str | Path], *, entity: str | None = None, cap: int | None = None) -> int:
|
|
276
|
+
"""How many rows the files hold, stopping early past ``cap``."""
|
|
277
|
+
total = 0
|
|
278
|
+
for _ in iter_rows(sources, entity=entity):
|
|
279
|
+
total += 1
|
|
280
|
+
if cap is not None and total > cap:
|
|
281
|
+
break
|
|
282
|
+
return total
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------- chunks
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass
|
|
289
|
+
class Chunk:
|
|
290
|
+
ordinal: int
|
|
291
|
+
path: Path
|
|
292
|
+
digest: str # sha256 of the gzipped bytes: the chunk's address
|
|
293
|
+
bytes: int # gzipped size
|
|
294
|
+
rows: int
|
|
295
|
+
entity: str | None
|
|
296
|
+
content_bytes: int # the JSONL before gzip
|
|
297
|
+
content_sha256: str
|
|
298
|
+
first_row: int # index of the chunk's first row in the input stream
|
|
299
|
+
|
|
300
|
+
def manifest_entry(self) -> dict:
|
|
301
|
+
return {
|
|
302
|
+
"ordinal": self.ordinal,
|
|
303
|
+
"digest": self.digest,
|
|
304
|
+
"bytes": self.bytes,
|
|
305
|
+
"rows": self.rows,
|
|
306
|
+
"entity": self.entity,
|
|
307
|
+
"chunkId": None,
|
|
308
|
+
"uploaded": False,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class _ChunkWriter:
|
|
313
|
+
def __init__(self, path: Path, ordinal: int, entity: str | None, first_row: int):
|
|
314
|
+
self.path = path
|
|
315
|
+
self.ordinal = ordinal
|
|
316
|
+
self.entity = entity
|
|
317
|
+
self.first_row = first_row
|
|
318
|
+
self.rows = 0
|
|
319
|
+
self.content_bytes = 0
|
|
320
|
+
self._hash = hashlib.sha256()
|
|
321
|
+
self._raw = open(path, "wb")
|
|
322
|
+
self._gz = gzip.GzipFile(fileobj=self._raw, mode="wb", mtime=0)
|
|
323
|
+
|
|
324
|
+
def write(self, line: bytes) -> None:
|
|
325
|
+
self._gz.write(line)
|
|
326
|
+
self._hash.update(line)
|
|
327
|
+
self.rows += 1
|
|
328
|
+
self.content_bytes += len(line)
|
|
329
|
+
|
|
330
|
+
def close(self) -> Chunk:
|
|
331
|
+
self._gz.close()
|
|
332
|
+
self._raw.close()
|
|
333
|
+
digest = hashlib.sha256()
|
|
334
|
+
with open(self.path, "rb") as stream:
|
|
335
|
+
for block in iter(lambda: stream.read(_READ), b""):
|
|
336
|
+
digest.update(block)
|
|
337
|
+
return Chunk(
|
|
338
|
+
ordinal=self.ordinal,
|
|
339
|
+
path=self.path,
|
|
340
|
+
digest=digest.hexdigest(),
|
|
341
|
+
bytes=self.path.stat().st_size,
|
|
342
|
+
rows=self.rows,
|
|
343
|
+
entity=self.entity,
|
|
344
|
+
content_bytes=self.content_bytes,
|
|
345
|
+
content_sha256=self._hash.hexdigest(),
|
|
346
|
+
first_row=self.first_row,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def chunk_rows(
|
|
351
|
+
rows: Iterable[tuple[str, dict]],
|
|
352
|
+
workdir: str | Path,
|
|
353
|
+
*,
|
|
354
|
+
chunk_bytes: int = CHUNK_BYTES,
|
|
355
|
+
chunk_rows: int | None = None,
|
|
356
|
+
entity: str | None = None,
|
|
357
|
+
) -> Iterator[Chunk]:
|
|
358
|
+
"""Cut a row stream into gzipped JSONL chunk files under ``workdir``.
|
|
359
|
+
|
|
360
|
+
Lines are tagged ``{"entity", "row"}`` unless ``entity`` names the one entity
|
|
361
|
+
every row belongs to, in which case they are bare rows. A chunk closes when the
|
|
362
|
+
next line would take it past ``chunk_bytes`` of JSONL (never mid-row; a single
|
|
363
|
+
row larger than the bound is a chunk of its own) or when it holds ``chunk_rows``
|
|
364
|
+
rows. Chunks are yielded as they close, so one chunk is on disk at a time."""
|
|
365
|
+
workdir = Path(workdir)
|
|
366
|
+
workdir.mkdir(parents=True, exist_ok=True)
|
|
367
|
+
if chunk_bytes < 1 or (chunk_rows is not None and chunk_rows < 1):
|
|
368
|
+
fail("chunk", "chunk bounds must be positive", "/chunk")
|
|
369
|
+
writer: _ChunkWriter | None = None
|
|
370
|
+
ordinal = 0
|
|
371
|
+
seen = 0
|
|
372
|
+
for name, row in rows:
|
|
373
|
+
if entity is not None and name != entity:
|
|
374
|
+
fail("rows", f"Row {seen} belongs to {name}; a chunk declared for {entity} holds only its rows", "/rows")
|
|
375
|
+
line = (encode_row(row) if entity is not None else encode_row({"entity": name, "row": row})) + "\n"
|
|
376
|
+
encoded = line.encode("utf-8")
|
|
377
|
+
if writer is not None and (
|
|
378
|
+
(chunk_rows is not None and writer.rows >= chunk_rows)
|
|
379
|
+
or (writer.content_bytes > 0 and writer.content_bytes + len(encoded) > chunk_bytes)
|
|
380
|
+
):
|
|
381
|
+
yield writer.close()
|
|
382
|
+
writer = None
|
|
383
|
+
if writer is None:
|
|
384
|
+
writer = _ChunkWriter(workdir / f"chunk-{ordinal:05d}.jsonl.gz", ordinal, entity, seen)
|
|
385
|
+
ordinal += 1
|
|
386
|
+
writer.write(encoded)
|
|
387
|
+
seen += 1
|
|
388
|
+
if writer is not None:
|
|
389
|
+
yield writer.close()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def iter_chunk_rows(path: str | Path, entity: str | None = None) -> Iterator[tuple[str, dict]]:
|
|
393
|
+
"""Stream ``(entity, row)`` from one chunk file (gzip over JSONL; a plain file
|
|
394
|
+
is read as is), tagged lines or bare rows of the declared entity."""
|
|
395
|
+
path = Path(path)
|
|
396
|
+
with open(path, "rb") as probe:
|
|
397
|
+
compressed = probe.read(2) == b"\x1f\x8b"
|
|
398
|
+
opener = gzip.open if compressed else open
|
|
399
|
+
with opener(path, "rt", encoding="utf-8") as stream: # type: ignore[call-overload]
|
|
400
|
+
reader = _Reader(stream)
|
|
401
|
+
first = reader.line()
|
|
402
|
+
if first is not None:
|
|
403
|
+
yield from _stream_jsonl(reader, first, str(path), entity)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ---------------------------------------------------------------- refusals
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _field_of(error: ContractError) -> str | None:
|
|
410
|
+
path = error.detail.get("path") or ""
|
|
411
|
+
return path[1:] if path.startswith("/") and len(path) > 1 else None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def refusal(
|
|
415
|
+
chunk: int | None,
|
|
416
|
+
index: int | None,
|
|
417
|
+
entity: str | None,
|
|
418
|
+
row: Any,
|
|
419
|
+
field_name: str | None,
|
|
420
|
+
detail: str,
|
|
421
|
+
contract: Contract | None = None,
|
|
422
|
+
) -> dict:
|
|
423
|
+
"""One refusal record. ``primaryKey`` is the row's key fields when it carries
|
|
424
|
+
them all, ``field`` the property refused (None for a row-level refusal)."""
|
|
425
|
+
key = None
|
|
426
|
+
if contract is not None and isinstance(row, dict) and entity in contract.entities:
|
|
427
|
+
fields = contract.entities[entity]["primary_key"]
|
|
428
|
+
if all(name in row for name in fields):
|
|
429
|
+
key = {name: row[name] for name in fields}
|
|
430
|
+
pointer = "/" + (entity or "")
|
|
431
|
+
if index is not None:
|
|
432
|
+
pointer += f"/{index}"
|
|
433
|
+
if field_name:
|
|
434
|
+
pointer += f"/{field_name}"
|
|
435
|
+
return {
|
|
436
|
+
"chunk": chunk,
|
|
437
|
+
"index": index,
|
|
438
|
+
"entity": entity,
|
|
439
|
+
"primaryKey": key,
|
|
440
|
+
"field": field_name,
|
|
441
|
+
"pointer": pointer,
|
|
442
|
+
"detail": detail,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def shape_refusal(contract: Contract, chunk: int, index: int, entity: str, row: Any) -> dict | None:
|
|
447
|
+
"""A row's shape against the contract: type, bounds, required, unknown property
|
|
448
|
+
and primary key present. No reference checks. None when the row conforms."""
|
|
449
|
+
if entity not in contract.entities:
|
|
450
|
+
return refusal(
|
|
451
|
+
chunk,
|
|
452
|
+
index,
|
|
453
|
+
entity,
|
|
454
|
+
row,
|
|
455
|
+
None,
|
|
456
|
+
f"Unknown entity {entity}; the world has {', '.join(sorted(contract.entities))}",
|
|
457
|
+
)
|
|
458
|
+
if not isinstance(row, dict):
|
|
459
|
+
return refusal(chunk, index, entity, row, None, "A row must be an object")
|
|
460
|
+
spec = contract.entities[entity]
|
|
461
|
+
for name in spec["primary_key"]:
|
|
462
|
+
if name not in row:
|
|
463
|
+
return refusal(chunk, index, entity, row, name, f"Row is missing primary key field {name!r}", contract)
|
|
464
|
+
try:
|
|
465
|
+
validate(spec["schema"], row, location=f"{spec['location']}/schema")
|
|
466
|
+
except ContractError as error:
|
|
467
|
+
return refusal(chunk, index, entity, row, _field_of(error), error.detail["message"], contract)
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def write_report(records: Iterable[dict], destination: IO[str], *, cap: int = REPORT_CAP) -> int:
|
|
472
|
+
"""Refusal records as JSONL, at most ``cap`` of them followed by
|
|
473
|
+
``{"truncated": true, "total": N}`` when there were more. Returns the total."""
|
|
474
|
+
total = 0
|
|
475
|
+
for record in records:
|
|
476
|
+
total += 1
|
|
477
|
+
if total <= cap:
|
|
478
|
+
destination.write(encode_row(record) + "\n")
|
|
479
|
+
if total > cap:
|
|
480
|
+
destination.write(encode_row({"truncated": True, "total": total}) + "\n")
|
|
481
|
+
return total
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ---------------------------------------------------------------- batches
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@dataclass
|
|
488
|
+
class ChunkSpec:
|
|
489
|
+
ordinal: int
|
|
490
|
+
path: Path
|
|
491
|
+
entity: str | None = None
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@dataclass
|
|
495
|
+
class _Staged:
|
|
496
|
+
"""The valid rows of a batch after the in-batch merge, keyed the way the store keys."""
|
|
497
|
+
|
|
498
|
+
rows: dict[str, dict[str, dict]] = field(default_factory=dict) # entity -> key -> row
|
|
499
|
+
origin: dict[str, dict[str, tuple[int, int]]] = field(default_factory=dict) # entity -> key -> (chunk, index)
|
|
500
|
+
refusals: list[dict] = field(default_factory=list)
|
|
501
|
+
read: int = 0
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def dependency_order(contract: Contract) -> list[str]:
|
|
505
|
+
"""Entities with the targets of relationships first; a cycle keeps name order."""
|
|
506
|
+
remaining = dict(sorted(contract.entities.items()))
|
|
507
|
+
ordered: list[str] = []
|
|
508
|
+
while remaining:
|
|
509
|
+
ready = [
|
|
510
|
+
name
|
|
511
|
+
for name, entity in remaining.items()
|
|
512
|
+
if all(rel["target"] == name or rel["target"] not in remaining for rel in entity["relationships"])
|
|
513
|
+
]
|
|
514
|
+
if not ready:
|
|
515
|
+
ready = [next(iter(remaining))]
|
|
516
|
+
for name in ready:
|
|
517
|
+
ordered.append(name)
|
|
518
|
+
del remaining[name]
|
|
519
|
+
return ordered
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _stage(
|
|
523
|
+
contract: Contract,
|
|
524
|
+
store: SQLiteStore,
|
|
525
|
+
chunks: Iterable[ChunkSpec],
|
|
526
|
+
merge: str,
|
|
527
|
+
staged: _Staged | None = None,
|
|
528
|
+
) -> _Staged:
|
|
529
|
+
"""Shape-check every row of the chunks and merge them by primary key."""
|
|
530
|
+
staged = staged or _Staged()
|
|
531
|
+
for chunk in chunks:
|
|
532
|
+
for index, (entity, row) in enumerate(iter_chunk_rows(chunk.path, chunk.entity)):
|
|
533
|
+
staged.read += 1
|
|
534
|
+
refused = shape_refusal(contract, chunk.ordinal, index, entity, row)
|
|
535
|
+
if refused is not None:
|
|
536
|
+
staged.refusals.append(refused)
|
|
537
|
+
continue
|
|
538
|
+
key = store._key(entity, row)
|
|
539
|
+
held = staged.rows.setdefault(entity, {})
|
|
540
|
+
origin = staged.origin.setdefault(entity, {})
|
|
541
|
+
if key in held and merge == "fill":
|
|
542
|
+
first = held[key]
|
|
543
|
+
for name, value in row.items():
|
|
544
|
+
if name not in first:
|
|
545
|
+
first[name] = value
|
|
546
|
+
continue
|
|
547
|
+
held[key] = row
|
|
548
|
+
origin[key] = (chunk.ordinal, index)
|
|
549
|
+
return staged
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _reference_refusals(contract: Contract, store: SQLiteStore, staged: _Staged, mode: str) -> list[dict]:
|
|
553
|
+
"""Every row whose relationship points at a key neither the parent snapshot
|
|
554
|
+
(minus replaced entities) nor the batch holds."""
|
|
555
|
+
refusals: list[dict] = []
|
|
556
|
+
known: dict[str, set[str]] = {}
|
|
557
|
+
|
|
558
|
+
def keys_of(target: str) -> set[str]:
|
|
559
|
+
if target not in known:
|
|
560
|
+
held = set(staged.rows.get(target, {}))
|
|
561
|
+
if not (mode == "replace" and target in staged.rows):
|
|
562
|
+
with store._trusted():
|
|
563
|
+
held |= {row[0] for row in store._conn.execute(f"SELECT _key FROM {quote(target)}")}
|
|
564
|
+
known[target] = held
|
|
565
|
+
return known[target]
|
|
566
|
+
|
|
567
|
+
for entity, rows in staged.rows.items():
|
|
568
|
+
relationships = contract.entities[entity]["relationships"]
|
|
569
|
+
if not relationships:
|
|
570
|
+
continue
|
|
571
|
+
dropped = []
|
|
572
|
+
for key, row in rows.items():
|
|
573
|
+
for rel in relationships:
|
|
574
|
+
values = [row.get(name) for name in rel["fields"]]
|
|
575
|
+
if all(value is None for value in values):
|
|
576
|
+
continue
|
|
577
|
+
chunk, index = staged.origin[entity][key]
|
|
578
|
+
if any(value is None for value in values):
|
|
579
|
+
refusals.append(
|
|
580
|
+
refusal(
|
|
581
|
+
chunk,
|
|
582
|
+
index,
|
|
583
|
+
entity,
|
|
584
|
+
row,
|
|
585
|
+
rel["fields"][0],
|
|
586
|
+
"Composite references must be wholly present or wholly absent/null",
|
|
587
|
+
contract,
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
dropped.append(key)
|
|
591
|
+
break
|
|
592
|
+
target = rel["target"]
|
|
593
|
+
try:
|
|
594
|
+
wanted = store._key(target, dict(zip(contract.entities[target]["primary_key"], values)))
|
|
595
|
+
except ContractError as error:
|
|
596
|
+
refusals.append(
|
|
597
|
+
refusal(chunk, index, entity, row, rel["fields"][0], error.detail["message"], contract)
|
|
598
|
+
)
|
|
599
|
+
dropped.append(key)
|
|
600
|
+
break
|
|
601
|
+
if wanted not in keys_of(target):
|
|
602
|
+
refusals.append(
|
|
603
|
+
refusal(
|
|
604
|
+
chunk,
|
|
605
|
+
index,
|
|
606
|
+
entity,
|
|
607
|
+
row,
|
|
608
|
+
rel["fields"][0],
|
|
609
|
+
f"Relationship target does not exist in {target}",
|
|
610
|
+
contract,
|
|
611
|
+
)
|
|
612
|
+
)
|
|
613
|
+
dropped.append(key)
|
|
614
|
+
break
|
|
615
|
+
for key in dropped:
|
|
616
|
+
del rows[key]
|
|
617
|
+
return refusals
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _apply(
|
|
621
|
+
contract: Contract, store: SQLiteStore, staged: _Staged, mode: str, batch_id: str
|
|
622
|
+
) -> tuple[int, dict | None]:
|
|
623
|
+
"""Apply the staged rows as one batch; the count applied, or the store's refusal."""
|
|
624
|
+
from .data import seed_batch
|
|
625
|
+
|
|
626
|
+
ordered = {name: list(staged.rows[name].values()) for name in dependency_order(contract) if name in staged.rows}
|
|
627
|
+
batch = seed_batch(store, contract, ordered, mode, batch_id=batch_id)
|
|
628
|
+
origins = {id(row): staged.origin[entity][key] for entity, rows in staged.rows.items() for key, row in rows.items()}
|
|
629
|
+
try:
|
|
630
|
+
store.apply_batch(batch)
|
|
631
|
+
except ContractError as error:
|
|
632
|
+
detail = error.detail
|
|
633
|
+
mutation = None
|
|
634
|
+
index = detail.get("index")
|
|
635
|
+
if isinstance(index, int) and 0 <= index < len(batch["mutations"]):
|
|
636
|
+
mutation = batch["mutations"][index]
|
|
637
|
+
origin = origins.get(id(mutation.get("value"))) if mutation else None
|
|
638
|
+
entity = detail.get("entity") if isinstance(detail.get("entity"), str) else None
|
|
639
|
+
row = mutation.get("value") if mutation else None
|
|
640
|
+
chunk, position = origin if origin else (None, None)
|
|
641
|
+
return 0, refusal(chunk, position, entity, row, _field_of(error), detail["message"], contract)
|
|
642
|
+
applied = sum(1 for mutation in batch["mutations"] if mutation["op"] != "delete")
|
|
643
|
+
return applied, None
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def validate_chunks(contract: Contract, chunks: list[ChunkSpec]) -> dict:
|
|
647
|
+
"""``validate-rows``: shape-only, every row of every chunk."""
|
|
648
|
+
rows = 0
|
|
649
|
+
refusals: list[dict] = []
|
|
650
|
+
entities: dict[str, int] = {}
|
|
651
|
+
for chunk in chunks:
|
|
652
|
+
for index, (entity, row) in enumerate(iter_chunk_rows(chunk.path, chunk.entity)):
|
|
653
|
+
rows += 1
|
|
654
|
+
refused = shape_refusal(contract, chunk.ordinal, index, entity, row)
|
|
655
|
+
if refused is not None:
|
|
656
|
+
refusals.append(refused)
|
|
657
|
+
continue
|
|
658
|
+
entities[entity] = entities.get(entity, 0) + 1
|
|
659
|
+
return {"rows": rows, "refusals": refusals, "entities": entities}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def build_batch(
|
|
663
|
+
contract: Contract,
|
|
664
|
+
parent: Path,
|
|
665
|
+
chunks: list[ChunkSpec],
|
|
666
|
+
*,
|
|
667
|
+
mode: str = "append",
|
|
668
|
+
merge: str = "upsert",
|
|
669
|
+
atomic: bool = True,
|
|
670
|
+
batch_id: str,
|
|
671
|
+
parent_digest: str,
|
|
672
|
+
dry_run: bool = False,
|
|
673
|
+
) -> dict:
|
|
674
|
+
"""Apply ``chunks`` onto the snapshot at ``parent`` (a private copy: it is written
|
|
675
|
+
in place, so the caller passes the file the result should be) and describe the
|
|
676
|
+
result. Atomic: one transaction, any refusal means nothing applied and every
|
|
677
|
+
refusal found. Per chunk: one transaction per chunk, a refused chunk is left out
|
|
678
|
+
and the rest applied. ``dry_run`` finds the refusals and applies nothing."""
|
|
679
|
+
from .snapshot import batch_digest, file_sha256, write_sidecar, contract_hash
|
|
680
|
+
|
|
681
|
+
if mode not in ("append", "replace"):
|
|
682
|
+
fail("mode", "mode must be append or replace", "/mode")
|
|
683
|
+
if merge not in ("upsert", "fill"):
|
|
684
|
+
fail("merge", "merge must be upsert or fill", "/merge")
|
|
685
|
+
refusals: list[dict] = []
|
|
686
|
+
applied_rows = 0
|
|
687
|
+
applied_chunks: list[int] = []
|
|
688
|
+
applied_digests: list[str] = []
|
|
689
|
+
counts: dict[str, int] = {}
|
|
690
|
+
with SQLiteStore.open(parent, contract, audit=False) as store:
|
|
691
|
+
if atomic:
|
|
692
|
+
staged = _stage(contract, store, chunks, merge)
|
|
693
|
+
refusals.extend(staged.refusals)
|
|
694
|
+
refusals.extend(_reference_refusals(contract, store, staged, mode))
|
|
695
|
+
applied, refused = _apply(contract, store, staged, mode, batch_id)
|
|
696
|
+
if refused is not None:
|
|
697
|
+
refusals.append(refused)
|
|
698
|
+
if not refusals and not dry_run:
|
|
699
|
+
applied_rows = applied
|
|
700
|
+
applied_chunks = [chunk.ordinal for chunk in chunks]
|
|
701
|
+
applied_digests = [file_sha256(chunk.path) for chunk in chunks]
|
|
702
|
+
else:
|
|
703
|
+
for chunk in chunks:
|
|
704
|
+
staged = _stage(contract, store, [chunk], merge)
|
|
705
|
+
found = list(staged.refusals) + _reference_refusals(contract, store, staged, mode)
|
|
706
|
+
applied, refused = _apply(contract, store, staged, mode, f"{batch_id}/{chunk.ordinal}")
|
|
707
|
+
if refused is not None:
|
|
708
|
+
found.append(refused)
|
|
709
|
+
refusals.extend(found)
|
|
710
|
+
if found and not dry_run:
|
|
711
|
+
# The store rolled the chunk's transaction back; the rows it did
|
|
712
|
+
# accept are gone with it, so the chunk is refused as a whole.
|
|
713
|
+
continue
|
|
714
|
+
if not dry_run:
|
|
715
|
+
applied_rows += applied
|
|
716
|
+
applied_chunks.append(chunk.ordinal)
|
|
717
|
+
applied_digests.append(file_sha256(chunk.path))
|
|
718
|
+
counts = store.counts()
|
|
719
|
+
digest = batch_digest(parent_digest, batch_id, applied_digests)
|
|
720
|
+
result: dict[str, Any] = {
|
|
721
|
+
"contractHash": contract_hash(contract),
|
|
722
|
+
"batchDigest": digest,
|
|
723
|
+
"entityCounts": counts,
|
|
724
|
+
"appliedRows": applied_rows,
|
|
725
|
+
"refusals": refusals[:REPORT_CAP],
|
|
726
|
+
"refusalCount": len(refusals),
|
|
727
|
+
"appliedChunks": applied_chunks,
|
|
728
|
+
}
|
|
729
|
+
if dry_run or (atomic and refusals):
|
|
730
|
+
parent.unlink(missing_ok=True)
|
|
731
|
+
return result
|
|
732
|
+
write_sidecar(parent, result["contractHash"], digest, counts)
|
|
733
|
+
result["snapshot"] = parent
|
|
734
|
+
result["fileSha256"] = file_sha256(parent)
|
|
735
|
+
return result
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
__all__ = [
|
|
739
|
+
"CHUNK_BYTES",
|
|
740
|
+
"REPORT_CAP",
|
|
741
|
+
"Chunk",
|
|
742
|
+
"ChunkSpec",
|
|
743
|
+
"build_batch",
|
|
744
|
+
"chunk_rows",
|
|
745
|
+
"count_rows",
|
|
746
|
+
"dependency_order",
|
|
747
|
+
"encode_row",
|
|
748
|
+
"iter_chunk_rows",
|
|
749
|
+
"iter_file_rows",
|
|
750
|
+
"iter_rows",
|
|
751
|
+
"refusal",
|
|
752
|
+
"shape_refusal",
|
|
753
|
+
"validate_chunks",
|
|
754
|
+
"write_report",
|
|
755
|
+
]
|