openrunner-sdk 2.41.7__tar.gz → 2.41.9__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 (236) hide show
  1. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/.gitignore +2 -0
  2. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/PKG-INFO +8 -8
  3. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/README.md +1 -1
  4. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/__init__.py +36 -5
  5. openrunner_sdk-2.41.9/openrunner/_multipart.py +116 -0
  6. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/api_client.py +181 -45
  7. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/artifact.py +49 -0
  8. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/cli.py +280 -54
  9. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/datasets.py +345 -24
  10. openrunner_sdk-2.41.9/openrunner/errors.py +41 -0
  11. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/api_integration.py +29 -3
  12. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/runner.py +14 -2
  13. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/install_commands.py +358 -18
  14. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/mcp_server.py +38 -0
  15. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/media.py +5 -0
  16. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/migrate.py +29 -7
  17. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/projects.py +51 -0
  18. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/run.py +94 -19
  19. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/pyproject.toml +6 -6
  20. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/integration/test_artifact_round_trip.py +1 -1
  21. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_artifact.py +59 -0
  22. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cli.py +4 -1
  23. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cli_artifact_sweep.py +3 -1
  24. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cli_infra.py +27 -0
  25. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_datasets_module.py +164 -2
  26. openrunner_sdk-2.41.9/tests/test_gpu/test_engine_serverside.py +62 -0
  27. openrunner_sdk-2.41.9/tests/test_gpu/test_engine_stream_keepalive.py +61 -0
  28. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_init_facade.py +8 -2
  29. openrunner_sdk-2.41.9/tests/test_large_file_uploads.py +393 -0
  30. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_machine_migrate.py +47 -3
  31. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_media_extra.py +15 -0
  32. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_run_module.py +64 -5
  33. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sdk_cluster_j3.py +5 -1
  34. openrunner_sdk-2.41.7/tests/test_gpu/test_engine_serverside.py +0 -30
  35. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/=6.0 +0 -0
  36. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/=8.1 +0 -0
  37. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/CLAUDE.md +0 -0
  38. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/_native.py +0 -0
  39. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/_retry_transport.py +0 -0
  40. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/_safepath.py +0 -0
  41. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/_secret_rules.py +0 -0
  42. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/autoresearch.py +0 -0
  43. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/buffer.py +0 -0
  44. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/cache.py +0 -0
  45. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/config.py +0 -0
  46. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/cost.py +0 -0
  47. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/dataset.py +0 -0
  48. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/environment.py +0 -0
  49. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/evaluation.py +0 -0
  50. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/feedback.py +0 -0
  51. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/git_info.py +0 -0
  52. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/__init__.py +0 -0
  53. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/cost_guard.py +0 -0
  54. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/gpu_types.py +0 -0
  55. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/price_compare.py +0 -0
  56. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/provider_base.py +0 -0
  57. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/providers/__init__.py +0 -0
  58. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/providers/forge_provider.py +0 -0
  59. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/providers/lambda_cloud.py +0 -0
  60. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/providers/modal_provider.py +0 -0
  61. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/providers/ovhcloud.py +0 -0
  62. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/providers/runpod.py +0 -0
  63. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/providers/vastai.py +0 -0
  64. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/gpu/workspace_sync.py +0 -0
  65. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/guardrails.py +0 -0
  66. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/handover.py +0 -0
  67. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/hooks/patent-lab-post-commit +0 -0
  68. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/__init__.py +0 -0
  69. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/accelerate.py +0 -0
  70. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/anthropic_tracer.py +0 -0
  71. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/catboost.py +0 -0
  72. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/diffusers.py +0 -0
  73. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/fastai.py +0 -0
  74. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/forced_alignment.py +0 -0
  75. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/gladia.py +0 -0
  76. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/gymnasium.py +0 -0
  77. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/huggingface.py +0 -0
  78. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/hydra.py +0 -0
  79. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/ignite.py +0 -0
  80. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/jax.py +0 -0
  81. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/keras.py +0 -0
  82. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/langchain.py +0 -0
  83. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/lightgbm.py +0 -0
  84. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/lightning.py +0 -0
  85. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/llamaindex.py +0 -0
  86. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/openai_finetune.py +0 -0
  87. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/openai_tracer.py +0 -0
  88. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/optuna.py +0 -0
  89. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/pytorch.py +0 -0
  90. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/sb3.py +0 -0
  91. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/sklearn.py +0 -0
  92. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/skypilot.py +0 -0
  93. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/tensorflow.py +0 -0
  94. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/trl.py +0 -0
  95. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/tts.py +0 -0
  96. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/ultralytics.py +0 -0
  97. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/voice_agent.py +0 -0
  98. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/whisper.py +0 -0
  99. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/integration/xgboost.py +0 -0
  100. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/launch.py +0 -0
  101. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/llms-full.txt +0 -0
  102. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/llms.py +0 -0
  103. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/llms.txt +0 -0
  104. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/machine.py +0 -0
  105. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/model.py +0 -0
  106. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/model_card.py +0 -0
  107. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/offline.py +0 -0
  108. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/patent_scan.py +0 -0
  109. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/pii.py +0 -0
  110. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/plot.py +0 -0
  111. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/prompt.py +0 -0
  112. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/query_api.py +0 -0
  113. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/redact.py +0 -0
  114. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/reload.py +0 -0
  115. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/research.py +0 -0
  116. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/research_policy.py +0 -0
  117. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/scorers.py +0 -0
  118. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/sender.py +0 -0
  119. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/session.py +0 -0
  120. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/settings.py +0 -0
  121. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/summary.py +0 -0
  122. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/sweep.py +0 -0
  123. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/sync.py +0 -0
  124. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/system_metrics.py +0 -0
  125. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/tensorboard.py +0 -0
  126. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/tools/__init__.py +0 -0
  127. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/tools/patent_report.py +0 -0
  128. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/trace.py +0 -0
  129. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/transcript_formatter.py +0 -0
  130. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/wal.py +0 -0
  131. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/wandb_compat/__init__.py +0 -0
  132. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/wandb_compat/_shim.py +0 -0
  133. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/openrunner/wer.py +0 -0
  134. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/scripts/gen_betterleaks_rules.py +0 -0
  135. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/__init__.py +0 -0
  136. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/conftest.py +0 -0
  137. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-aaa/config.json +0 -0
  138. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-aaa/metadata.json +0 -0
  139. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-aaa/metrics.jsonl +0 -0
  140. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-aaa/summary.json +0 -0
  141. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/model-checkpoint/manifest.json +0 -0
  142. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/model-checkpoint/weights.bin +0 -0
  143. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/sample-image/manifest.json +0 -0
  144. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/artifacts/sample-image/preview.png +0 -0
  145. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/config.json +0 -0
  146. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/metadata.json +0 -0
  147. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/metrics.jsonl +0 -0
  148. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/fixtures/wandb-export-mini/run-bbb/summary.json +0 -0
  149. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/integration/__init__.py +0 -0
  150. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/integration/conftest.py +0 -0
  151. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_alert.py +0 -0
  152. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_aliases.py +0 -0
  153. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_api_client.py +0 -0
  154. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_api_client_methods.py +0 -0
  155. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_api_client_transport.py +0 -0
  156. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_buffer.py +0 -0
  157. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cache.py +0 -0
  158. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_class_scorers.py +0 -0
  159. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cli_datasets.py +0 -0
  160. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cli_gpu_registry_deploy.py +0 -0
  161. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cli_remainder.py +0 -0
  162. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_cli_topup.py +0 -0
  163. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_config.py +0 -0
  164. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_evaluation.py +0 -0
  165. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_finish.py +0 -0
  166. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_git_info.py +0 -0
  167. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/__init__.py +0 -0
  168. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/conftest.py +0 -0
  169. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_cost_guard.py +0 -0
  170. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_forge.py +0 -0
  171. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_governed_jobs.py +0 -0
  172. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_gpu_cluster.py +0 -0
  173. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_gpu_types.py +0 -0
  174. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_lambda.py +0 -0
  175. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_modal.py +0 -0
  176. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_ovh_training.py +0 -0
  177. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_ovhcloud.py +0 -0
  178. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_price_compare.py +0 -0
  179. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_provider_base.py +0 -0
  180. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_runner.py +0 -0
  181. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_runpod.py +0 -0
  182. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_sdk_gpu_api.py +0 -0
  183. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_vastai.py +0 -0
  184. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu/test_workspace_sync.py +0 -0
  185. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_gpu_modal_creds.py +0 -0
  186. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_init.py +0 -0
  187. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_install_commands.py +0 -0
  188. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_fastai.py +0 -0
  189. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_huggingface.py +0 -0
  190. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_keras.py +0 -0
  191. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_langchain.py +0 -0
  192. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_lightning.py +0 -0
  193. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_pytorch.py +0 -0
  194. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_setA.py +0 -0
  195. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_setB.py +0 -0
  196. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_setC.py +0 -0
  197. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_sklearn.py +0 -0
  198. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_integration_xgboost.py +0 -0
  199. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_launch.py +0 -0
  200. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_llms.py +0 -0
  201. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_log.py +0 -0
  202. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_log_code.py +0 -0
  203. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_mcp_decisions.py +0 -0
  204. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_mcp_monitor.py +0 -0
  205. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_mcp_predictions.py +0 -0
  206. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_mcp_server.py +0 -0
  207. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_media.py +0 -0
  208. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_migrate.py +0 -0
  209. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_offline.py +0 -0
  210. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_offline_sync.py +0 -0
  211. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_patent_scan.py +0 -0
  212. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_pii.py +0 -0
  213. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_plot.py +0 -0
  214. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_plot_media_extra2.py +0 -0
  215. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_prompt_module.py +0 -0
  216. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_pure_modules.py +0 -0
  217. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_query_api.py +0 -0
  218. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_redact.py +0 -0
  219. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_reload_module.py +0 -0
  220. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_research_module.py +0 -0
  221. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_resume.py +0 -0
  222. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_retry_transport.py +0 -0
  223. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sdk_cluster_j1.py +0 -0
  224. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sdk_cluster_j2.py +0 -0
  225. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sdk_cluster_j4.py +0 -0
  226. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sdk_core_edges.py +0 -0
  227. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sdk_features.py +0 -0
  228. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sender.py +0 -0
  229. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_session_module.py +0 -0
  230. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_session_sync.py +0 -0
  231. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_summary.py +0 -0
  232. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_sweep.py +0 -0
  233. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_system_metrics.py +0 -0
  234. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_topup_hi.py +0 -0
  235. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_trace.py +0 -0
  236. {openrunner_sdk-2.41.7 → openrunner_sdk-2.41.9}/tests/test_wandb_compat.py +0 -0
