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,992 @@
|
|
|
1
|
+
"""Gateway benchmark-container push client.
|
|
2
|
+
|
|
3
|
+
Drives the Gateway hub push flow end-to-end — the reimplemented (not Prime's
|
|
4
|
+
CLI-welded) client, talking only to a Gateway host, never to Prime:
|
|
5
|
+
|
|
6
|
+
resolve (get-or-create container)
|
|
7
|
+
-> versions (create/dedup at content hash, get a presigned PUT)
|
|
8
|
+
-> PUT the source tarball (with the real object sha256 as the S3 checksum)
|
|
9
|
+
-> finalize (mark the version READY)
|
|
10
|
+
|
|
11
|
+
Stdlib only (urllib/hashlib/base64/tarfile/tomllib) — no extra dependencies.
|
|
12
|
+
|
|
13
|
+
The source content hash is byte-compatible with Prime's hub (vendored helper).
|
|
14
|
+
The *tarball* object sha256 is computed separately and sent as the presign
|
|
15
|
+
checksum (base64) — distinct from the content hash — which is what satisfies the
|
|
16
|
+
x-amz-checksum-sha256 the Gateway presign bakes into the signature.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import gzip
|
|
23
|
+
import hashlib
|
|
24
|
+
import io
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tarfile
|
|
31
|
+
import urllib.error
|
|
32
|
+
import urllib.parse
|
|
33
|
+
import urllib.request
|
|
34
|
+
import zipfile
|
|
35
|
+
from email.parser import Parser
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, Dict, List, Optional
|
|
38
|
+
from urllib.parse import urlencode
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import tomllib
|
|
42
|
+
except ModuleNotFoundError:
|
|
43
|
+
import tomli as tomllib
|
|
44
|
+
|
|
45
|
+
from ..environment._materialize import validate_schema_materialization
|
|
46
|
+
from ..environment.schema.compiler import bundle_mode
|
|
47
|
+
from ..environment.schema.validation import ContractError
|
|
48
|
+
from ._archive import collect_archive_files, compute_content_hash
|
|
49
|
+
from ._tar import safe_tar_extract
|
|
50
|
+
from .versioning import (
|
|
51
|
+
bump_post_version,
|
|
52
|
+
bump_rc_version,
|
|
53
|
+
bump_version,
|
|
54
|
+
update_pyproject_version,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ── Local working-copy metadata (.gateway/.env-metadata.json) ────────────────
|
|
58
|
+
# The dir's link back to the hub: which container it came from, which VERSION
|
|
59
|
+
# it was based on, and which BRANCH it tracks. `pull`/`checkout` write it,
|
|
60
|
+
# `push` reads it (the fast-forward base) and rewrites it (your own push
|
|
61
|
+
# becomes the new base) — the same role .git/HEAD plays for git.
|
|
62
|
+
|
|
63
|
+
DEFAULT_BRANCH = "main"
|
|
64
|
+
_META_RELPATH = Path(".gateway") / ".env-metadata.json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def read_local_meta(env_path: Path) -> Optional[Dict[str, Any]]:
|
|
68
|
+
"""The dir's recorded base/branch, or None (an ad-hoc dir)."""
|
|
69
|
+
meta_path = Path(env_path) / _META_RELPATH
|
|
70
|
+
if not meta_path.is_file():
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
loaded = json.loads(meta_path.read_text())
|
|
74
|
+
return loaded if isinstance(loaded, dict) else None
|
|
75
|
+
except (OSError, json.JSONDecodeError):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def snapshot_tree_hashes(env_path: Path) -> Dict[str, str]:
|
|
80
|
+
"""rel path -> sha256 over the ARCHIVE-VISIBLE tree (collect_archive_files
|
|
81
|
+
rules). This is the working copy's base-file record: diffing against the
|
|
82
|
+
server manifest instead would surface phantom deletes, because the browser
|
|
83
|
+
manifest deliberately lists files (dotfiles, gitignored paths) that the
|
|
84
|
+
source tarball never contains."""
|
|
85
|
+
out: Dict[str, str] = {}
|
|
86
|
+
for file_path in collect_archive_files(env_path):
|
|
87
|
+
rel = str(file_path.relative_to(env_path)).replace("\\", "/")
|
|
88
|
+
out[rel] = hashlib.sha256(file_path.read_bytes()).hexdigest()
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def local_tree_state(env_path: Path) -> Dict[str, Any]:
|
|
93
|
+
"""The dir's hub link plus whether its ARCHIVE-VISIBLE tree still matches
|
|
94
|
+
the recorded base: ``{"meta", "dirty", "clean"}``. ``meta`` is None for an
|
|
95
|
+
ad-hoc dir (never pulled/pushed); ``dirty`` lists rel paths that changed,
|
|
96
|
+
appeared, or vanished vs the base snapshot; ``clean`` is True only when a
|
|
97
|
+
base exists AND nothing drifted — the one state where a local run can be
|
|
98
|
+
honestly pinned to the recorded version."""
|
|
99
|
+
meta = read_local_meta(Path(env_path))
|
|
100
|
+
base = meta.get("baseFiles") if meta else None
|
|
101
|
+
if not isinstance(base, dict):
|
|
102
|
+
return {"meta": meta, "dirty": [], "clean": False}
|
|
103
|
+
now = snapshot_tree_hashes(Path(env_path))
|
|
104
|
+
dirty = sorted({p for p, h in now.items() if base.get(p) != h} | {p for p in base if p not in now})
|
|
105
|
+
return {"meta": meta, "dirty": dirty, "clean": not dirty}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def write_local_meta(env_path: Path, meta: Dict[str, Any]) -> None:
|
|
109
|
+
meta_dir = Path(env_path) / _META_RELPATH.parent
|
|
110
|
+
meta_dir.mkdir(exist_ok=True)
|
|
111
|
+
(Path(env_path) / _META_RELPATH).write_text(json.dumps(meta, indent=2))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Includes the build-artifact dirs the ARCHIVE collector prunes (_archive.py) —
|
|
115
|
+
# otherwise `push --build-wheel` writes dist/<name>.whl BEFORE the manifest is
|
|
116
|
+
# collected and the browser lists a file the source bundle cannot serve.
|
|
117
|
+
_SKIP_DIRS = {
|
|
118
|
+
".git",
|
|
119
|
+
"__pycache__",
|
|
120
|
+
"node_modules",
|
|
121
|
+
".venv",
|
|
122
|
+
".mypy_cache",
|
|
123
|
+
"dist",
|
|
124
|
+
"build",
|
|
125
|
+
"outputs",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class BenchmarkPushError(RuntimeError):
|
|
130
|
+
"""Raised when any step of the push flow fails."""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_pyproject(env_path: Path) -> Dict[str, Any]:
|
|
134
|
+
"""Best-effort name/version/deps from pyproject.toml (empty dict if absent)."""
|
|
135
|
+
env_path = Path(env_path)
|
|
136
|
+
pp = env_path / "pyproject.toml"
|
|
137
|
+
if not pp.exists():
|
|
138
|
+
return {}
|
|
139
|
+
try:
|
|
140
|
+
data = tomllib.loads(pp.read_text())
|
|
141
|
+
except Exception:
|
|
142
|
+
return {}
|
|
143
|
+
project = data.get("project", {}) if isinstance(data, dict) else {}
|
|
144
|
+
meta: Dict[str, Any] = {}
|
|
145
|
+
for key, out in (
|
|
146
|
+
("name", "name"),
|
|
147
|
+
("version", "version"),
|
|
148
|
+
("dependencies", "dependencies"),
|
|
149
|
+
("requires-python", "requires_python"),
|
|
150
|
+
):
|
|
151
|
+
if key in project:
|
|
152
|
+
meta[out] = project[key]
|
|
153
|
+
return meta
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_source_tarball(env_path: Path) -> bytes:
|
|
157
|
+
"""A deterministic .tar.gz of the SOURCE files (the same set the content hash
|
|
158
|
+
covers), with cleared mtime/owner so identical source yields a stable bundle."""
|
|
159
|
+
env_path = Path(env_path)
|
|
160
|
+
files = sorted(
|
|
161
|
+
collect_archive_files(env_path),
|
|
162
|
+
key=lambda p: str(Path(p).relative_to(env_path)).replace("\\", "/"),
|
|
163
|
+
)
|
|
164
|
+
buf = io.BytesIO()
|
|
165
|
+
# Wrap the tar in a gzip stream with mtime=0 (tarfile's own "w:gz" stamps the
|
|
166
|
+
# current time into the gzip header) + clear per-entry mtime/owner, so
|
|
167
|
+
# identical source yields identical bytes.
|
|
168
|
+
gz = gzip.GzipFile(fileobj=buf, mode="wb", mtime=0, compresslevel=9)
|
|
169
|
+
try:
|
|
170
|
+
with tarfile.open(fileobj=gz, mode="w:") as tar:
|
|
171
|
+
for f in files:
|
|
172
|
+
rel = str(Path(f).relative_to(env_path)).replace("\\", "/")
|
|
173
|
+
payload = Path(f).read_bytes()
|
|
174
|
+
info = tarfile.TarInfo(name=rel)
|
|
175
|
+
info.size = len(payload)
|
|
176
|
+
info.mtime = 0
|
|
177
|
+
info.uid = info.gid = 0
|
|
178
|
+
info.uname = info.gname = ""
|
|
179
|
+
info.mode = 0o644
|
|
180
|
+
tar.addfile(info, io.BytesIO(payload))
|
|
181
|
+
finally:
|
|
182
|
+
gz.close()
|
|
183
|
+
return buf.getvalue()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_file_manifest(env_path: Path) -> List[Dict[str, Any]]:
|
|
187
|
+
"""The FULL file tree (configs, data, .github — not just source), decoupled
|
|
188
|
+
from the source-only content hash, to power the GitHub-style browser."""
|
|
189
|
+
env_path = Path(env_path)
|
|
190
|
+
entries: List[Dict[str, Any]] = []
|
|
191
|
+
for root, dirs, filenames in os.walk(env_path):
|
|
192
|
+
dirs[:] = sorted(d for d in dirs if d not in _SKIP_DIRS and not d.endswith(".egg-info"))
|
|
193
|
+
for fn in sorted(filenames):
|
|
194
|
+
p = Path(root) / fn
|
|
195
|
+
try:
|
|
196
|
+
payload = p.read_bytes()
|
|
197
|
+
except OSError:
|
|
198
|
+
continue
|
|
199
|
+
entries.append(
|
|
200
|
+
{
|
|
201
|
+
"path": str(p.relative_to(env_path)).replace("\\", "/"),
|
|
202
|
+
"isDirectory": False,
|
|
203
|
+
"size": len(payload),
|
|
204
|
+
"sha256": hashlib.sha256(payload).hexdigest(),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
return entries
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def sha256_base64(data: bytes) -> str:
|
|
211
|
+
"""Base64 of the raw sha256 digest — the format S3/MinIO want for
|
|
212
|
+
x-amz-checksum-sha256 (NOT hex)."""
|
|
213
|
+
return base64.b64encode(hashlib.sha256(data).digest()).decode("ascii")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def normalize_package_name(name: str) -> str:
|
|
217
|
+
"""PyPI-normalized module/dist name (the pip target uses underscores)."""
|
|
218
|
+
return name.replace("-", "_").lower()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def build_wheel(env_path: Path) -> Path:
|
|
222
|
+
"""Build a wheel from the env dir via ``uv build --wheel`` (fallback
|
|
223
|
+
``python -m build --wheel``). Cleans dist/ first; returns the wheel path."""
|
|
224
|
+
env_path = Path(env_path)
|
|
225
|
+
dist = env_path / "dist"
|
|
226
|
+
if dist.exists():
|
|
227
|
+
shutil.rmtree(dist)
|
|
228
|
+
try:
|
|
229
|
+
if shutil.which("uv"):
|
|
230
|
+
subprocess.run(
|
|
231
|
+
["uv", "build", "--wheel", "--out-dir", "dist"],
|
|
232
|
+
cwd=env_path,
|
|
233
|
+
check=True,
|
|
234
|
+
capture_output=True,
|
|
235
|
+
text=True,
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
subprocess.run(
|
|
239
|
+
[sys.executable, "-m", "build", "--wheel", str(env_path)],
|
|
240
|
+
check=True,
|
|
241
|
+
capture_output=True,
|
|
242
|
+
text=True,
|
|
243
|
+
)
|
|
244
|
+
except subprocess.CalledProcessError as e:
|
|
245
|
+
raise BenchmarkPushError(f"wheel build failed: {(e.stderr or e.stdout or str(e))[:500]}") from None
|
|
246
|
+
wheels = sorted(dist.glob("*.whl"))
|
|
247
|
+
if not wheels:
|
|
248
|
+
raise BenchmarkPushError("wheel build produced no .whl")
|
|
249
|
+
return wheels[-1]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def parse_wheel_requires_dist(wheel_path: Path) -> List[str]:
|
|
253
|
+
"""The ``Requires-Dist`` lines from a wheel's ``*.dist-info/METADATA``."""
|
|
254
|
+
with zipfile.ZipFile(wheel_path) as zf:
|
|
255
|
+
metas = [n for n in zf.namelist() if n.endswith(".dist-info/METADATA")]
|
|
256
|
+
if not metas:
|
|
257
|
+
return []
|
|
258
|
+
text = zf.read(metas[0]).decode("utf-8", "replace")
|
|
259
|
+
return Parser().parsestr(text).get_all("Requires-Dist") or []
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class GatewayBenchmarkClient:
|
|
263
|
+
"""Talks to a Gateway host's benchmark-container public API. Writes require a
|
|
264
|
+
secret key, so authenticate with the project's public + secret key (Basic)."""
|
|
265
|
+
|
|
266
|
+
def __init__(self, host: str, public_key: str, secret_key: str) -> None:
|
|
267
|
+
if not host:
|
|
268
|
+
raise ValueError("host is required (e.g. https://withgateway.ai)")
|
|
269
|
+
if not public_key or not secret_key:
|
|
270
|
+
raise ValueError("both public_key and secret_key are required")
|
|
271
|
+
self.host = host.rstrip("/")
|
|
272
|
+
self._auth = base64.b64encode(f"{public_key}:{secret_key}".encode("utf-8")).decode("ascii")
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def from_env(cls, host: Optional[str] = None) -> "GatewayBenchmarkClient":
|
|
276
|
+
return cls(
|
|
277
|
+
host=host or os.environ.get("GATEWAY_HOST", ""),
|
|
278
|
+
public_key=os.environ.get("GATEWAY_PUBLIC_KEY", ""),
|
|
279
|
+
secret_key=os.environ.get("GATEWAY_SECRET_KEY", ""),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
283
|
+
req = urllib.request.Request(
|
|
284
|
+
f"{self.host}{path}",
|
|
285
|
+
data=json.dumps(body).encode("utf-8"),
|
|
286
|
+
method="POST",
|
|
287
|
+
headers={
|
|
288
|
+
"Authorization": f"Basic {self._auth}",
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
try:
|
|
293
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
294
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
295
|
+
except urllib.error.HTTPError as e:
|
|
296
|
+
detail = e.read().decode("utf-8", "replace")[:500]
|
|
297
|
+
raise BenchmarkPushError(f"POST {path} -> {e.code}: {detail}") from None
|
|
298
|
+
|
|
299
|
+
def _get(self, path: str) -> Dict[str, Any]:
|
|
300
|
+
req = urllib.request.Request(
|
|
301
|
+
f"{self.host}{path}",
|
|
302
|
+
method="GET",
|
|
303
|
+
headers={"Authorization": f"Basic {self._auth}"},
|
|
304
|
+
)
|
|
305
|
+
try:
|
|
306
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
307
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
308
|
+
except urllib.error.HTTPError as e:
|
|
309
|
+
detail = e.read().decode("utf-8", "replace")[:500]
|
|
310
|
+
raise BenchmarkPushError(f"GET {path} -> {e.code}: {detail}") from None
|
|
311
|
+
|
|
312
|
+
def _put_object(
|
|
313
|
+
self,
|
|
314
|
+
url: str,
|
|
315
|
+
data: bytes,
|
|
316
|
+
sha256_b64: str,
|
|
317
|
+
content_type: str = "application/gzip",
|
|
318
|
+
) -> None:
|
|
319
|
+
req = urllib.request.Request(
|
|
320
|
+
url,
|
|
321
|
+
data=data,
|
|
322
|
+
method="PUT",
|
|
323
|
+
headers={
|
|
324
|
+
"Content-Type": content_type,
|
|
325
|
+
"x-amz-checksum-sha256": sha256_b64,
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
try:
|
|
329
|
+
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
330
|
+
resp.read()
|
|
331
|
+
except urllib.error.HTTPError as e:
|
|
332
|
+
detail = e.read().decode("utf-8", "replace")[:500]
|
|
333
|
+
raise BenchmarkPushError(f"object PUT -> {e.code}: {detail}") from None
|
|
334
|
+
|
|
335
|
+
def push(
|
|
336
|
+
self,
|
|
337
|
+
env_path: str | Path,
|
|
338
|
+
*,
|
|
339
|
+
slug: Optional[str] = None,
|
|
340
|
+
name: Optional[str] = None,
|
|
341
|
+
description: Optional[str] = None,
|
|
342
|
+
tags: Optional[List[str]] = None,
|
|
343
|
+
visibility: Optional[str] = None,
|
|
344
|
+
auto_bump: bool = False,
|
|
345
|
+
rc: bool = False,
|
|
346
|
+
post: bool = False,
|
|
347
|
+
image_ref: Optional[str] = None,
|
|
348
|
+
wheel: bool = False,
|
|
349
|
+
branch: Optional[str] = None,
|
|
350
|
+
force: bool = False,
|
|
351
|
+
message: Optional[str] = None,
|
|
352
|
+
author: Optional[str] = None,
|
|
353
|
+
) -> Dict[str, Any]:
|
|
354
|
+
env_path = Path(env_path)
|
|
355
|
+
if not env_path.is_dir():
|
|
356
|
+
raise BenchmarkPushError(f"Not a directory: {env_path}")
|
|
357
|
+
|
|
358
|
+
# Working-copy state: the base version this dir was pulled from and the
|
|
359
|
+
# branch it tracks. The server compare-and-sets the branch tip against
|
|
360
|
+
# the base at finalize — a stale base gets a 409 ("branch has moved"),
|
|
361
|
+
# never a silent overwrite. An ad-hoc dir (no metadata) declares
|
|
362
|
+
# base=None; `force` skips the check entirely.
|
|
363
|
+
local_meta = read_local_meta(env_path) or {}
|
|
364
|
+
push_branch = branch or local_meta.get("branch") or DEFAULT_BRANCH
|
|
365
|
+
base_version_id = local_meta.get("versionId")
|
|
366
|
+
|
|
367
|
+
# Optional semver bump (Prime `env push` parity): rewrite pyproject.toml's
|
|
368
|
+
# version BEFORE hashing/tarring so the bump lands in both the bundle and
|
|
369
|
+
# the semanticVersion sent to the hub. Mutates the working tree, like Prime.
|
|
370
|
+
if sum(bool(x) for x in (auto_bump, rc, post)) > 1:
|
|
371
|
+
raise BenchmarkPushError("--auto-bump, --rc and --post are mutually exclusive")
|
|
372
|
+
if auto_bump or rc or post:
|
|
373
|
+
current = read_pyproject(env_path).get("version")
|
|
374
|
+
if not current:
|
|
375
|
+
raise BenchmarkPushError("no [project].version in pyproject.toml to bump")
|
|
376
|
+
new_version = (
|
|
377
|
+
bump_version(current) if auto_bump else bump_rc_version(current) if rc else bump_post_version(current)
|
|
378
|
+
)
|
|
379
|
+
update_pyproject_version(env_path / "pyproject.toml", new_version)
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
if bundle_mode(env_path) is not None:
|
|
383
|
+
validate_schema_materialization(env_path)
|
|
384
|
+
except ContractError as error:
|
|
385
|
+
raise BenchmarkPushError(f"Schema world check failed before push: {error}") from None
|
|
386
|
+
|
|
387
|
+
content_hash = compute_content_hash(env_path)
|
|
388
|
+
pyproject = read_pyproject(env_path)
|
|
389
|
+
slug = slug or pyproject.get("name") or env_path.name
|
|
390
|
+
|
|
391
|
+
# Optional wheel build (pip-install-by-name). Build BEFORE the versions POST
|
|
392
|
+
# so its object sha256 + Requires-Dist travel with the version request and we
|
|
393
|
+
# get a second presigned PUT back.
|
|
394
|
+
wheel_bytes: Optional[bytes] = None
|
|
395
|
+
wheel_sha256: Optional[str] = None
|
|
396
|
+
wheel_filename: Optional[str] = None
|
|
397
|
+
requires_dist: List[str] = []
|
|
398
|
+
if wheel:
|
|
399
|
+
wheel_path = build_wheel(env_path)
|
|
400
|
+
wheel_bytes = wheel_path.read_bytes()
|
|
401
|
+
wheel_sha256 = sha256_base64(wheel_bytes)
|
|
402
|
+
wheel_filename = wheel_path.name
|
|
403
|
+
requires_dist = parse_wheel_requires_dist(wheel_path)
|
|
404
|
+
|
|
405
|
+
resolve = self._post(
|
|
406
|
+
"/api/public/benchmark-containers/resolve",
|
|
407
|
+
{
|
|
408
|
+
"slug": slug,
|
|
409
|
+
"name": name or pyproject.get("name") or slug,
|
|
410
|
+
**({"description": description} if description else {}),
|
|
411
|
+
**({"tags": tags} if tags else {}),
|
|
412
|
+
**({"visibility": visibility} if visibility else {}),
|
|
413
|
+
},
|
|
414
|
+
)
|
|
415
|
+
container_id = resolve["containerId"]
|
|
416
|
+
|
|
417
|
+
tarball = build_source_tarball(env_path)
|
|
418
|
+
bundle_sha256 = sha256_base64(tarball)
|
|
419
|
+
|
|
420
|
+
version = self._post(
|
|
421
|
+
"/api/public/benchmark-containers/versions",
|
|
422
|
+
{
|
|
423
|
+
"containerId": container_id,
|
|
424
|
+
"contentHash": content_hash,
|
|
425
|
+
"bundleSha256": bundle_sha256,
|
|
426
|
+
"bundleSizeBytes": len(tarball),
|
|
427
|
+
**({"semanticVersion": pyproject["version"]} if pyproject.get("version") else {}),
|
|
428
|
+
"pyprojectMeta": pyproject,
|
|
429
|
+
"fileManifest": build_file_manifest(env_path),
|
|
430
|
+
"branch": push_branch,
|
|
431
|
+
"baseVersionId": base_version_id,
|
|
432
|
+
**({"force": True} if force else {}),
|
|
433
|
+
**({"changeReason": message} if message else {}),
|
|
434
|
+
**({"author": author} if author else {}),
|
|
435
|
+
**({"imageRef": image_ref} if image_ref else {}),
|
|
436
|
+
**(
|
|
437
|
+
{
|
|
438
|
+
"wheelSha256": wheel_sha256,
|
|
439
|
+
"wheelSizeBytes": len(wheel_bytes),
|
|
440
|
+
"wheelFilename": wheel_filename,
|
|
441
|
+
"requiresDist": requires_dist,
|
|
442
|
+
}
|
|
443
|
+
if wheel_bytes is not None and wheel_sha256
|
|
444
|
+
else {}
|
|
445
|
+
),
|
|
446
|
+
},
|
|
447
|
+
)
|
|
448
|
+
version_id = version["versionId"]
|
|
449
|
+
|
|
450
|
+
if version.get("uploadUrl"):
|
|
451
|
+
self._put_object(version["uploadUrl"], tarball, bundle_sha256)
|
|
452
|
+
if version.get("wheelUploadUrl") and wheel_bytes is not None and wheel_sha256:
|
|
453
|
+
self._put_object(
|
|
454
|
+
version["wheelUploadUrl"],
|
|
455
|
+
wheel_bytes,
|
|
456
|
+
wheel_sha256,
|
|
457
|
+
content_type="application/octet-stream",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
final = self._post(
|
|
461
|
+
f"/api/public/benchmark-containers/versions/{version_id}/finalize",
|
|
462
|
+
{
|
|
463
|
+
"branch": push_branch,
|
|
464
|
+
"baseVersionId": base_version_id,
|
|
465
|
+
**({"force": True} if force else {}),
|
|
466
|
+
},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# The push is the dir's new base — rewrite the metadata so the NEXT
|
|
470
|
+
# push fast-forwards instead of conflicting with your own work.
|
|
471
|
+
write_local_meta(
|
|
472
|
+
env_path,
|
|
473
|
+
{
|
|
474
|
+
"slug": slug,
|
|
475
|
+
"ref": push_branch,
|
|
476
|
+
"branch": push_branch,
|
|
477
|
+
"versionId": version_id,
|
|
478
|
+
"contentHash": content_hash,
|
|
479
|
+
"semanticVersion": pyproject.get("version"),
|
|
480
|
+
"forkedFromVersionId": base_version_id,
|
|
481
|
+
"baseFiles": snapshot_tree_hashes(env_path),
|
|
482
|
+
},
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
"containerId": container_id,
|
|
487
|
+
"versionId": version_id,
|
|
488
|
+
"slug": slug,
|
|
489
|
+
"contentHash": content_hash,
|
|
490
|
+
"state": final.get("state"),
|
|
491
|
+
"branch": final.get("branch", push_branch),
|
|
492
|
+
"alreadyUploaded": bool(version.get("alreadyUploaded")),
|
|
493
|
+
"wheelBuilt": wheel_bytes is not None,
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
def dispatch_run_config(
|
|
497
|
+
self,
|
|
498
|
+
run_config_id: str,
|
|
499
|
+
*,
|
|
500
|
+
container_id: Optional[str] = None,
|
|
501
|
+
version_id: Optional[str] = None,
|
|
502
|
+
) -> Dict[str, Any]:
|
|
503
|
+
"""Start a hosted run of a run config — the same dispatch the Run
|
|
504
|
+
button uses, parallelism included (the config's `parallelism` fans the
|
|
505
|
+
task list across that many containers per model). Returns
|
|
506
|
+
{versionLabel, runs: [{model, runId, ok, error?}]}."""
|
|
507
|
+
return self._post(
|
|
508
|
+
"/api/public/benchmark-containers/run-configs/dispatch",
|
|
509
|
+
{
|
|
510
|
+
"runConfigId": run_config_id,
|
|
511
|
+
**({"containerId": container_id} if container_id else {}),
|
|
512
|
+
**({"versionId": version_id} if version_id else {}),
|
|
513
|
+
},
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def _json_request(self, method: str, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
517
|
+
req = urllib.request.Request(
|
|
518
|
+
f"{self.host}{path}",
|
|
519
|
+
data=json.dumps(body).encode("utf-8"),
|
|
520
|
+
method=method,
|
|
521
|
+
headers={
|
|
522
|
+
"Authorization": f"Basic {self._auth}",
|
|
523
|
+
"Content-Type": "application/json",
|
|
524
|
+
},
|
|
525
|
+
)
|
|
526
|
+
try:
|
|
527
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
528
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
529
|
+
except urllib.error.HTTPError as e:
|
|
530
|
+
detail = e.read().decode("utf-8", "replace")[:500]
|
|
531
|
+
raise BenchmarkPushError(f"{method} {path} -> {e.code}: {detail}") from None
|
|
532
|
+
|
|
533
|
+
def list_run_configs(self) -> Dict[str, Any]:
|
|
534
|
+
"""Run configs in the project — id, name, models, parallelism. The id
|
|
535
|
+
feeds dispatch_run_config; the name is how CI resolves it."""
|
|
536
|
+
return self._get("/api/public/benchmark-containers/run-configs")
|
|
537
|
+
|
|
538
|
+
def upsert_run_config(self, **fields: Any) -> Dict[str, Any]:
|
|
539
|
+
"""Create a run config (or update, when ``id`` is given). Fields match
|
|
540
|
+
the Configs form: name, models, parallelism, numExamples,
|
|
541
|
+
rolloutsPerExample, timeoutMinutes, agent, taskSetDatasetId,
|
|
542
|
+
taskNames, worldTasks, autoRunOnPush. A config runs EITHER the bundle's
|
|
543
|
+
own scenarios (``taskNames`` / ``taskSetDatasetId``) OR the project's
|
|
544
|
+
stored tasks (``worldTasks``: ``[{"id": ..., "version"?: ...}]``, api
|
|
545
|
+
worlds only) — never both."""
|
|
546
|
+
return self._json_request("POST", "/api/public/benchmark-containers/run-configs", fields)
|
|
547
|
+
|
|
548
|
+
def delete_run_config(self, config_id: str) -> Dict[str, Any]:
|
|
549
|
+
return self._json_request(
|
|
550
|
+
"DELETE",
|
|
551
|
+
"/api/public/benchmark-containers/run-configs",
|
|
552
|
+
{"id": config_id},
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
def set_env_secret(self, slug: str, key: str, value: str) -> Dict[str, Any]:
|
|
556
|
+
"""Set (or rotate) one environment secret — an env var every hosted
|
|
557
|
+
run of the world receives. Write-only: no API returns the value."""
|
|
558
|
+
return self._json_request(
|
|
559
|
+
"PUT",
|
|
560
|
+
"/api/public/benchmark-containers/secrets",
|
|
561
|
+
{"slug": slug, "key": key, "value": value},
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
def list_env_secret_keys(self, slug: str) -> Dict[str, Any]:
|
|
565
|
+
"""Keys and rotation timestamps only — never values."""
|
|
566
|
+
return self._get(f"/api/public/benchmark-containers/secrets?slug={urllib.parse.quote(slug)}")
|
|
567
|
+
|
|
568
|
+
def delete_env_secret(self, slug: str, key: str) -> Dict[str, Any]:
|
|
569
|
+
return self._json_request(
|
|
570
|
+
"DELETE",
|
|
571
|
+
"/api/public/benchmark-containers/secrets",
|
|
572
|
+
{"slug": slug, "key": key},
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
def resolve_version(self, slug: str, ref: str = "latest") -> Dict[str, Any]:
|
|
576
|
+
"""Resolve a container by slug + ref ('latest' | semver | contentHash
|
|
577
|
+
prefix) to a concrete READY version + a signed source-bundle URL."""
|
|
578
|
+
return self._post(
|
|
579
|
+
"/api/public/benchmark-containers/resolve-version",
|
|
580
|
+
{"slug": slug, **({"ref": ref} if ref else {})},
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def pull(self, slug_ref: str, target: Optional[str] = None) -> Dict[str, Any]:
|
|
584
|
+
"""Reconstruct a working dir from a version, addressed by name.
|
|
585
|
+
|
|
586
|
+
`slug_ref` is ``slug`` or ``slug@latest|@<semver>|@<hash-prefix>``. When
|
|
587
|
+
`target` is omitted the dir is `./<slug>`, de-duplicated with a ``-1``/``-2``
|
|
588
|
+
suffix (Prime `env pull` behavior). Writes `.gateway/.env-metadata.json`
|
|
589
|
+
(a dotdir, excluded from the content hash) so pull→edit→re-push is stable.
|
|
590
|
+
"""
|
|
591
|
+
if "@" in slug_ref:
|
|
592
|
+
slug, ref = slug_ref.rsplit("@", 1)
|
|
593
|
+
else:
|
|
594
|
+
slug, ref = slug_ref, "latest"
|
|
595
|
+
|
|
596
|
+
details = self.resolve_version(slug, ref)
|
|
597
|
+
url = details.get("downloadUrl")
|
|
598
|
+
if not url:
|
|
599
|
+
raise BenchmarkPushError(f"{slug}@{ref} resolved but has no uploaded bundle")
|
|
600
|
+
|
|
601
|
+
base = Path(target) if target else Path.cwd() / slug
|
|
602
|
+
dst = base
|
|
603
|
+
if target is None and dst.exists():
|
|
604
|
+
i = 1
|
|
605
|
+
while dst.exists():
|
|
606
|
+
dst = base.parent / f"{base.name}-{i}"
|
|
607
|
+
i += 1
|
|
608
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
609
|
+
|
|
610
|
+
tmp = dst / ".bundle.tar.gz"
|
|
611
|
+
try: # signed URL — no auth header
|
|
612
|
+
with urllib.request.urlopen(urllib.request.Request(url), timeout=300) as r:
|
|
613
|
+
bundle_bytes = r.read()
|
|
614
|
+
except urllib.error.HTTPError as e:
|
|
615
|
+
detail = e.read().decode("utf-8", "replace")[:300]
|
|
616
|
+
raise BenchmarkPushError(f"bundle download -> {e.code}: {detail}") from None
|
|
617
|
+
|
|
618
|
+
# Verify the downloaded bytes against the version's recorded object
|
|
619
|
+
# sha256, so a divergent bundle (overwritten object, corrupted transfer)
|
|
620
|
+
# fails loudly instead of silently extracting different content under a
|
|
621
|
+
# pinned "immutable" version. Older servers omit the field -> skip.
|
|
622
|
+
expected_sha = details.get("bundleSha256")
|
|
623
|
+
if expected_sha:
|
|
624
|
+
actual_sha = base64.b64encode(hashlib.sha256(bundle_bytes).digest()).decode()
|
|
625
|
+
if actual_sha != expected_sha:
|
|
626
|
+
raise BenchmarkPushError(
|
|
627
|
+
f"bundle integrity check failed for {slug}@{ref}: downloaded "
|
|
628
|
+
f"object sha256 ({actual_sha}) != recorded bundleSha256 "
|
|
629
|
+
f"({expected_sha})"
|
|
630
|
+
)
|
|
631
|
+
tmp.write_bytes(bundle_bytes)
|
|
632
|
+
|
|
633
|
+
with tarfile.open(tmp, "r:gz") as tar:
|
|
634
|
+
safe_tar_extract(tar, dst)
|
|
635
|
+
tmp.unlink(missing_ok=True)
|
|
636
|
+
|
|
637
|
+
# Track the branch that answered the ref; a semver/hash pin is a
|
|
638
|
+
# detached checkout, which pushes onto main unless told otherwise.
|
|
639
|
+
branch = details.get("branch") or (DEFAULT_BRANCH if ref == "latest" else None)
|
|
640
|
+
write_local_meta(
|
|
641
|
+
dst,
|
|
642
|
+
{
|
|
643
|
+
"slug": slug,
|
|
644
|
+
"ref": ref,
|
|
645
|
+
"branch": branch or DEFAULT_BRANCH,
|
|
646
|
+
"versionId": details.get("versionId"),
|
|
647
|
+
"contentHash": details.get("contentHash"),
|
|
648
|
+
"semanticVersion": details.get("semanticVersion"),
|
|
649
|
+
"forkedFromVersionId": details.get("forkedFromVersionId"),
|
|
650
|
+
"baseFiles": snapshot_tree_hashes(dst),
|
|
651
|
+
},
|
|
652
|
+
)
|
|
653
|
+
return {
|
|
654
|
+
"slug": slug,
|
|
655
|
+
"ref": ref,
|
|
656
|
+
"branch": branch or DEFAULT_BRANCH,
|
|
657
|
+
"versionId": details.get("versionId"),
|
|
658
|
+
"contentHash": details.get("contentHash"),
|
|
659
|
+
"target": str(dst),
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
# ── Refs (branches + tags) ───────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
def list_refs(self, slug: str) -> Dict[str, Any]:
|
|
665
|
+
"""Every branch and tag of a container with tip version info."""
|
|
666
|
+
qs = urlencode({"slug": slug})
|
|
667
|
+
return self._get(f"/api/public/benchmark-containers/refs?{qs}")
|
|
668
|
+
|
|
669
|
+
def create_ref(
|
|
670
|
+
self,
|
|
671
|
+
slug: str,
|
|
672
|
+
name: str,
|
|
673
|
+
*,
|
|
674
|
+
kind: str = "branch",
|
|
675
|
+
version_id: Optional[str] = None,
|
|
676
|
+
) -> Dict[str, Any]:
|
|
677
|
+
"""Create a branch (movable, starts at the main tip unless pinned) or
|
|
678
|
+
an immutable tag (points at a concrete version forever)."""
|
|
679
|
+
return self._post(
|
|
680
|
+
"/api/public/benchmark-containers/refs",
|
|
681
|
+
{
|
|
682
|
+
"slug": slug,
|
|
683
|
+
"name": name,
|
|
684
|
+
"kind": kind,
|
|
685
|
+
**({"versionId": version_id} if version_id else {}),
|
|
686
|
+
},
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def log(self, slug: str, *, branch: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
|
|
690
|
+
"""A branch's version lineage, tip first (`bench log`)."""
|
|
691
|
+
qs = urlencode({"slug": slug, "limit": str(limit), **({"branch": branch} if branch else {})})
|
|
692
|
+
return self._get(f"/api/public/benchmark-containers/log?{qs}")
|
|
693
|
+
|
|
694
|
+
def status(self, env_path: str | Path) -> Dict[str, Any]:
|
|
695
|
+
"""The working dir's position: branch, base, dirtiness, and whether
|
|
696
|
+
the remote branch tip has moved past the base (`bench status`)."""
|
|
697
|
+
env_path = Path(env_path)
|
|
698
|
+
meta = read_local_meta(env_path)
|
|
699
|
+
if not meta or not meta.get("slug"):
|
|
700
|
+
raise BenchmarkPushError(
|
|
701
|
+
f"{env_path} has no .gateway/.env-metadata.json — pull or push it "
|
|
702
|
+
"first so the dir knows which container/branch it tracks."
|
|
703
|
+
)
|
|
704
|
+
slug = meta["slug"]
|
|
705
|
+
branch = meta.get("branch") or DEFAULT_BRANCH
|
|
706
|
+
local_hash = compute_content_hash(env_path)
|
|
707
|
+
dirty = local_hash != meta.get("contentHash")
|
|
708
|
+
|
|
709
|
+
refs = self.list_refs(slug).get("refs", [])
|
|
710
|
+
tip = next((r for r in refs if r.get("name") == branch), None)
|
|
711
|
+
behind = bool(tip and tip.get("versionId") != meta.get("versionId"))
|
|
712
|
+
return {
|
|
713
|
+
"slug": slug,
|
|
714
|
+
"branch": branch,
|
|
715
|
+
"baseVersionId": meta.get("versionId"),
|
|
716
|
+
"baseContentHash": meta.get("contentHash"),
|
|
717
|
+
"localContentHash": local_hash,
|
|
718
|
+
"dirty": dirty,
|
|
719
|
+
"behind": behind,
|
|
720
|
+
"remoteTipVersionId": tip.get("versionId") if tip else None,
|
|
721
|
+
"remoteTipContentHash": tip.get("contentHash") if tip else None,
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
def propose(
|
|
725
|
+
self,
|
|
726
|
+
env_path: str | Path,
|
|
727
|
+
*,
|
|
728
|
+
title: str,
|
|
729
|
+
message: str,
|
|
730
|
+
target_branch: Optional[str] = None,
|
|
731
|
+
) -> Dict[str, Any]:
|
|
732
|
+
"""File the working dir's local edits as a change proposal ("PR to the
|
|
733
|
+
env") instead of pushing straight to the branch. The changeset is the
|
|
734
|
+
diff between the dir and its recorded base version's file manifest;
|
|
735
|
+
review + apply live on the environment homepage's Changes tab.
|
|
736
|
+
|
|
737
|
+
A dir sitting on a pushed BRANCH proposing into a different target is
|
|
738
|
+
a branch-backed PR (GitHub semantics): the diff is computed against
|
|
739
|
+
the TARGET branch's tip — diffing against the dir's own base right
|
|
740
|
+
after a push is empty by construction — and the branch is recorded
|
|
741
|
+
so runs of the proposal dispatch the branch tip directly."""
|
|
742
|
+
env_path = Path(env_path)
|
|
743
|
+
meta = read_local_meta(env_path)
|
|
744
|
+
if not meta or not meta.get("slug") or not meta.get("versionId"):
|
|
745
|
+
raise BenchmarkPushError(
|
|
746
|
+
f"{env_path} has no recorded base (.gateway/.env-metadata.json) — "
|
|
747
|
+
"a proposal is a diff against a base version, pull first."
|
|
748
|
+
)
|
|
749
|
+
slug = meta["slug"]
|
|
750
|
+
base_version_id = meta["versionId"]
|
|
751
|
+
recorded_branch = meta.get("branch") or meta.get("ref")
|
|
752
|
+
target = target_branch or DEFAULT_BRANCH
|
|
753
|
+
|
|
754
|
+
def _manifest_hashes(version_id: str) -> Dict[str, Any]:
|
|
755
|
+
# Server manifest as a base-file record. Dot-paths are dropped —
|
|
756
|
+
# the manifest deliberately lists files the source tarball never
|
|
757
|
+
# contained, which would read as phantom deletes here.
|
|
758
|
+
listed = self._get(f"/api/public/benchmark-containers/versions/{version_id}/files").get("files", [])
|
|
759
|
+
return {
|
|
760
|
+
f["path"]: f.get("sha256")
|
|
761
|
+
for f in listed
|
|
762
|
+
if not f.get("isDirectory") and not any(seg.startswith(".") for seg in f["path"].split("/"))
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
source_branch: Optional[str] = None
|
|
766
|
+
if recorded_branch and recorded_branch != target:
|
|
767
|
+
source_branch = str(recorded_branch)
|
|
768
|
+
try:
|
|
769
|
+
tip = self.resolve_version(slug, target)
|
|
770
|
+
except Exception as exc:
|
|
771
|
+
raise BenchmarkPushError(
|
|
772
|
+
f"could not resolve target branch '{target}' to propose " f"against: {exc}"
|
|
773
|
+
) from None
|
|
774
|
+
base_version_id = tip["versionId"]
|
|
775
|
+
base_by_path: Dict[str, Any] = _manifest_hashes(base_version_id)
|
|
776
|
+
else:
|
|
777
|
+
# Same-branch proposal: the tarball tree snapshotted at pull/push
|
|
778
|
+
# time; the manifest is only a pre-baseFiles fallback.
|
|
779
|
+
base_by_path = meta.get("baseFiles") or {}
|
|
780
|
+
if not base_by_path:
|
|
781
|
+
base_by_path = _manifest_hashes(base_version_id)
|
|
782
|
+
|
|
783
|
+
changes: List[Dict[str, Any]] = []
|
|
784
|
+
local_paths = set()
|
|
785
|
+
for file_path in collect_archive_files(env_path):
|
|
786
|
+
rel = str(file_path.relative_to(env_path)).replace("\\", "/")
|
|
787
|
+
local_paths.add(rel)
|
|
788
|
+
data = file_path.read_bytes()
|
|
789
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
790
|
+
if base_by_path.get(rel) == digest:
|
|
791
|
+
continue
|
|
792
|
+
try:
|
|
793
|
+
text = data.decode("utf-8")
|
|
794
|
+
except UnicodeDecodeError:
|
|
795
|
+
raise BenchmarkPushError(
|
|
796
|
+
f"{rel} is not utf-8 text — proposals carry text changesets; "
|
|
797
|
+
"push binary changes directly to a branch instead."
|
|
798
|
+
) from None
|
|
799
|
+
changes.append({"path": rel, "action": "write", "newContent": text})
|
|
800
|
+
for rel in sorted(set(base_by_path) - local_paths):
|
|
801
|
+
changes.append({"path": rel, "action": "delete"})
|
|
802
|
+
|
|
803
|
+
if not changes:
|
|
804
|
+
raise BenchmarkPushError(
|
|
805
|
+
f"No changes against {target} — nothing to propose."
|
|
806
|
+
+ (f" (branch {source_branch} matches the target's tip)" if source_branch else "")
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
result = self._post(
|
|
810
|
+
"/api/public/benchmark-containers/proposals",
|
|
811
|
+
{
|
|
812
|
+
"slug": slug,
|
|
813
|
+
"title": title,
|
|
814
|
+
"changeReason": message,
|
|
815
|
+
"baseVersionId": base_version_id,
|
|
816
|
+
**({"targetBranch": target_branch} if target_branch else {}),
|
|
817
|
+
**({"sourceBranch": source_branch} if source_branch else {}),
|
|
818
|
+
"changes": changes,
|
|
819
|
+
},
|
|
820
|
+
)
|
|
821
|
+
return result
|
|
822
|
+
|
|
823
|
+
def fork(self, slug: str, target_slug: Optional[str] = None) -> Dict[str, Any]:
|
|
824
|
+
"""Fork a container: create a new container whose head version copies the
|
|
825
|
+
source's latest READY version (shared bundle, recorded provenance). Forks
|
|
826
|
+
are container-level — pass a bare slug, not ``slug@version``."""
|
|
827
|
+
if "@" in slug:
|
|
828
|
+
raise BenchmarkPushError("fork a container head; omit the @version suffix")
|
|
829
|
+
return self._post(
|
|
830
|
+
"/api/public/benchmark-containers/fork",
|
|
831
|
+
{
|
|
832
|
+
"slug": slug,
|
|
833
|
+
**({"targetSlug": target_slug} if target_slug else {}),
|
|
834
|
+
},
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
def diff(self, base: str, head: str) -> Dict[str, Any]:
|
|
838
|
+
"""Per-file diff (added/removed/modified/unchanged) between two version
|
|
839
|
+
ids, keyed on the stored per-file sha256."""
|
|
840
|
+
from urllib.parse import urlencode
|
|
841
|
+
|
|
842
|
+
qs = urlencode({"base": base, "head": head})
|
|
843
|
+
return self._get(f"/api/public/benchmark-containers/versions/diff?{qs}")
|
|
844
|
+
|
|
845
|
+
def install(self, slug_ref: str, *, tool: str = "uv", no_upgrade: bool = False) -> Dict[str, Any]:
|
|
846
|
+
"""Install a version's built wheel by name (wheel-url-direct). Requires the
|
|
847
|
+
version to have been pushed with ``wheel=True``. ``tool`` is 'uv' or 'pip';
|
|
848
|
+
runs in the current Python environment (activate a venv first for uv)."""
|
|
849
|
+
if "@" in slug_ref:
|
|
850
|
+
slug, ref = slug_ref.rsplit("@", 1)
|
|
851
|
+
else:
|
|
852
|
+
slug, ref = slug_ref, "latest"
|
|
853
|
+
details = self.resolve_version(slug, ref)
|
|
854
|
+
version_id = details.get("versionId")
|
|
855
|
+
dl = self._get(f"/api/public/benchmark-containers/versions/{version_id}" "/download?artifact=wheel")
|
|
856
|
+
wheel_url = dl.get("downloadUrl")
|
|
857
|
+
if not wheel_url:
|
|
858
|
+
raise BenchmarkPushError(
|
|
859
|
+
f"{slug}@{ref} has no built wheel — push it with " "wheel=True (`gateway bench push --build-wheel`)"
|
|
860
|
+
)
|
|
861
|
+
# Download to a temp file with the canonical `.whl` basename (the object-key
|
|
862
|
+
# basename); a presigned URL basename carries a query string that pip/uv
|
|
863
|
+
# can't parse into a wheel name.
|
|
864
|
+
import tempfile
|
|
865
|
+
from urllib.parse import unquote, urlparse
|
|
866
|
+
|
|
867
|
+
wheel_name = os.path.basename(unquote(urlparse(wheel_url).path)) or "pkg.whl"
|
|
868
|
+
tmpdir = tempfile.mkdtemp(prefix="gateway-wheel-")
|
|
869
|
+
wheel_file = os.path.join(tmpdir, wheel_name)
|
|
870
|
+
try:
|
|
871
|
+
with (
|
|
872
|
+
urllib.request.urlopen(urllib.request.Request(wheel_url), timeout=300) as r,
|
|
873
|
+
open(wheel_file, "wb") as f,
|
|
874
|
+
):
|
|
875
|
+
shutil.copyfileobj(r, f)
|
|
876
|
+
except urllib.error.HTTPError as e:
|
|
877
|
+
raise BenchmarkPushError(f"wheel download -> {e.code}") from None
|
|
878
|
+
|
|
879
|
+
if tool == "uv":
|
|
880
|
+
cmd = ["uv", "pip", "install"]
|
|
881
|
+
if not no_upgrade:
|
|
882
|
+
cmd.append("--reinstall")
|
|
883
|
+
else:
|
|
884
|
+
cmd = ["pip", "install"]
|
|
885
|
+
if not no_upgrade:
|
|
886
|
+
cmd.append("--upgrade")
|
|
887
|
+
cmd.append(wheel_file)
|
|
888
|
+
try:
|
|
889
|
+
subprocess.run(cmd, check=True)
|
|
890
|
+
except FileNotFoundError:
|
|
891
|
+
raise BenchmarkPushError(
|
|
892
|
+
f"'{cmd[0]}' is not installed or not on PATH — install it "
|
|
893
|
+
"(https://docs.astral.sh/uv/) or rerun with tool='pip' "
|
|
894
|
+
"(`gateway bench install --tool pip`)."
|
|
895
|
+
) from None
|
|
896
|
+
except subprocess.CalledProcessError as e:
|
|
897
|
+
raise BenchmarkPushError(f"install failed (exit {e.returncode})") from None
|
|
898
|
+
finally:
|
|
899
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
900
|
+
return {
|
|
901
|
+
"slug": slug,
|
|
902
|
+
"ref": ref,
|
|
903
|
+
"versionId": version_id,
|
|
904
|
+
"tool": tool,
|
|
905
|
+
"wheelFile": wheel_name,
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
910
|
+
"""`python -m gatewaysdk.benchmark_hub.client push ./env [--slug ...]`."""
|
|
911
|
+
import argparse
|
|
912
|
+
|
|
913
|
+
parser = argparse.ArgumentParser(prog="gateway bench")
|
|
914
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
915
|
+
push_p = sub.add_parser("push", help="Push a benchmark container to Gateway")
|
|
916
|
+
push_p.add_argument("path")
|
|
917
|
+
push_p.add_argument("--host", default=None)
|
|
918
|
+
push_p.add_argument("--slug", default=None)
|
|
919
|
+
push_p.add_argument("--name", default=None)
|
|
920
|
+
push_p.add_argument("--description", default=None)
|
|
921
|
+
push_p.add_argument("--tag", action="append", dest="tags", default=None)
|
|
922
|
+
push_p.add_argument("--visibility", choices=["PRIVATE", "PUBLIC"], default=None)
|
|
923
|
+
push_p.add_argument("--auto-bump", action="store_true", dest="auto_bump")
|
|
924
|
+
push_p.add_argument("--rc", action="store_true")
|
|
925
|
+
push_p.add_argument("--post", action="store_true")
|
|
926
|
+
push_p.add_argument("--image", dest="image_ref", default=None)
|
|
927
|
+
push_p.add_argument("--build-wheel", action="store_true", dest="build_wheel")
|
|
928
|
+
|
|
929
|
+
pull_p = sub.add_parser("pull", help="Reconstruct a container version's source dir by name")
|
|
930
|
+
pull_p.add_argument("slug", help="slug or slug@latest|@semver|@hash")
|
|
931
|
+
pull_p.add_argument("--host", default=None)
|
|
932
|
+
pull_p.add_argument("--target", "-t", default=None)
|
|
933
|
+
|
|
934
|
+
fork_p = sub.add_parser("fork", help="Fork a container head into a new container")
|
|
935
|
+
fork_p.add_argument("slug", help="source container slug (fork the head)")
|
|
936
|
+
fork_p.add_argument("--host", default=None)
|
|
937
|
+
fork_p.add_argument("--into", dest="target_slug", default=None)
|
|
938
|
+
|
|
939
|
+
diff_p = sub.add_parser("diff", help="Per-file diff between two version ids")
|
|
940
|
+
diff_p.add_argument("base", help="base version id")
|
|
941
|
+
diff_p.add_argument("head", help="head version id")
|
|
942
|
+
diff_p.add_argument("--host", default=None)
|
|
943
|
+
|
|
944
|
+
install_p = sub.add_parser("install", help="pip-install a version's wheel by name")
|
|
945
|
+
install_p.add_argument("slug", help="slug or slug@latest|@semver|@hash")
|
|
946
|
+
install_p.add_argument("--host", default=None)
|
|
947
|
+
install_p.add_argument("--tool", choices=["uv", "pip"], default="uv")
|
|
948
|
+
install_p.add_argument("--no-upgrade", action="store_true", dest="no_upgrade")
|
|
949
|
+
args = parser.parse_args(argv)
|
|
950
|
+
|
|
951
|
+
if args.cmd == "push":
|
|
952
|
+
client = GatewayBenchmarkClient.from_env(host=args.host)
|
|
953
|
+
result = client.push(
|
|
954
|
+
args.path,
|
|
955
|
+
slug=args.slug,
|
|
956
|
+
name=args.name,
|
|
957
|
+
description=args.description,
|
|
958
|
+
tags=args.tags,
|
|
959
|
+
visibility=args.visibility,
|
|
960
|
+
auto_bump=args.auto_bump,
|
|
961
|
+
rc=args.rc,
|
|
962
|
+
post=args.post,
|
|
963
|
+
image_ref=args.image_ref,
|
|
964
|
+
wheel=args.build_wheel,
|
|
965
|
+
)
|
|
966
|
+
print(json.dumps(result, indent=2))
|
|
967
|
+
return 0
|
|
968
|
+
if args.cmd == "pull":
|
|
969
|
+
client = GatewayBenchmarkClient.from_env(host=args.host)
|
|
970
|
+
result = client.pull(args.slug, target=args.target)
|
|
971
|
+
print(json.dumps(result, indent=2))
|
|
972
|
+
return 0
|
|
973
|
+
if args.cmd == "fork":
|
|
974
|
+
client = GatewayBenchmarkClient.from_env(host=args.host)
|
|
975
|
+
result = client.fork(args.slug, target_slug=args.target_slug)
|
|
976
|
+
print(json.dumps(result, indent=2))
|
|
977
|
+
return 0
|
|
978
|
+
if args.cmd == "diff":
|
|
979
|
+
client = GatewayBenchmarkClient.from_env(host=args.host)
|
|
980
|
+
result = client.diff(args.base, args.head)
|
|
981
|
+
print(json.dumps(result, indent=2))
|
|
982
|
+
return 0
|
|
983
|
+
if args.cmd == "install":
|
|
984
|
+
client = GatewayBenchmarkClient.from_env(host=args.host)
|
|
985
|
+
result = client.install(args.slug, tool=args.tool, no_upgrade=args.no_upgrade)
|
|
986
|
+
print(json.dumps(result, indent=2))
|
|
987
|
+
return 0
|
|
988
|
+
return 1
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
if __name__ == "__main__": # pragma: no cover
|
|
992
|
+
raise SystemExit(main())
|