openrunner-sdk 2.41.8__tar.gz → 2.41.10__tar.gz
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.
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/PKG-INFO +3 -3
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/CLAUDE.md +1 -3
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/__init__.py +19 -2
- openrunner_sdk-2.41.10/openrunner/_multipart.py +116 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/api_client.py +181 -45
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/cli.py +159 -23
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/datasets.py +216 -90
- openrunner_sdk-2.41.10/openrunner/errors.py +41 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/install_commands.py +40 -16
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/mcp_server.py +38 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/media.py +5 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/projects.py +51 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/run.py +72 -16
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/pyproject.toml +3 -3
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli.py +4 -1
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_artifact_sweep.py +3 -1
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_infra.py +27 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_datasets_module.py +92 -57
- openrunner_sdk-2.41.10/tests/test_large_file_uploads.py +393 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_media_extra.py +15 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_run_module.py +24 -5
- openrunner_sdk-2.41.8/openrunner/_native.py +0 -37
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/.gitignore +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/=6.0 +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/=8.1 +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/README.md +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/_retry_transport.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/_safepath.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/_secret_rules.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/artifact.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/autoresearch.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/buffer.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/cache.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/config.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/cost.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/dataset.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/environment.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/evaluation.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/feedback.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/git_info.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/api_integration.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/cost_guard.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/gpu_types.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/price_compare.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/provider_base.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/forge_provider.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/lambda_cloud.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/modal_provider.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/ovhcloud.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/runpod.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/vastai.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/runner.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/workspace_sync.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/guardrails.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/handover.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/hooks/patent-lab-post-commit +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/accelerate.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/anthropic_tracer.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/catboost.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/diffusers.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/fastai.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/forced_alignment.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/gladia.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/gymnasium.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/huggingface.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/hydra.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/ignite.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/jax.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/keras.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/langchain.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/lightgbm.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/lightning.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/llamaindex.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/openai_finetune.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/openai_tracer.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/optuna.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/pytorch.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/sb3.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/sklearn.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/skypilot.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/tensorflow.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/trl.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/tts.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/ultralytics.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/voice_agent.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/whisper.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/xgboost.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/launch.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/llms-full.txt +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/llms.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/llms.txt +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/machine.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/migrate.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/model.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/model_card.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/offline.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/patent_scan.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/pii.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/plot.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/prompt.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/query_api.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/redact.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/reload.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/research.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/research_policy.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/scorers.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/sender.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/session.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/settings.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/summary.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/sweep.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/sync.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/system_metrics.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/tensorboard.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/tools/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/tools/patent_report.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/trace.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/transcript_formatter.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wal.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wandb_compat/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wandb_compat/_shim.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wer.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/scripts/gen_betterleaks_rules.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/conftest.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/config.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/metadata.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/metrics.jsonl +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/summary.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/model-checkpoint/manifest.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/model-checkpoint/weights.bin +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/sample-image/manifest.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/sample-image/preview.png +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/config.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/metadata.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/metrics.jsonl +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/summary.json +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/integration/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/integration/conftest.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/integration/test_artifact_round_trip.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_alert.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_aliases.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_api_client.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_api_client_methods.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_api_client_transport.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_artifact.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_buffer.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cache.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_class_scorers.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_datasets.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_gpu_registry_deploy.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_remainder.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_topup.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_config.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_evaluation.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_finish.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_git_info.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/__init__.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/conftest.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_cost_guard.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_engine_serverside.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_engine_stream_keepalive.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_forge.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_governed_jobs.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_gpu_cluster.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_gpu_types.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_lambda.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_modal.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_ovh_training.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_ovhcloud.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_price_compare.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_provider_base.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_runner.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_runpod.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_sdk_gpu_api.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_vastai.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_workspace_sync.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu_modal_creds.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_init.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_init_facade.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_install_commands.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_fastai.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_huggingface.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_keras.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_langchain.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_lightning.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_pytorch.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_setA.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_setB.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_setC.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_sklearn.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_xgboost.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_launch.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_llms.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_log.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_log_code.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_machine_migrate.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_decisions.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_monitor.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_predictions.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_server.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_media.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_migrate.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_offline.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_offline_sync.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_patent_scan.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_pii.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_plot.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_plot_media_extra2.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_prompt_module.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_pure_modules.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_query_api.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_redact.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_reload_module.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_research_module.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_resume.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_retry_transport.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j1.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j2.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j3.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j4.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_core_edges.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_features.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sender.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_session_module.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_session_sync.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_summary.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sweep.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_system_metrics.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_topup_hi.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_trace.py +0 -0
- {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_wandb_compat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: openrunner-sdk
|
|
3
|
-
Version: 2.41.
|
|
3
|
+
Version: 2.41.10
|
|
4
4
|
Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
|
|
5
5
|
Project-URL: Homepage, https://github.com/gladiaio/openrunner
|
|
6
6
|
Project-URL: Repository, https://github.com/gladiaio/openrunner
|
|
@@ -23,7 +23,7 @@ Requires-Dist: click<9.0,>=8.1
|
|
|
23
23
|
Requires-Dist: httpx<1.0,>=0.27
|
|
24
24
|
Requires-Dist: mcp<2.0,>=1.0
|
|
25
25
|
Requires-Dist: nvidia-ml-py<14.0,>=12.0
|
|
26
|
-
Requires-Dist: pillow<
|
|
26
|
+
Requires-Dist: pillow<14,>=10.0
|
|
27
27
|
Requires-Dist: psutil<8.0,>=6.0
|
|
28
28
|
Provides-Extra: accelerate
|
|
29
29
|
Requires-Dist: accelerate<1.0,>=0.24; extra == 'accelerate'
|
|
@@ -35,7 +35,7 @@ Provides-Extra: dev
|
|
|
35
35
|
Requires-Dist: matplotlib<5.0,>=3.7; extra == 'dev'
|
|
36
36
|
Requires-Dist: num2words2<2.0,>=0.1; extra == 'dev'
|
|
37
37
|
Requires-Dist: numpy<3.0,>=1.24; extra == 'dev'
|
|
38
|
-
Requires-Dist: pillow<
|
|
38
|
+
Requires-Dist: pillow<14,>=10.0; extra == 'dev'
|
|
39
39
|
Requires-Dist: pytest-asyncio<1.0,>=0.23; extra == 'dev'
|
|
40
40
|
Requires-Dist: pytest-cov<6.0,>=5.0; extra == 'dev'
|
|
41
41
|
Requires-Dist: pytest<9.0,>=8.0; extra == 'dev'
|
|
@@ -88,9 +88,7 @@ the LanceDB table on the dataset's own connection.
|
|
|
88
88
|
|
|
89
89
|
Beyond FTS, the client exposes semantic / lexical search and **derivation by
|
|
90
90
|
query** — build a child dataset from a SUBSET of a parent selected by a search
|
|
91
|
-
(only matching primary keys stored; no media copied; de-dup + lineage automatic)
|
|
92
|
-
These delegate to the native Rust core (`openrunner-core` wheel) when installed,
|
|
93
|
-
falling back to pure-Python httpx otherwise:
|
|
91
|
+
(only matching primary keys stored; no media copied; de-dup + lineage automatic):
|
|
94
92
|
|
|
95
93
|
```python
|
|
96
94
|
ord.search_dataset(repo, q, mode="mixed"|"exact", lang="fr") # BM25 / phrase, lang filter
|
|
@@ -72,6 +72,7 @@ from pathlib import Path
|
|
|
72
72
|
from typing import Any
|
|
73
73
|
|
|
74
74
|
from openrunner.artifact import Artifact
|
|
75
|
+
from openrunner.errors import ArtifactUploadError, DatasetUploadError, UploadError
|
|
75
76
|
from openrunner.model_card import (
|
|
76
77
|
MODEL_CARD_STANDARD,
|
|
77
78
|
model_card_standard,
|
|
@@ -166,7 +167,7 @@ from openrunner.reload import (
|
|
|
166
167
|
dataset_handle,
|
|
167
168
|
)
|
|
168
169
|
from openrunner.sync import sync_environment, scan_environment
|
|
169
|
-
from openrunner.projects import create_project
|
|
170
|
+
from openrunner.projects import create_project, rename_project
|
|
170
171
|
from openrunner.run import Run
|
|
171
172
|
from openrunner.settings import SDKSettings, resolve_project, pin_project
|
|
172
173
|
from openrunner.summary import Summary
|
|
@@ -861,7 +862,10 @@ def log_artifact(
|
|
|
861
862
|
type: str | None = None,
|
|
862
863
|
aliases: list[str] | None = None,
|
|
863
864
|
) -> dict[str, Any] | None:
|
|
864
|
-
"""Upload an artifact to the active run.
|
|
865
|
+
"""Upload an artifact to the active run.
|
|
866
|
+
|
|
867
|
+
Raises :class:`ArtifactUploadError` when the upload fails (unless
|
|
868
|
+
``OPENRUNNER_ARTIFACT_STRICT=0``); other SDK errors are logged, never raised.
|
|
865
869
|
|
|
866
870
|
Args:
|
|
867
871
|
artifact: An Artifact with files added via add_file/add_dir, OR a path
|
|
@@ -880,6 +884,9 @@ def log_artifact(
|
|
|
880
884
|
return _active_run.log_artifact(
|
|
881
885
|
artifact, name=name, type=type, aliases=aliases
|
|
882
886
|
)
|
|
887
|
+
except ArtifactUploadError:
|
|
888
|
+
# An upload failure must reach the caller (see OPENRUNNER_ARTIFACT_STRICT).
|
|
889
|
+
raise
|
|
883
890
|
except Exception as e:
|
|
884
891
|
logger.warning("openrunner.log_artifact() failed: %s", e)
|
|
885
892
|
return None
|
|
@@ -903,6 +910,8 @@ def finish_artifact(artifact: Artifact) -> dict[str, Any] | None:
|
|
|
903
910
|
logger.warning("openrunner.finish_artifact(): no active run")
|
|
904
911
|
return None
|
|
905
912
|
return _active_run.finish_artifact(artifact)
|
|
913
|
+
except ArtifactUploadError:
|
|
914
|
+
raise
|
|
906
915
|
except Exception as e:
|
|
907
916
|
logger.warning("openrunner.finish_artifact() failed: %s", e)
|
|
908
917
|
return None
|
|
@@ -928,6 +937,8 @@ def link_artifact(
|
|
|
928
937
|
logger.warning("openrunner.link_artifact(): no active run")
|
|
929
938
|
return None
|
|
930
939
|
return _active_run.link_artifact(artifact, aliases=aliases)
|
|
940
|
+
except ArtifactUploadError:
|
|
941
|
+
raise
|
|
931
942
|
except Exception as e:
|
|
932
943
|
logger.warning("openrunner.link_artifact() failed: %s", e)
|
|
933
944
|
return None
|
|
@@ -1079,6 +1090,10 @@ def push(
|
|
|
1079
1090
|
Returns:
|
|
1080
1091
|
The created version info dict, or None on failure.
|
|
1081
1092
|
|
|
1093
|
+
Raises:
|
|
1094
|
+
ArtifactUploadError: when any file fails to upload or the version
|
|
1095
|
+
cannot be confirmed (unless ``OPENRUNNER_ARTIFACT_STRICT=0``).
|
|
1096
|
+
|
|
1082
1097
|
Example::
|
|
1083
1098
|
|
|
1084
1099
|
openrunner.push("model.safetensors", "gladia/my-project/my-model",
|
|
@@ -1103,6 +1118,8 @@ def push(
|
|
|
1103
1118
|
except Exception as e: # noqa: BLE001
|
|
1104
1119
|
logger.warning("openrunner.push(): set_model_card failed: %s", e)
|
|
1105
1120
|
return result
|
|
1121
|
+
except ArtifactUploadError:
|
|
1122
|
+
raise
|
|
1106
1123
|
except Exception as e: # noqa: BLE001
|
|
1107
1124
|
logger.warning("openrunner.push() failed: %s", e)
|
|
1108
1125
|
return None
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Client side of S3 multipart uploads (presigned part URLs).
|
|
2
|
+
|
|
3
|
+
The server signs one PUT URL per part (``/.../multipart/start``); this module
|
|
4
|
+
PUTs the parts straight to object storage -- each part read from disk on
|
|
5
|
+
demand, retried independently, a few in parallel -- and returns the ETags the
|
|
6
|
+
server needs for ``CompleteMultipartUpload``. A single dropped connection now
|
|
7
|
+
costs one 100 MiB part instead of the whole multi-GB file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import random
|
|
15
|
+
import time
|
|
16
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from .errors import UploadError
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("openrunner")
|
|
24
|
+
|
|
25
|
+
MIB = 1024 * 1024
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _env_mb(name: str, default: int) -> int:
|
|
29
|
+
try:
|
|
30
|
+
return max(1, int(os.environ.get(name, "") or default))
|
|
31
|
+
except ValueError:
|
|
32
|
+
return default
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def multipart_threshold() -> int:
|
|
36
|
+
"""Files at or above this size (bytes) use multipart. Env:
|
|
37
|
+
``OPENRUNNER_MULTIPART_THRESHOLD_MB`` (default 100)."""
|
|
38
|
+
return _env_mb("OPENRUNNER_MULTIPART_THRESHOLD_MB", 100) * MIB
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def part_size() -> int:
|
|
42
|
+
"""Requested part size (bytes). Env: ``OPENRUNNER_MULTIPART_PART_MB``
|
|
43
|
+
(default 100; the server clamps to S3 limits)."""
|
|
44
|
+
return _env_mb("OPENRUNNER_MULTIPART_PART_MB", 100) * MIB
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def part_workers() -> int:
|
|
48
|
+
"""Parts uploaded concurrently. Env: ``OPENRUNNER_MULTIPART_WORKERS``
|
|
49
|
+
(default 4 -> ~4 x part size of RAM)."""
|
|
50
|
+
return _env_mb("OPENRUNNER_MULTIPART_WORKERS", 4)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _put_part(url: str, data: bytes, headers: dict[str, str], retries: int,
|
|
54
|
+
timeout: httpx.Timeout) -> str:
|
|
55
|
+
last: Exception | None = None
|
|
56
|
+
for attempt in range(retries):
|
|
57
|
+
try:
|
|
58
|
+
resp = httpx.put(url, content=data, headers=headers, timeout=timeout)
|
|
59
|
+
if resp.status_code >= 400:
|
|
60
|
+
raise UploadError(
|
|
61
|
+
f"HTTP {resp.status_code}: {(resp.text or '')[:300]}"
|
|
62
|
+
)
|
|
63
|
+
etag = resp.headers.get("ETag") or resp.headers.get("etag")
|
|
64
|
+
if not etag:
|
|
65
|
+
raise UploadError(
|
|
66
|
+
"storage returned no ETag for the part (bucket CORS must "
|
|
67
|
+
"expose the ETag header)"
|
|
68
|
+
)
|
|
69
|
+
return etag
|
|
70
|
+
except Exception as e: # noqa: BLE001
|
|
71
|
+
last = e
|
|
72
|
+
if attempt < retries - 1:
|
|
73
|
+
delay = min(1.0 * (2 ** attempt), 20.0) * (0.5 + random.random())
|
|
74
|
+
logger.warning(
|
|
75
|
+
"part upload failed (attempt %d/%d), retrying in %.1fs: %s",
|
|
76
|
+
attempt + 1, retries, delay, e,
|
|
77
|
+
)
|
|
78
|
+
time.sleep(delay)
|
|
79
|
+
raise UploadError(str(last) if last else "part upload failed")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def upload_parts(
|
|
83
|
+
file_path: str,
|
|
84
|
+
parts: list[dict[str, Any]],
|
|
85
|
+
part_size: int,
|
|
86
|
+
*,
|
|
87
|
+
headers: dict[str, str] | None = None,
|
|
88
|
+
workers: int | None = None,
|
|
89
|
+
retries: int = 4,
|
|
90
|
+
) -> list[dict[str, Any]]:
|
|
91
|
+
"""PUT every part of ``file_path`` to its presigned URL.
|
|
92
|
+
|
|
93
|
+
``parts`` is the server's ``[{"part_number", "url"}, ...]``. Returns
|
|
94
|
+
``[{"part_number", "etag"}, ...]``; raises :class:`UploadError` naming the
|
|
95
|
+
first part that exhausted its retries.
|
|
96
|
+
"""
|
|
97
|
+
size = os.path.getsize(file_path)
|
|
98
|
+
hdrs = dict(headers or {})
|
|
99
|
+
timeout = httpx.Timeout(connect=30.0, read=300.0, write=300.0, pool=30.0)
|
|
100
|
+
|
|
101
|
+
def _one(p: dict[str, Any]) -> dict[str, Any]:
|
|
102
|
+
n = int(p["part_number"])
|
|
103
|
+
offset = (n - 1) * part_size
|
|
104
|
+
length = max(0, min(part_size, size - offset))
|
|
105
|
+
with open(file_path, "rb") as fh:
|
|
106
|
+
fh.seek(offset)
|
|
107
|
+
data = fh.read(length)
|
|
108
|
+
try:
|
|
109
|
+
etag = _put_part(p["url"], data, hdrs, retries, timeout)
|
|
110
|
+
except UploadError as e:
|
|
111
|
+
raise UploadError(f"part {n}/{len(parts)} of {os.path.basename(file_path)} failed: {e}") from e
|
|
112
|
+
return {"part_number": n, "etag": etag}
|
|
113
|
+
|
|
114
|
+
with ThreadPoolExecutor(max_workers=max(1, workers or part_workers())) as ex:
|
|
115
|
+
results = list(ex.map(_one, parts))
|
|
116
|
+
return sorted(results, key=lambda r: r["part_number"])
|
|
@@ -19,6 +19,8 @@ from typing import Any
|
|
|
19
19
|
|
|
20
20
|
import httpx
|
|
21
21
|
|
|
22
|
+
from . import _multipart as _mp
|
|
23
|
+
|
|
22
24
|
logger = logging.getLogger("openrunner")
|
|
23
25
|
|
|
24
26
|
# -- Retry configuration ------------------------------------------------------
|
|
@@ -77,6 +79,38 @@ def _retry_request(fn, *args, **kwargs):
|
|
|
77
79
|
raise last_error
|
|
78
80
|
|
|
79
81
|
|
|
82
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
83
|
+
"""raise_for_status() that keeps the server's error detail in the message."""
|
|
84
|
+
if response.status_code < 400:
|
|
85
|
+
return
|
|
86
|
+
detail = ""
|
|
87
|
+
try:
|
|
88
|
+
body = response.json()
|
|
89
|
+
detail = body.get("detail", body) if isinstance(body, dict) else body
|
|
90
|
+
except Exception: # noqa: BLE001
|
|
91
|
+
detail = (response.text or "")[:300]
|
|
92
|
+
raise httpx.HTTPStatusError(
|
|
93
|
+
f"HTTP {response.status_code} for {response.request.method} "
|
|
94
|
+
f"{response.request.url.path}: {detail}",
|
|
95
|
+
request=response.request,
|
|
96
|
+
response=response,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _describe(e: BaseException) -> str:
|
|
101
|
+
"""Human-readable cause (``TimeoutException`` str() is often empty)."""
|
|
102
|
+
text = str(e).strip()
|
|
103
|
+
return f"{type(e).__name__}: {text}" if text else type(e).__name__
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _fmt_bytes(n: int) -> str:
|
|
107
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
108
|
+
if n < 1024 or unit == "GB":
|
|
109
|
+
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
|
110
|
+
n /= 1024
|
|
111
|
+
return f"{n} B"
|
|
112
|
+
|
|
113
|
+
|
|
80
114
|
class APIClient:
|
|
81
115
|
"""HTTP client for the OpenRunner server API.
|
|
82
116
|
|
|
@@ -779,6 +813,7 @@ class APIClient:
|
|
|
779
813
|
metadata: dict[str, Any] | None = None,
|
|
780
814
|
base_version_id: str | None = None,
|
|
781
815
|
message: str | None = None,
|
|
816
|
+
raise_on_error: bool = False,
|
|
782
817
|
) -> dict[str, Any] | None:
|
|
783
818
|
"""POST /runs/{run_id}/artifacts -- create a new artifact version.
|
|
784
819
|
|
|
@@ -807,27 +842,56 @@ class APIClient:
|
|
|
807
842
|
body["base_version_id"] = base_version_id
|
|
808
843
|
if message is not None:
|
|
809
844
|
body["message"] = message
|
|
810
|
-
|
|
811
|
-
|
|
845
|
+
# The server does per-file dedup lookups + layer building on this
|
|
846
|
+
# call; under load it can exceed the 30s default read timeout, and a
|
|
847
|
+
# timed-out POST that is retried races the still-running first
|
|
848
|
+
# request (version-number unique constraint -> 500). Give it room.
|
|
849
|
+
response = self._request(
|
|
850
|
+
"post", f"/runs/{run_id}/artifacts", json=body,
|
|
851
|
+
timeout=httpx.Timeout(30.0, read=180.0),
|
|
852
|
+
)
|
|
853
|
+
_raise_for_status(response)
|
|
812
854
|
return response.json()
|
|
813
855
|
except Exception as e:
|
|
856
|
+
if raise_on_error:
|
|
857
|
+
from .errors import ArtifactUploadError
|
|
858
|
+
|
|
859
|
+
raise ArtifactUploadError(
|
|
860
|
+
f"could not create artifact version {name!r}: {_describe(e)}"
|
|
861
|
+
) from e
|
|
814
862
|
logger.warning("create_artifact_version failed: %s", e)
|
|
815
863
|
return None
|
|
816
864
|
|
|
817
865
|
def confirm_artifact_version(
|
|
818
|
-
self, version_id: str
|
|
866
|
+
self, version_id: str, raise_on_error: bool = False
|
|
819
867
|
) -> dict[str, Any] | None:
|
|
820
868
|
"""POST /artifacts/versions/{version_id}/confirm -- confirm upload.
|
|
821
869
|
|
|
822
|
-
Returns {status, version}, or None on failure.
|
|
870
|
+
Returns {status, version}, or None on failure. With
|
|
871
|
+
``raise_on_error=True`` a failure (including a server response whose
|
|
872
|
+
status is not ``confirmed``, e.g. 409 missing objects) raises
|
|
873
|
+
:class:`~openrunner.errors.ArtifactUploadError`.
|
|
823
874
|
"""
|
|
824
875
|
try:
|
|
825
876
|
response = self._request("post",
|
|
826
|
-
f"/artifacts/versions/{version_id}/confirm"
|
|
877
|
+
f"/artifacts/versions/{version_id}/confirm",
|
|
878
|
+
timeout=httpx.Timeout(30.0, read=180.0),
|
|
827
879
|
)
|
|
828
|
-
response
|
|
829
|
-
|
|
830
|
-
|
|
880
|
+
_raise_for_status(response)
|
|
881
|
+
data = response.json()
|
|
882
|
+
if raise_on_error and isinstance(data, dict) and data.get("status") not in (
|
|
883
|
+
None, "confirmed"
|
|
884
|
+
):
|
|
885
|
+
raise RuntimeError(f"server left the version in state {data.get('status')!r}")
|
|
886
|
+
return data
|
|
887
|
+
except Exception as e:
|
|
888
|
+
if raise_on_error:
|
|
889
|
+
from .errors import ArtifactUploadError
|
|
890
|
+
|
|
891
|
+
raise ArtifactUploadError(
|
|
892
|
+
f"could not confirm artifact version {version_id}: {_describe(e)}",
|
|
893
|
+
version_id=version_id,
|
|
894
|
+
) from e
|
|
831
895
|
logger.warning("confirm_artifact_version failed: %s", e)
|
|
832
896
|
return None
|
|
833
897
|
|
|
@@ -1129,52 +1193,124 @@ class APIClient:
|
|
|
1129
1193
|
return []
|
|
1130
1194
|
|
|
1131
1195
|
def upload_file_to_presigned_url(
|
|
1132
|
-
self,
|
|
1196
|
+
self,
|
|
1197
|
+
presigned_url: str,
|
|
1198
|
+
file_path: str,
|
|
1199
|
+
run_id: str = "",
|
|
1200
|
+
*,
|
|
1201
|
+
version_id: str | None = None,
|
|
1202
|
+
name: str | None = None,
|
|
1203
|
+
raise_on_error: bool = False,
|
|
1133
1204
|
) -> bool:
|
|
1134
|
-
"""
|
|
1135
|
-
|
|
1136
|
-
Handles
|
|
1137
|
-
1.
|
|
1138
|
-
|
|
1139
|
-
|
|
1205
|
+
"""Upload a local file for an artifact version.
|
|
1206
|
+
|
|
1207
|
+
Handles:
|
|
1208
|
+
1. Large files (>= ``OPENRUNNER_MULTIPART_THRESHOLD_MB``, default 100)
|
|
1209
|
+
when ``version_id`` + ``name`` are given and storage is directly
|
|
1210
|
+
reachable: S3 multipart via presigned part URLs (parts retried
|
|
1211
|
+
independently). Falls back to a single PUT on servers without the
|
|
1212
|
+
multipart endpoints.
|
|
1213
|
+
2. Relative proxy URL (/api/v1/runs/...) — streamed PUT via the
|
|
1214
|
+
authenticated client.
|
|
1215
|
+
3. Absolute URL — streamed direct PUT, falling back to the API proxy on
|
|
1216
|
+
ConnectError.
|
|
1217
|
+
|
|
1218
|
+
Returns True on success. On failure returns False after logging the
|
|
1219
|
+
real cause at ERROR level, or raises
|
|
1220
|
+
:class:`~openrunner.errors.ArtifactUploadError` if ``raise_on_error``.
|
|
1140
1221
|
"""
|
|
1141
|
-
import os
|
|
1142
|
-
|
|
1143
1222
|
try:
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1223
|
+
size = os.path.getsize(file_path)
|
|
1224
|
+
if (
|
|
1225
|
+
version_id
|
|
1226
|
+
and name
|
|
1227
|
+
and not presigned_url.startswith("/")
|
|
1228
|
+
and size >= _mp.multipart_threshold()
|
|
1229
|
+
):
|
|
1230
|
+
if self._upload_multipart(version_id, name, file_path, size):
|
|
1231
|
+
return True
|
|
1232
|
+
self._upload_single(presigned_url, file_path, size, run_id)
|
|
1233
|
+
return True
|
|
1234
|
+
except Exception as e:
|
|
1235
|
+
label = name or os.path.basename(file_path)
|
|
1236
|
+
msg = f"upload of {label!r} ({_fmt_bytes(os.path.getsize(file_path)) if os.path.exists(file_path) else '?'}) failed: {_describe(e)}"
|
|
1237
|
+
if raise_on_error:
|
|
1238
|
+
from .errors import ArtifactUploadError
|
|
1151
1239
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1240
|
+
raise ArtifactUploadError(
|
|
1241
|
+
msg, version_id=version_id, failed_files=[label]
|
|
1242
|
+
) from e
|
|
1243
|
+
logger.error("upload_file_to_presigned_url: %s", msg)
|
|
1244
|
+
return False
|
|
1154
1245
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1246
|
+
def _upload_single(self, presigned_url: str, file_path: str, size: int, run_id: str) -> None:
|
|
1247
|
+
"""One streamed PUT (file read in chunks, never fully in RAM). Raises."""
|
|
1248
|
+
# Per-operation timeouts. The read timeout covers the wait for the
|
|
1249
|
+
# response AFTER the body is sent: the API proxy hashes the body and
|
|
1250
|
+
# copies it to storage before answering, so scale it with size.
|
|
1251
|
+
read_timeout = max(120.0, size / (2 * 1024 * 1024))
|
|
1252
|
+
timeout = httpx.Timeout(connect=30.0, read=read_timeout, write=300.0, pool=30.0)
|
|
1253
|
+
|
|
1254
|
+
def _put(client_put, url: str) -> httpx.Response:
|
|
1255
|
+
with open(file_path, "rb") as fh: # fresh handle per attempt
|
|
1256
|
+
return client_put(url, content=fh, timeout=timeout)
|
|
1160
1257
|
|
|
1161
|
-
|
|
1258
|
+
if presigned_url.startswith("/"):
|
|
1259
|
+
api_path = (
|
|
1260
|
+
presigned_url.replace("/api/v1/", "/", 1)
|
|
1261
|
+
if presigned_url.startswith("/api/v1/") else presigned_url
|
|
1262
|
+
)
|
|
1263
|
+
_raise_for_status(_put(self._client.put, api_path))
|
|
1264
|
+
return
|
|
1265
|
+
try:
|
|
1266
|
+
_raise_for_status(_put(httpx.put, presigned_url))
|
|
1267
|
+
except (httpx.ConnectError, httpx.ConnectTimeout):
|
|
1268
|
+
if not run_id:
|
|
1269
|
+
raise
|
|
1270
|
+
fname = os.path.basename(file_path)
|
|
1271
|
+
_raise_for_status(_put(self._client.put, f"/runs/{run_id}/files/{fname}"))
|
|
1272
|
+
|
|
1273
|
+
def _upload_multipart(self, version_id: str, name: str, file_path: str, size: int) -> bool:
|
|
1274
|
+
"""Multipart upload of one artifact file. Returns False when the server
|
|
1275
|
+
cannot do multipart (old server / proxy-only storage) so the caller
|
|
1276
|
+
falls back to a single PUT; raises on an actual upload failure."""
|
|
1277
|
+
base = f"/artifacts/versions/{version_id}/multipart"
|
|
1278
|
+
start = self._request(
|
|
1279
|
+
"post", f"{base}/start",
|
|
1280
|
+
json={"name": name, "size": size, "part_size": _mp.part_size()},
|
|
1281
|
+
)
|
|
1282
|
+
if start.status_code in (404, 405):
|
|
1283
|
+
detail = ""
|
|
1162
1284
|
try:
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if run_id:
|
|
1169
|
-
fname = os.path.basename(file_path)
|
|
1170
|
-
proxy_resp = self._client.put(
|
|
1171
|
-
f"/runs/{run_id}/files/{fname}", content=data, timeout=upload_timeout
|
|
1172
|
-
)
|
|
1173
|
-
return proxy_resp.status_code == 200
|
|
1285
|
+
detail = str(start.json().get("detail", ""))
|
|
1286
|
+
except Exception: # noqa: BLE001
|
|
1287
|
+
pass
|
|
1288
|
+
if start.status_code == 405 or detail in ("", "Not Found"):
|
|
1289
|
+
logger.info("server has no multipart endpoint; using a single PUT for %s", name)
|
|
1174
1290
|
return False
|
|
1175
|
-
|
|
1176
|
-
|
|
1291
|
+
_raise_for_status(start)
|
|
1292
|
+
info = start.json()
|
|
1293
|
+
if not info.get("multipart"):
|
|
1177
1294
|
return False
|
|
1295
|
+
upload_id = info["upload_id"]
|
|
1296
|
+
logger.info(
|
|
1297
|
+
"uploading %s (%s) in %d parts", name, _fmt_bytes(size), len(info["parts"])
|
|
1298
|
+
)
|
|
1299
|
+
try:
|
|
1300
|
+
etags = _mp.upload_parts(file_path, info["parts"], int(info["part_size"]))
|
|
1301
|
+
done = self._request(
|
|
1302
|
+
"post", f"{base}/complete",
|
|
1303
|
+
json={"name": name, "upload_id": upload_id, "parts": etags},
|
|
1304
|
+
timeout=httpx.Timeout(30.0, read=300.0),
|
|
1305
|
+
)
|
|
1306
|
+
_raise_for_status(done)
|
|
1307
|
+
except Exception:
|
|
1308
|
+
try:
|
|
1309
|
+
self._client.post(f"{base}/abort", json={"name": name, "upload_id": upload_id})
|
|
1310
|
+
except Exception: # noqa: BLE001
|
|
1311
|
+
pass
|
|
1312
|
+
raise
|
|
1313
|
+
return True
|
|
1178
1314
|
|
|
1179
1315
|
def download_file_from_presigned_url(
|
|
1180
1316
|
self, presigned_url: str
|