@@ -153,6 +153,8 @@ activemq-data/
153
153
  # Environments
154
154
  .env
155
155
  .envrc
156
+ .forge.env
157
+ .forge-venv/
156
158
 
157
159
  # Docker secrets (real values — only examples tracked)
158
160
  secrets/
@@ -1,10 +1,10 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: openrunner-sdk
3
- Version: 2.41.7
3
+ Version: 2.41.9
4
4
  Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
5
- Project-URL: Homepage, https://github.com/jqueguiner/openrunner
6
- Project-URL: Repository, https://github.com/jqueguiner/openrunner
7
- Project-URL: Issues, https://github.com/jqueguiner/openrunner/issues
5
+ Project-URL: Homepage, https://github.com/gladiaio/openrunner
6
+ Project-URL: Repository, https://github.com/gladiaio/openrunner
7
+ Project-URL: Issues, https://github.com/gladiaio/openrunner/issues
8
8
  Author-email: JL Queguiner <jl@gladia.io>
9
9
  License: MIT
10
10
  Keywords: experiment-tracking,invention-disclosure,machine-learning,ml,mlops,patent,wandb
@@ -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'
@@ -656,7 +656,7 @@ W&B env vars (`WANDB_API_KEY`, `WANDB_BASE_URL`) are also supported as fallback
656
656
 
