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.
Files changed (235) hide show
  1. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/PKG-INFO +3 -3
  2. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/CLAUDE.md +1 -3
  3. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/__init__.py +19 -2
  4. openrunner_sdk-2.41.10/openrunner/_multipart.py +116 -0
  5. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/api_client.py +181 -45
  6. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/cli.py +159 -23
  7. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/datasets.py +216 -90
  8. openrunner_sdk-2.41.10/openrunner/errors.py +41 -0
  9. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/install_commands.py +40 -16
  10. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/mcp_server.py +38 -0
  11. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/media.py +5 -0
  12. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/projects.py +51 -0
  13. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/run.py +72 -16
  14. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/pyproject.toml +3 -3
  15. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli.py +4 -1
  16. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_artifact_sweep.py +3 -1
  17. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_infra.py +27 -0
  18. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_datasets_module.py +92 -57
  19. openrunner_sdk-2.41.10/tests/test_large_file_uploads.py +393 -0
  20. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_media_extra.py +15 -0
  21. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_run_module.py +24 -5
  22. openrunner_sdk-2.41.8/openrunner/_native.py +0 -37
  23. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/.gitignore +0 -0
  24. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/=6.0 +0 -0
  25. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/=8.1 +0 -0
  26. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/README.md +0 -0
  27. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/_retry_transport.py +0 -0
  28. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/_safepath.py +0 -0
  29. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/_secret_rules.py +0 -0
  30. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/artifact.py +0 -0
  31. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/autoresearch.py +0 -0
  32. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/buffer.py +0 -0
  33. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/cache.py +0 -0
  34. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/config.py +0 -0
  35. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/cost.py +0 -0
  36. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/dataset.py +0 -0
  37. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/environment.py +0 -0
  38. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/evaluation.py +0 -0
  39. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/feedback.py +0 -0
  40. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/git_info.py +0 -0
  41. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/__init__.py +0 -0
  42. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/api_integration.py +0 -0
  43. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/cost_guard.py +0 -0
  44. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/gpu_types.py +0 -0
  45. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/price_compare.py +0 -0
  46. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/provider_base.py +0 -0
  47. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/__init__.py +0 -0
  48. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/forge_provider.py +0 -0
  49. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/lambda_cloud.py +0 -0
  50. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/modal_provider.py +0 -0
  51. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/ovhcloud.py +0 -0
  52. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/runpod.py +0 -0
  53. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/providers/vastai.py +0 -0
  54. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/runner.py +0 -0
  55. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/gpu/workspace_sync.py +0 -0
  56. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/guardrails.py +0 -0
  57. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/handover.py +0 -0
  58. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/hooks/patent-lab-post-commit +0 -0
  59. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/__init__.py +0 -0
  60. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/accelerate.py +0 -0
  61. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/anthropic_tracer.py +0 -0
  62. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/catboost.py +0 -0
  63. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/diffusers.py +0 -0
  64. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/fastai.py +0 -0
  65. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/forced_alignment.py +0 -0
  66. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/gladia.py +0 -0
  67. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/gymnasium.py +0 -0
  68. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/huggingface.py +0 -0
  69. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/hydra.py +0 -0
  70. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/ignite.py +0 -0
  71. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/jax.py +0 -0
  72. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/keras.py +0 -0
  73. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/langchain.py +0 -0
  74. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/lightgbm.py +0 -0
  75. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/lightning.py +0 -0
  76. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/llamaindex.py +0 -0
  77. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/openai_finetune.py +0 -0
  78. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/openai_tracer.py +0 -0
  79. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/optuna.py +0 -0
  80. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/pytorch.py +0 -0
  81. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/sb3.py +0 -0
  82. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/sklearn.py +0 -0
  83. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/skypilot.py +0 -0
  84. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/tensorflow.py +0 -0
  85. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/trl.py +0 -0
  86. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/tts.py +0 -0
  87. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/ultralytics.py +0 -0
  88. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/voice_agent.py +0 -0
  89. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/whisper.py +0 -0
  90. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/integration/xgboost.py +0 -0
  91. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/launch.py +0 -0
  92. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/llms-full.txt +0 -0
  93. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/llms.py +0 -0
  94. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/llms.txt +0 -0
  95. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/machine.py +0 -0
  96. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/migrate.py +0 -0
  97. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/model.py +0 -0
  98. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/model_card.py +0 -0
  99. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/offline.py +0 -0
  100. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/patent_scan.py +0 -0
  101. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/pii.py +0 -0
  102. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/plot.py +0 -0
  103. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/prompt.py +0 -0
  104. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/query_api.py +0 -0
  105. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/redact.py +0 -0
  106. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/reload.py +0 -0
  107. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/research.py +0 -0
  108. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/research_policy.py +0 -0
  109. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/scorers.py +0 -0
  110. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/sender.py +0 -0
  111. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/session.py +0 -0
  112. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/settings.py +0 -0
  113. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/summary.py +0 -0
  114. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/sweep.py +0 -0
  115. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/sync.py +0 -0
  116. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/system_metrics.py +0 -0
  117. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/tensorboard.py +0 -0
  118. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/tools/__init__.py +0 -0
  119. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/tools/patent_report.py +0 -0
  120. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/trace.py +0 -0
  121. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/transcript_formatter.py +0 -0
  122. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wal.py +0 -0
  123. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wandb_compat/__init__.py +0 -0
  124. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wandb_compat/_shim.py +0 -0
  125. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/openrunner/wer.py +0 -0
  126. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/scripts/gen_betterleaks_rules.py +0 -0
  127. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/__init__.py +0 -0
  128. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/conftest.py +0 -0
  129. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/config.json +0 -0
  130. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/metadata.json +0 -0
  131. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/metrics.jsonl +0 -0
  132. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-aaa/summary.json +0 -0
  133. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/model-checkpoint/manifest.json +0 -0
  134. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/model-checkpoint/weights.bin +0 -0
  135. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/sample-image/manifest.json +0 -0
  136. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/sample-image/preview.png +0 -0
  137. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/config.json +0 -0
  138. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/metadata.json +0 -0
  139. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/metrics.jsonl +0 -0
  140. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/fixtures/wandb-export-mini/run-bbb/summary.json +0 -0
  141. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/integration/__init__.py +0 -0
  142. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/integration/conftest.py +0 -0
  143. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/integration/test_artifact_round_trip.py +0 -0
  144. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_alert.py +0 -0
  145. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_aliases.py +0 -0
  146. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_api_client.py +0 -0
  147. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_api_client_methods.py +0 -0
  148. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_api_client_transport.py +0 -0
  149. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_artifact.py +0 -0
  150. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_buffer.py +0 -0
  151. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cache.py +0 -0
  152. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_class_scorers.py +0 -0
  153. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_datasets.py +0 -0
  154. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_gpu_registry_deploy.py +0 -0
  155. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_remainder.py +0 -0
  156. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_cli_topup.py +0 -0
  157. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_config.py +0 -0
  158. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_evaluation.py +0 -0
  159. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_finish.py +0 -0
  160. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_git_info.py +0 -0
  161. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/__init__.py +0 -0
  162. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/conftest.py +0 -0
  163. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_cost_guard.py +0 -0
  164. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_engine_serverside.py +0 -0
  165. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_engine_stream_keepalive.py +0 -0
  166. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_forge.py +0 -0
  167. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_governed_jobs.py +0 -0
  168. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_gpu_cluster.py +0 -0
  169. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_gpu_types.py +0 -0
  170. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_lambda.py +0 -0
  171. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_modal.py +0 -0
  172. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_ovh_training.py +0 -0
  173. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_ovhcloud.py +0 -0
  174. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_price_compare.py +0 -0
  175. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_provider_base.py +0 -0
  176. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_runner.py +0 -0
  177. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_runpod.py +0 -0
  178. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_sdk_gpu_api.py +0 -0
  179. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_vastai.py +0 -0
  180. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu/test_workspace_sync.py +0 -0
  181. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_gpu_modal_creds.py +0 -0
  182. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_init.py +0 -0
  183. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_init_facade.py +0 -0
  184. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_install_commands.py +0 -0
  185. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_fastai.py +0 -0
  186. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_huggingface.py +0 -0
  187. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_keras.py +0 -0
  188. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_langchain.py +0 -0
  189. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_lightning.py +0 -0
  190. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_pytorch.py +0 -0
  191. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_setA.py +0 -0
  192. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_setB.py +0 -0
  193. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_setC.py +0 -0
  194. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_sklearn.py +0 -0
  195. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_integration_xgboost.py +0 -0
  196. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_launch.py +0 -0
  197. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_llms.py +0 -0
  198. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_log.py +0 -0
  199. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_log_code.py +0 -0
  200. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_machine_migrate.py +0 -0
  201. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_decisions.py +0 -0
  202. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_monitor.py +0 -0
  203. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_predictions.py +0 -0
  204. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_mcp_server.py +0 -0
  205. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_media.py +0 -0
  206. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_migrate.py +0 -0
  207. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_offline.py +0 -0
  208. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_offline_sync.py +0 -0
  209. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_patent_scan.py +0 -0
  210. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_pii.py +0 -0
  211. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_plot.py +0 -0
  212. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_plot_media_extra2.py +0 -0
  213. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_prompt_module.py +0 -0
  214. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_pure_modules.py +0 -0
  215. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_query_api.py +0 -0
  216. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_redact.py +0 -0
  217. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_reload_module.py +0 -0
  218. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_research_module.py +0 -0
  219. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_resume.py +0 -0
  220. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_retry_transport.py +0 -0
  221. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j1.py +0 -0
  222. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j2.py +0 -0
  223. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j3.py +0 -0
  224. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_cluster_j4.py +0 -0
  225. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_core_edges.py +0 -0
  226. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sdk_features.py +0 -0
  227. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sender.py +0 -0
  228. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_session_module.py +0 -0
  229. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_session_sync.py +0 -0
  230. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_summary.py +0 -0
  231. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_sweep.py +0 -0
  232. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_system_metrics.py +0 -0
  233. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_topup_hi.py +0 -0
  234. {openrunner_sdk-2.41.8 → openrunner_sdk-2.41.10}/tests/test_trace.py +0 -0
  235. {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.8
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<13.0,>=10.0
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<13.0,>=10.0; extra == 'dev'
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. Never raises.
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
- response = self._request("post",f"/runs/{run_id}/artifacts", json=body)
811
- response.raise_for_status()
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.raise_for_status()
829
- return response.json()
830
- except Exception as e:
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, presigned_url: str, file_path: str, run_id: str = ""
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
- """PUT raw file bytes to a presigned URL.
1135
-
1136
- Handles three cases:
1137
- 1. Relative proxy URL (/api/v1/runs/...) — use authenticated client
1138
- 2. Absolute URL — try direct PUT, fall back to proxy on ConnectError
1139
- 3. ConnectError fallback — PUT via /runs/{run_id}/files/{name}
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
- file_size = os.path.getsize(file_path)
1145
- # Use streaming for large files, read into memory for small ones
1146
- if file_size > 50 * 1024 * 1024: # 50 MB
1147
- data = open(file_path, "rb") # noqa: SIM115
1148
- else:
1149
- with open(file_path, "rb") as f:
1150
- data = f.read()
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
- # Scale timeout by file size (minimum 60s, ~30s per 100MB)
1153
- upload_timeout = max(60.0, file_size / (3 * 1024 * 1024))
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
- # Case 1: relative proxy URL from server
1156
- if presigned_url.startswith("/"):
1157
- api_path = presigned_url.replace("/api/v1/", "/", 1) if presigned_url.startswith("/api/v1/") else presigned_url
1158
- resp = self._client.put(api_path, content=data, timeout=upload_timeout)
1159
- return resp.status_code == 200
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
- # Case 2: absolute presigned URL (direct to S3)
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
- resp = httpx.put(presigned_url, content=data, timeout=upload_timeout)
1164
- resp.raise_for_status()
1165
- return True
1166
- except (httpx.ConnectError, httpx.ConnectTimeout):
1167
- # Case 3: fallback to API proxy
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
- except Exception as e:
1176
- logger.warning("upload_file_to_presigned_url failed: %s", e)
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