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,428 @@
|
|
|
1
|
+
"""Conformance: does the served world answer the way the vendor's OpenAPI says it does?
|
|
2
|
+
|
|
3
|
+
A world is 1:1 when every declared operation, called the way the vendor's client
|
|
4
|
+
calls it, comes back with the status the vendor documents and a body the vendor's
|
|
5
|
+
response schema accepts. This runs the world in-process, drives each operation
|
|
6
|
+
with arguments taken from the world's own seeded rows (so lookups hit data), and
|
|
7
|
+
validates each answer against the OpenAPI document TRM or the vendor supplied.
|
|
8
|
+
|
|
9
|
+
gateway worlds connector conform ./torchlight-world --openapi briefs/torchlight-openapi.yaml
|
|
10
|
+
|
|
11
|
+
The validator here is deliberately lenient and standalone: vendor documents use
|
|
12
|
+
the whole of OpenAPI 3.1 (``nullable``, ``readOnly``, ``example`` siblings,
|
|
13
|
+
``discriminator``), which the world contract's strict validator refuses on
|
|
14
|
+
purpose. What is checked: types (with ``null``), ``required``, ``properties``
|
|
15
|
+
(unknown members allowed unless ``additionalProperties: false``), ``items``,
|
|
16
|
+
``enum``, ``const``, ``allOf``/``anyOf``/``oneOf`` and ``$ref`` into the document.
|
|
17
|
+
Formats are not checked — a mock that answers a UUID-shaped string where the
|
|
18
|
+
vendor answers a UUID is not the failure this tool exists to find.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import json
|
|
25
|
+
import threading
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.parse
|
|
28
|
+
import urllib.request
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from .api import WorldState, load_api_settings, serve
|
|
34
|
+
from .connector import Operation, load_spec
|
|
35
|
+
from .validation import fail
|
|
36
|
+
|
|
37
|
+
PIN = "conform"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------- the document
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_openapi(path: str | Path) -> dict:
|
|
44
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
45
|
+
if str(path).endswith((".yaml", ".yml")):
|
|
46
|
+
try:
|
|
47
|
+
import yaml # optional at runtime; the SDK depends on it, the vendored server does not
|
|
48
|
+
except ImportError: # pragma: no cover
|
|
49
|
+
fail("conform", "Reading a YAML document needs PyYAML; convert it to JSON", "/conform/openapi")
|
|
50
|
+
document = yaml.safe_load(text)
|
|
51
|
+
else:
|
|
52
|
+
document = json.loads(text)
|
|
53
|
+
if not isinstance(document, dict) or "paths" not in document:
|
|
54
|
+
fail("conform", f"{path} is not an OpenAPI document (no paths)", "/conform/openapi")
|
|
55
|
+
return document
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def server_prefixes(document: dict) -> list[str]:
|
|
59
|
+
"""The path part of each server URL, longest first, so a world whose paths carry
|
|
60
|
+
the vendor's prefix (``/investigations-v2/breach/...``) still lines up."""
|
|
61
|
+
prefixes: list[str] = []
|
|
62
|
+
for server in document.get("servers") or []:
|
|
63
|
+
url = server.get("url", "") if isinstance(server, dict) else ""
|
|
64
|
+
for name, variable in ((server.get("variables") or {}) if isinstance(server, dict) else {}).items():
|
|
65
|
+
url = url.replace("{" + name + "}", str(variable.get("default", "")))
|
|
66
|
+
path = urllib.parse.urlsplit(url).path.rstrip("/") if "://" in url else url.rstrip("/")
|
|
67
|
+
if path and path not in prefixes:
|
|
68
|
+
prefixes.append(path)
|
|
69
|
+
return sorted(prefixes, key=len, reverse=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _canonical(path: str) -> str:
|
|
73
|
+
"""Placeholder names differ between the world and the document; shapes must not."""
|
|
74
|
+
out, depth = [], 0
|
|
75
|
+
for char in path:
|
|
76
|
+
if char == "{":
|
|
77
|
+
depth += 1
|
|
78
|
+
out.append("{}")
|
|
79
|
+
elif char == "}":
|
|
80
|
+
depth -= 1
|
|
81
|
+
elif depth == 0:
|
|
82
|
+
out.append(char)
|
|
83
|
+
return "".join(out)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def find_operation(document: dict, prefixes: list[str], operation: Operation) -> tuple[str, dict] | None:
|
|
87
|
+
"""The document's path item + method for a world operation, prefix-tolerant."""
|
|
88
|
+
candidates = [operation.path] + [operation.path[len(p) :] for p in prefixes if operation.path.startswith(p + "/")]
|
|
89
|
+
wanted = {_canonical(c) for c in candidates}
|
|
90
|
+
for path, item in (document.get("paths") or {}).items():
|
|
91
|
+
if _canonical(path) in wanted and isinstance(item, dict) and operation.method.lower() in item:
|
|
92
|
+
return path, item[operation.method.lower()]
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------- lenient validation
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Checker:
|
|
100
|
+
def __init__(self, document: dict):
|
|
101
|
+
self.document = document
|
|
102
|
+
|
|
103
|
+
def deref(self, schema: Any, seen: tuple[str, ...] = ()) -> dict:
|
|
104
|
+
while isinstance(schema, dict) and "$ref" in schema:
|
|
105
|
+
ref = schema["$ref"]
|
|
106
|
+
if ref in seen or not ref.startswith("#/"):
|
|
107
|
+
return {}
|
|
108
|
+
seen = seen + (ref,)
|
|
109
|
+
node: Any = self.document
|
|
110
|
+
for part in ref[2:].split("/"):
|
|
111
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
112
|
+
node = node.get(part) if isinstance(node, dict) else None
|
|
113
|
+
if node is None:
|
|
114
|
+
return {}
|
|
115
|
+
siblings = {k: v for k, v in schema.items() if k != "$ref"}
|
|
116
|
+
schema = {**node, **siblings} if isinstance(node, dict) else {}
|
|
117
|
+
return schema if isinstance(schema, dict) else {}
|
|
118
|
+
|
|
119
|
+
def check(self, schema: Any, value: Any, path: str = "$") -> list[str]:
|
|
120
|
+
schema = self.deref(schema)
|
|
121
|
+
if not schema:
|
|
122
|
+
return []
|
|
123
|
+
problems: list[str] = []
|
|
124
|
+
types = schema.get("type")
|
|
125
|
+
types = [types] if isinstance(types, str) else list(types or [])
|
|
126
|
+
if schema.get("nullable") and "null" not in types:
|
|
127
|
+
types.append("null")
|
|
128
|
+
actual = {
|
|
129
|
+
type(None): "null",
|
|
130
|
+
bool: "boolean",
|
|
131
|
+
str: "string",
|
|
132
|
+
dict: "object",
|
|
133
|
+
list: "array",
|
|
134
|
+
int: "integer",
|
|
135
|
+
float: "number",
|
|
136
|
+
}.get(type(value), "unknown")
|
|
137
|
+
if types and not (
|
|
138
|
+
actual in types
|
|
139
|
+
or (actual == "integer" and "number" in types)
|
|
140
|
+
or (actual == "number" and "integer" in types and float(value).is_integer())
|
|
141
|
+
):
|
|
142
|
+
return [f"{path}: expected {' | '.join(types)}, got {actual}"]
|
|
143
|
+
if value is None:
|
|
144
|
+
return problems
|
|
145
|
+
if "enum" in schema and value not in schema["enum"]:
|
|
146
|
+
problems.append(f"{path}: {value!r} is not one of the enum")
|
|
147
|
+
if "const" in schema and value != schema["const"]:
|
|
148
|
+
problems.append(f"{path}: differs from const {schema['const']!r}")
|
|
149
|
+
for keyword in ("allOf", "anyOf", "oneOf"):
|
|
150
|
+
branches = schema.get(keyword) or []
|
|
151
|
+
if not branches:
|
|
152
|
+
continue
|
|
153
|
+
results = [self.check(branch, value, path) for branch in branches]
|
|
154
|
+
passing = sum(1 for r in results if not r)
|
|
155
|
+
if keyword == "allOf" and passing != len(branches):
|
|
156
|
+
problems.extend(p for r in results for p in r)
|
|
157
|
+
elif keyword == "anyOf" and passing == 0:
|
|
158
|
+
problems.append(f"{path}: matches no anyOf branch ({'; '.join(results[0][:2])})")
|
|
159
|
+
elif keyword == "oneOf" and passing != 1:
|
|
160
|
+
problems.append(f"{path}: matches {passing} oneOf branches, not exactly one")
|
|
161
|
+
if isinstance(value, dict):
|
|
162
|
+
properties = schema.get("properties") or {}
|
|
163
|
+
for name in schema.get("required") or []:
|
|
164
|
+
if name not in value:
|
|
165
|
+
problems.append(f"{path}.{name}: required by the vendor, missing")
|
|
166
|
+
for name, member in value.items():
|
|
167
|
+
if name in properties:
|
|
168
|
+
problems.extend(self.check(properties[name], member, f"{path}.{name}"))
|
|
169
|
+
elif schema.get("additionalProperties") is False:
|
|
170
|
+
problems.append(f"{path}.{name}: not a property the vendor documents")
|
|
171
|
+
elif isinstance(schema.get("additionalProperties"), dict):
|
|
172
|
+
problems.extend(self.check(schema["additionalProperties"], member, f"{path}.{name}"))
|
|
173
|
+
if isinstance(value, list) and isinstance(schema.get("items"), dict):
|
|
174
|
+
for index, item in enumerate(value[:50]):
|
|
175
|
+
problems.extend(self.check(schema["items"], item, f"{path}[{index}]"))
|
|
176
|
+
return problems
|
|
177
|
+
|
|
178
|
+
def example(self, schema: Any, depth: int = 0) -> Any:
|
|
179
|
+
"""A body the vendor's request schema accepts, from its examples or its shape."""
|
|
180
|
+
schema = self.deref(schema)
|
|
181
|
+
for key in ("example", "default"):
|
|
182
|
+
if key in schema:
|
|
183
|
+
return schema[key]
|
|
184
|
+
if isinstance(schema.get("examples"), list) and schema["examples"]:
|
|
185
|
+
return schema["examples"][0]
|
|
186
|
+
if "enum" in schema and schema["enum"]:
|
|
187
|
+
return schema["enum"][0]
|
|
188
|
+
for keyword in ("allOf", "anyOf", "oneOf"):
|
|
189
|
+
if schema.get(keyword):
|
|
190
|
+
merged: dict = {}
|
|
191
|
+
for branch in schema[keyword] if keyword == "allOf" else schema[keyword][:1]:
|
|
192
|
+
value = self.example(branch, depth + 1)
|
|
193
|
+
if isinstance(value, dict):
|
|
194
|
+
merged.update(value)
|
|
195
|
+
else:
|
|
196
|
+
return value
|
|
197
|
+
return merged
|
|
198
|
+
types = schema.get("type")
|
|
199
|
+
kind = types if isinstance(types, str) else next((t for t in (types or []) if t != "null"), None)
|
|
200
|
+
if kind == "object" or (kind is None and "properties" in schema):
|
|
201
|
+
if depth > 6:
|
|
202
|
+
return {}
|
|
203
|
+
return {
|
|
204
|
+
name: self.example(prop, depth + 1)
|
|
205
|
+
for name, prop in (schema.get("properties") or {}).items()
|
|
206
|
+
if name in (schema.get("required") or [])
|
|
207
|
+
}
|
|
208
|
+
if kind == "array":
|
|
209
|
+
return (
|
|
210
|
+
[self.example(schema["items"], depth + 1)]
|
|
211
|
+
if isinstance(schema.get("items"), dict) and depth < 6
|
|
212
|
+
else []
|
|
213
|
+
)
|
|
214
|
+
if kind == "integer":
|
|
215
|
+
return int(schema.get("minimum", 1))
|
|
216
|
+
if kind == "number":
|
|
217
|
+
return float(schema.get("minimum", 1))
|
|
218
|
+
if kind == "boolean":
|
|
219
|
+
return True
|
|
220
|
+
if kind == "string":
|
|
221
|
+
fmt = schema.get("format")
|
|
222
|
+
if fmt == "date-time":
|
|
223
|
+
return "2026-01-01T00:00:00Z"
|
|
224
|
+
if fmt == "date":
|
|
225
|
+
return "2026-01-01"
|
|
226
|
+
if fmt == "uuid":
|
|
227
|
+
return "00000000-0000-4000-8000-000000000001"
|
|
228
|
+
if fmt == "email":
|
|
229
|
+
return "sample@example.invalid"
|
|
230
|
+
return "sample"
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---------------------------------------------------------------- the run
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass
|
|
238
|
+
class Outcome:
|
|
239
|
+
operation: str
|
|
240
|
+
method: str
|
|
241
|
+
path: str
|
|
242
|
+
documented: bool
|
|
243
|
+
status: int | None = None
|
|
244
|
+
expected: list[str] = field(default_factory=list)
|
|
245
|
+
schema: str = "unchecked" # ok | violations | no-schema | unchecked
|
|
246
|
+
problems: list[str] = field(default_factory=list)
|
|
247
|
+
request: dict = field(default_factory=dict)
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def ok(self) -> bool:
|
|
251
|
+
return (
|
|
252
|
+
self.documented
|
|
253
|
+
and self.status is not None
|
|
254
|
+
and str(self.status) in self.expected
|
|
255
|
+
and self.schema in ("ok", "no-schema")
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass
|
|
260
|
+
class Report:
|
|
261
|
+
world: str
|
|
262
|
+
outcomes: list[Outcome]
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def summary(self) -> dict:
|
|
266
|
+
checked = [o for o in self.outcomes if o.documented]
|
|
267
|
+
return {
|
|
268
|
+
"operations": len(self.outcomes),
|
|
269
|
+
"documented": len(checked),
|
|
270
|
+
"conforming": sum(1 for o in checked if o.ok),
|
|
271
|
+
"status_mismatch": sum(1 for o in checked if o.status is not None and str(o.status) not in o.expected),
|
|
272
|
+
"schema_violations": sum(1 for o in checked if o.schema == "violations"),
|
|
273
|
+
"undocumented": sum(1 for o in self.outcomes if not o.documented),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
def to_json(self) -> dict:
|
|
277
|
+
return {"world": self.world, "summary": self.summary, "operations": [o.__dict__ for o in self.outcomes]}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _credentials(base: str, spec, settings) -> dict[str, str]:
|
|
281
|
+
"""Headers that get the conformance run through the world's own auth, using the pin."""
|
|
282
|
+
if spec.scheme == "none":
|
|
283
|
+
return {}
|
|
284
|
+
if spec.scheme == "header":
|
|
285
|
+
return {spec.auth_name or "": PIN}
|
|
286
|
+
if spec.scheme == "bearer":
|
|
287
|
+
return {"Authorization": f"Bearer {PIN}"}
|
|
288
|
+
if spec.scheme == "basic":
|
|
289
|
+
return {"Authorization": "Basic " + base64.b64encode(PIN.encode()).decode()}
|
|
290
|
+
if spec.scheme == "query":
|
|
291
|
+
return {}
|
|
292
|
+
form = urllib.parse.urlencode(
|
|
293
|
+
{"grant_type": "client_credentials", "client_id": "conform", "client_secret": PIN}
|
|
294
|
+
).encode()
|
|
295
|
+
prefix = urllib.parse.urlsplit(spec.base_url).path.rstrip("/")
|
|
296
|
+
request = urllib.request.Request(base + prefix + (settings.token_path or "/oauth/token"), data=form, method="POST")
|
|
297
|
+
with urllib.request.urlopen(request, timeout=10) as reply:
|
|
298
|
+
return {"Authorization": "Bearer " + json.loads(reply.read())["access_token"]}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _hints(spec, settings) -> dict[str, tuple[str, str]]:
|
|
302
|
+
"""Where a placeholder's values live, learned from every operation that filters on it:
|
|
303
|
+
``{recordId}`` on a handler route resolves through the read route that maps it to ``id``."""
|
|
304
|
+
hints: dict[str, tuple[str, str]] = {}
|
|
305
|
+
for operation in spec.operations.values():
|
|
306
|
+
if not operation.entity:
|
|
307
|
+
continue
|
|
308
|
+
for arg, path in settings.for_operation(operation.name).filter.items():
|
|
309
|
+
hints.setdefault(arg, (operation.entity, path))
|
|
310
|
+
return hints
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _first_value(rows: list[dict], path: str) -> Any:
|
|
314
|
+
for row in rows:
|
|
315
|
+
found: Any = row
|
|
316
|
+
for part in path.split("."):
|
|
317
|
+
found = found.get(part) if isinstance(found, dict) else None
|
|
318
|
+
if found is not None and not isinstance(found, (dict, list)):
|
|
319
|
+
return found
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _sample_args(
|
|
324
|
+
operation: Operation, spec, settings, state: WorldState, checker: Checker, documented: dict | None
|
|
325
|
+
) -> tuple[dict[str, str], dict[str, str]]:
|
|
326
|
+
"""Path arguments from the world's own rows — the operation's declared ``sample``
|
|
327
|
+
first (a value a validating handler accepts), then this operation's filter, then any
|
|
328
|
+
operation's filter on the same placeholder, then the entity's primary key — else the
|
|
329
|
+
document's examples. The point is to hit data, not to probe 404s."""
|
|
330
|
+
op = settings.for_operation(operation.name)
|
|
331
|
+
hints = _hints(spec, settings)
|
|
332
|
+
args: dict[str, str] = {}
|
|
333
|
+
params: dict[str, str] = {}
|
|
334
|
+
documented_params = {p.get("name"): p for p in (documented or {}).get("parameters", []) if isinstance(p, dict)}
|
|
335
|
+
for name in operation.params:
|
|
336
|
+
if name in op.sample:
|
|
337
|
+
params[name] = op.sample[name]
|
|
338
|
+
for name in operation.placeholders:
|
|
339
|
+
value = op.sample.get(name)
|
|
340
|
+
if value is None and name in op.filter and operation.entity:
|
|
341
|
+
value = _first_value(state.rows(operation.entity), op.filter[name])
|
|
342
|
+
if value is None and name in hints:
|
|
343
|
+
entity, path = hints[name]
|
|
344
|
+
value = _first_value(state.rows(entity), path)
|
|
345
|
+
if value is None and operation.entity:
|
|
346
|
+
key = state.contract.entities[operation.entity]["primary_key"]
|
|
347
|
+
if len(key) == 1:
|
|
348
|
+
value = _first_value(state.rows(operation.entity), key[0])
|
|
349
|
+
if value is None and name in documented_params:
|
|
350
|
+
value = checker.example(documented_params[name].get("schema") or {})
|
|
351
|
+
args[name] = str(value if value is not None else "1")
|
|
352
|
+
return args, params
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def conform(world: str | Path, openapi: list[str | Path], *, max_operations: int | None = None) -> Report:
|
|
356
|
+
root = Path(world)
|
|
357
|
+
spec = load_spec(root)
|
|
358
|
+
settings = load_api_settings(root, spec)
|
|
359
|
+
documents = [load_openapi(p) for p in openapi]
|
|
360
|
+
if not documents:
|
|
361
|
+
fail("conform", "Give at least one --openapi document to conform against", "/conform/openapi")
|
|
362
|
+
server = serve(root, port=0, host="127.0.0.1", api_key=PIN)
|
|
363
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
364
|
+
thread.start()
|
|
365
|
+
base = f"http://127.0.0.1:{server.server_port}"
|
|
366
|
+
prefix = urllib.parse.urlsplit(spec.base_url).path.rstrip("/")
|
|
367
|
+
state: WorldState = server.world_state # type: ignore[attr-defined]
|
|
368
|
+
outcomes: list[Outcome] = []
|
|
369
|
+
try:
|
|
370
|
+
headers = _credentials(base, spec, settings)
|
|
371
|
+
for operation in list(spec.operations.values())[:max_operations]:
|
|
372
|
+
located = None
|
|
373
|
+
checker = None
|
|
374
|
+
for document in documents:
|
|
375
|
+
found = find_operation(document, server_prefixes(document), operation)
|
|
376
|
+
if found:
|
|
377
|
+
located, checker = found, Checker(document)
|
|
378
|
+
break
|
|
379
|
+
outcome = Outcome(operation.name, operation.method, operation.path, documented=located is not None)
|
|
380
|
+
if located is None or checker is None:
|
|
381
|
+
outcomes.append(outcome)
|
|
382
|
+
continue
|
|
383
|
+
_, documented = located
|
|
384
|
+
responses = documented.get("responses") or {}
|
|
385
|
+
outcome.expected = [code for code in responses if code.startswith("2")] or ["200"]
|
|
386
|
+
args, params = _sample_args(operation, spec, settings, state, checker, documented)
|
|
387
|
+
if spec.scheme == "query":
|
|
388
|
+
params[spec.auth_name or ""] = PIN
|
|
389
|
+
path = operation.path
|
|
390
|
+
for name, value in args.items():
|
|
391
|
+
path = path.replace("{" + name + "}", urllib.parse.quote(value, safe=""))
|
|
392
|
+
url = base + prefix + path + (f"?{urllib.parse.urlencode(params)}" if params else "")
|
|
393
|
+
body = None
|
|
394
|
+
content = ((documented.get("requestBody") or {}).get("content") or {}).get("application/json") or {}
|
|
395
|
+
if operation.method in ("POST", "PUT", "PATCH"):
|
|
396
|
+
body = content.get("example") or checker.example(content.get("schema") or {}) or {}
|
|
397
|
+
outcome.request = {"url": prefix + path, "args": args, **({"body": body} if body is not None else {})}
|
|
398
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
399
|
+
request = urllib.request.Request(
|
|
400
|
+
url,
|
|
401
|
+
method=operation.method,
|
|
402
|
+
data=data,
|
|
403
|
+
headers={**headers, **({"Content-Type": "application/json"} if data else {})},
|
|
404
|
+
)
|
|
405
|
+
try:
|
|
406
|
+
with urllib.request.urlopen(request, timeout=15) as reply:
|
|
407
|
+
status, raw = reply.status, reply.read()
|
|
408
|
+
except urllib.error.HTTPError as error:
|
|
409
|
+
status, raw = error.code, error.read()
|
|
410
|
+
outcome.status = status
|
|
411
|
+
answer = responses.get(str(status)) or responses.get("default") or {}
|
|
412
|
+
schema = ((answer.get("content") or {}).get("application/json") or {}).get("schema")
|
|
413
|
+
if schema is None:
|
|
414
|
+
outcome.schema = "no-schema"
|
|
415
|
+
else:
|
|
416
|
+
try:
|
|
417
|
+
value = json.loads(raw) if raw else None
|
|
418
|
+
except json.JSONDecodeError:
|
|
419
|
+
value = None
|
|
420
|
+
outcome.problems.append("$: body is not JSON")
|
|
421
|
+
outcome.problems.extend(checker.check(schema, value))
|
|
422
|
+
outcome.schema = "violations" if outcome.problems else "ok"
|
|
423
|
+
outcomes.append(outcome)
|
|
424
|
+
finally:
|
|
425
|
+
server.shutdown()
|
|
426
|
+
server.server_close()
|
|
427
|
+
state.close()
|
|
428
|
+
return Report(str(root), outcomes)
|