657
657
  ## Self-Hosting
658
658
 
659
- OpenRunner is designed to be self-hosted. See the [main repo](https://github.com/jqueguiner/openrunner) for server setup with Docker Compose.
659
+ OpenRunner is designed to be self-hosted. See the [main repo](https://github.com/gladiaio/openrunner) for server setup with Docker Compose.
660
660
 
661
661
  ## License
662
662
 
@@ -541,7 +541,7 @@ W&B env vars (`WANDB_API_KEY`, `WANDB_BASE_URL`) are also supported as fallback
541
541
 
542
542
  ## Self-Hosting
543
543
 
544
- OpenRunner is designed to be self-hosted. See the [main repo](https://github.com/jqueguiner/openrunner) for server setup with Docker Compose.
544
+ OpenRunner is designed to be self-hosted. See the [main repo](https://github.com/gladiaio/openrunner) for server setup with Docker Compose.
545
545
 
546
546
  ## License
547
547
 
@@ -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,
@@ -93,6 +94,7 @@ import openrunner.datasets as datasets
93
94
  from openrunner.datasets import (
94
95
  DatasetInfo,
95
96
  DatasetsClient,
97
+ DeriveIncomplete,
96
98
  create_dataset,
97
99
  dataset_info,
98
100
  index_dataset,
@@ -101,6 +103,7 @@ from openrunner.datasets import (
101
103
  link_project,
102
104
  load_dataset,
103
105
  add_derivation,
106
+ derive_by_ids,
104
107
  derive_by_query,
105
108
  derived_rows,
106
109
  keyword_expand,
@@ -164,7 +167,7 @@ from openrunner.reload import (
164
167
  dataset_handle,
165
168
  )
166
169
  from openrunner.sync import sync_environment, scan_environment
167
- from openrunner.projects import create_project
170
+ from openrunner.projects import create_project, rename_project
168
171
  from openrunner.run import Run
169
172
  from openrunner.settings import SDKSettings, resolve_project, pin_project
170
173
  from openrunner.summary import Summary
@@ -852,11 +855,24 @@ def alert(
852
855
  return None
853
856
 
854
857
 
855
- def log_artifact(artifact: Artifact) -> dict[str, Any] | None:
856
- """Upload an artifact to the active run. Never raises.
858
+ def log_artifact(
859
+ artifact: "Artifact | str | Path",
860
+ *,
861
+ name: str | None = None,
862
+ type: str | None = None,
863
+ aliases: list[str] | None = None,
864
+ ) -> dict[str, Any] | None:
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.
857
869
 
858
870
  Args:
859
- artifact: The Artifact object with files added via add_file/add_dir.
871
+ artifact: An Artifact with files added via add_file/add_dir, OR a path
872
+ to a file or directory (W&B-style), built into an Artifact for you.
873
+ name: Artifact name when ``artifact`` is a path.
874
+ type: Advisory type when ``artifact`` is a path.
875
+ aliases: Aliases to set on the created version.
860
876
 
861
877
  Returns:
862
878
  Server response dict with version info, or None on failure.
@@ -865,7 +881,12 @@ def log_artifact(artifact: Artifact) -> dict[str, Any] | None:
865
881
  if _active_run is None:
866
882
  logger.warning("openrunner.log_artifact(): no active run")
867
883
  return None
868
- return _active_run.log_artifact(artifact)
884
+ return _active_run.log_artifact(
885
+ artifact, name=name, type=type, aliases=aliases
886
+ )
887
+ except ArtifactUploadError:
888
+ # An upload failure must reach the caller (see OPENRUNNER_ARTIFACT_STRICT).
889
+ raise
869
890
  except Exception as e:
870
891
  logger.warning("openrunner.log_artifact() failed: %s", e)
871
892
  return None
@@ -889,6 +910,8 @@ def finish_artifact(artifact: Artifact) -> dict[str, Any] | None:
889
910
  logger.warning("openrunner.finish_artifact(): no active run")
890
911
  return None
891
912
  return _active_run.finish_artifact(artifact)
913
+ except ArtifactUploadError:
914
+ raise
892
915
  except Exception as e:
893
916
  logger.warning("openrunner.finish_artifact() failed: %s", e)
894
917
  return None
@@ -914,6 +937,8 @@ def link_artifact(
914
937
  logger.warning("openrunner.link_artifact(): no active run")
915
938
  return None
916
939
  return _active_run.link_artifact(artifact, aliases=aliases)
940
+ except ArtifactUploadError:
941
+ raise
917
942
  except Exception as e:
918
943
  logger.warning("openrunner.link_artifact() failed: %s", e)
919
944
  return None
@@ -1065,6 +1090,10 @@ def push(
1065
1090
  Returns:
1066
1091
  The created version info dict, or None on failure.
1067
1092
 
1093
+ Raises:
1094
+ ArtifactUploadError: when any file fails to upload or the version
1095
+ cannot be confirmed (unless ``OPENRUNNER_ARTIFACT_STRICT=0``).
1096
+
1068
1097
  Example::
1069
1098
 
1070
1099
  openrunner.push("model.safetensors", "gladia/my-project/my-model",
@@ -1089,6 +1118,8 @@ def push(
1089
1118
  except Exception as e: # noqa: BLE001
1090
1119
  logger.warning("openrunner.push(): set_model_card failed: %s", e)
1091
1120
  return result
1121
+ except ArtifactUploadError:
1122
+ raise
1092
1123
  except Exception as e: # noqa: BLE001
1093
1124
  logger.warning("openrunner.push() failed: %s", e)
1094
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
@@ -29,6 +29,55 @@ def _sha256_file(path: Path, chunk_size: int = 8192) -> str:
29
29
  return h.hexdigest()
30
30
 
31
31
 
32
+ def coerce_artifact(
33
+ source: "Artifact | str | Path",
34
+ *,
35
+ name: str | None = None,
36
+ type: str | None = None,
37
+ description: str | None = None,
38
+ metadata: dict[str, Any] | None = None,
39
+ ) -> "Artifact":
40
+ """Return an Artifact, building one from a path if that's what was given.
41
+
42
+ ``log_artifact`` historically took a fully built ``Artifact``. Users coming
43
+ from W&B -- which this repo actively courts, see
44
+ ``examples/migrate-from-wandb/`` -- write the path form instead::
45
+
46
+ run.log_artifact("out/final", name="my-model", type="model")
47
+
48
+ and used to get ``TypeError: unexpected keyword argument 'name'``. Accept
49
+ both. A directory becomes ``add_dir``, a file ``add_file``; ``name``
50
+ defaults to the path's own name so the common case needs no kwargs.
51
+ """
52
+ if isinstance(source, Artifact):
53
+ # Already built: kwargs only fill gaps, they never silently override
54
+ # what the caller put on the object.
55
+ if name and not source.name:
56
+ source.name = name
57
+ if type and source.type is None:
58
+ source.type = type
59
+ return source
60
+
61
+ path = Path(source)
62
+ if not path.exists():
63
+ raise FileNotFoundError(f"artifact path does not exist: {path}")
64
+ art = Artifact(
65
+ name or path.name or path.stem,
66
+ type,
67
+ description,
68
+ metadata,
69
+ )
70
+ if path.is_dir():
71
+ # Existing add_dir semantics: files are prefixed with the directory
72
+ # name. Kept as-is so a coerced artifact lays out identically to a
73
+ # hand-built one -- changing it would silently move every existing
74
+ # caller's files.
75
+ art.add_dir(str(path))
76
+ else:
77
+ art.add_file(str(path))
78
+ return art
79
+
80
+
32
81
  class Artifact:
33
82
  """An untyped, versioned bucket of files for experiment tracking.
34
83