openrunner-sdk 2.2.0__tar.gz → 2.4.0__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 (114) hide show
  1. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/PKG-INFO +1 -1
  2. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/api_client.py +106 -10
  3. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/run.py +13 -3
  4. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/sender.py +8 -4
  5. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/pyproject.toml +1 -1
  6. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_alert.py +1 -1
  7. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_sklearn.py +7 -7
  8. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_media.py +12 -4
  9. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_plot.py +7 -0
  10. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_system_metrics.py +2 -2
  11. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/.gitignore +0 -0
  12. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/=6.0 +0 -0
  13. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/=8.1 +0 -0
  14. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/README.md +0 -0
  15. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/__init__.py +0 -0
  16. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/artifact.py +0 -0
  17. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/buffer.py +0 -0
  18. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/cache.py +0 -0
  19. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/cli.py +0 -0
  20. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/config.py +0 -0
  21. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/cost.py +0 -0
  22. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/dataset.py +0 -0
  23. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/environment.py +0 -0
  24. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/evaluation.py +0 -0
  25. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/feedback.py +0 -0
  26. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/git_info.py +0 -0
  27. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/guardrails.py +0 -0
  28. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/__init__.py +0 -0
  29. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/accelerate.py +0 -0
  30. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/anthropic_tracer.py +0 -0
  31. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/catboost.py +0 -0
  32. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/diffusers.py +0 -0
  33. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/fastai.py +0 -0
  34. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/forced_alignment.py +0 -0
  35. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/gladia.py +0 -0
  36. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/gymnasium.py +0 -0
  37. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/huggingface.py +0 -0
  38. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/hydra.py +0 -0
  39. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/ignite.py +0 -0
  40. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/jax.py +0 -0
  41. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/keras.py +0 -0
  42. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/langchain.py +0 -0
  43. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/lightgbm.py +0 -0
  44. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/lightning.py +0 -0
  45. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/llamaindex.py +0 -0
  46. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/openai_finetune.py +0 -0
  47. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/openai_tracer.py +0 -0
  48. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/optuna.py +0 -0
  49. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/pytorch.py +0 -0
  50. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/sb3.py +0 -0
  51. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/sklearn.py +0 -0
  52. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/tensorflow.py +0 -0
  53. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/trl.py +0 -0
  54. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/tts.py +0 -0
  55. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/ultralytics.py +0 -0
  56. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/voice_agent.py +0 -0
  57. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/whisper.py +0 -0
  58. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/integration/xgboost.py +0 -0
  59. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/launch.py +0 -0
  60. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/media.py +0 -0
  61. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/migrate.py +0 -0
  62. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/model.py +0 -0
  63. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/offline.py +0 -0
  64. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/pii.py +0 -0
  65. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/plot.py +0 -0
  66. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/prompt.py +0 -0
  67. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/query_api.py +0 -0
  68. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/scorers.py +0 -0
  69. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/settings.py +0 -0
  70. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/summary.py +0 -0
  71. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/sweep.py +0 -0
  72. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/system_metrics.py +0 -0
  73. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/tensorboard.py +0 -0
  74. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/trace.py +0 -0
  75. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/transcript_formatter.py +0 -0
  76. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/wal.py +0 -0
  77. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/wandb_compat/__init__.py +0 -0
  78. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/openrunner/wandb_compat/_shim.py +0 -0
  79. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/__init__.py +0 -0
  80. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/conftest.py +0 -0
  81. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_aliases.py +0 -0
  82. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_api_client.py +0 -0
  83. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_artifact.py +0 -0
  84. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_buffer.py +0 -0
  85. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_cache.py +0 -0
  86. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_class_scorers.py +0 -0
  87. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_cli.py +0 -0
  88. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_config.py +0 -0
  89. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_evaluation.py +0 -0
  90. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_finish.py +0 -0
  91. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_git_info.py +0 -0
  92. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_init.py +0 -0
  93. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_fastai.py +0 -0
  94. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_huggingface.py +0 -0
  95. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_keras.py +0 -0
  96. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_langchain.py +0 -0
  97. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_lightning.py +0 -0
  98. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_pytorch.py +0 -0
  99. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_integration_xgboost.py +0 -0
  100. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_launch.py +0 -0
  101. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_log.py +0 -0
  102. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_log_code.py +0 -0
  103. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_migrate.py +0 -0
  104. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_offline.py +0 -0
  105. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_offline_sync.py +0 -0
  106. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_pii.py +0 -0
  107. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_query_api.py +0 -0
  108. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_resume.py +0 -0
  109. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_sdk_features.py +0 -0
  110. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_sender.py +0 -0
  111. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_summary.py +0 -0
  112. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_sweep.py +0 -0
  113. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_trace.py +0 -0
  114. {openrunner_sdk-2.2.0 → openrunner_sdk-2.4.0}/tests/test_wandb_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrunner-sdk
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
5
5
  Project-URL: Homepage, https://github.com/jqueguiner/openrunner
6
6
  Project-URL: Repository, https://github.com/jqueguiner/openrunner
@@ -490,6 +490,86 @@ class APIClient:
490
490
  logger.warning("use_artifact failed: %s", e)
491
491
  return None
492
492
 
493
+ def download_artifact(
494
+ self,
495
+ run_id: str,
496
+ artifact_name: str,
497
+ dest_dir: str = ".",
498
+ version: int | None = None,
499
+ alias: str | None = None,
500
+ ) -> str | None:
501
+ """Download all files from an artifact version to a local directory.
502
+
503
+ Args:
504
+ run_id: Run ID that used/created the artifact
505
+ artifact_name: Artifact name (or "name:alias")
506
+ dest_dir: Local directory to save files (created if needed)
507
+ version: Specific version number (optional)
508
+ alias: Alias name like "latest", "best" (optional)
509
+
510
+ Returns:
511
+ Path to the download directory, or None on failure.
512
+
513
+ Example:
514
+ path = client.download_artifact(run_id, "model-checkpoint", "./models")
515
+ # Files saved to ./models/model-checkpoint/v3/...
516
+ """
517
+ import os
518
+ from pathlib import Path
519
+
520
+ info = self.use_artifact(run_id, artifact_name, version=version, alias=alias)
521
+ if not info:
522
+ logger.warning("download_artifact: use_artifact returned no data")
523
+ return None
524
+
525
+ # Extract version info and files
526
+ ver = info.get("version", version or 1)
527
+ files = info.get("files", [])
528
+ if not files:
529
+ logger.warning("download_artifact: no files in artifact")
530
+ return None
531
+
532
+ # Create destination directory
533
+ name_clean = artifact_name.split(":")[0].replace("/", "_")
534
+ out_dir = Path(dest_dir) / name_clean / f"v{ver}"
535
+ out_dir.mkdir(parents=True, exist_ok=True)
536
+
537
+ downloaded = 0
538
+ for f in files:
539
+ url = f.get("presigned_url") or f.get("url") or f.get("download_url")
540
+ fname = f.get("name") or f.get("path") or f"file_{downloaded}"
541
+ if not url:
542
+ continue
543
+
544
+ # Try presigned URL first, fall back to proxy
545
+ data = self.download_file_from_presigned_url(url)
546
+ if data is None and "localhost" in url:
547
+ # Try proxy
548
+ key = f.get("storage_key") or f.get("key")
549
+ if key:
550
+ try:
551
+ resp = self._request("GET", f"/storage/download?key={key}")
552
+ if resp.status_code == 200:
553
+ data = resp.content
554
+ except Exception:
555
+ pass
556
+
557
+ if data:
558
+ file_path = out_dir / fname
559
+ file_path.parent.mkdir(parents=True, exist_ok=True)
560
+ file_path.write_bytes(data)
561
+ downloaded += 1
562
+ logger.info("downloaded: %s (%d bytes)", fname, len(data))
563
+ else:
564
+ logger.warning("failed to download: %s", fname)
565
+
566
+ if downloaded == 0:
567
+ logger.warning("download_artifact: no files downloaded")
568
+ return None
569
+
570
+ logger.info("artifact downloaded: %d files → %s", downloaded, out_dir)
571
+ return str(out_dir)
572
+
493
573
  def set_alias(
494
574
  self,
495
575
  artifact_id: str,
@@ -684,21 +764,37 @@ class APIClient:
684
764
  presigned_url: str,
685
765
  data_bytes: bytes,
686
766
  content_type: str = "image/png",
767
+ run_id: str = "",
768
+ key: str = "",
687
769
  ) -> bool:
688
770
  """PUT bytes to a presigned URL with Content-Type header.
689
771
 
690
- Uses httpx directly (not self._client) because presigned URLs
691
- are absolute and should not have a base_url prefix.
772
+ Falls back to API proxy upload if presigned URL is unreachable
773
+ (e.g., MinIO not exposed publicly).
692
774
  """
693
775
  try:
694
- resp = httpx.put(
695
- presigned_url,
696
- content=data_bytes,
697
- headers={"Content-Type": content_type},
698
- timeout=300.0,
699
- )
700
- resp.raise_for_status()
701
- return True
776
+ try:
777
+ resp = httpx.put(
778
+ presigned_url,
779
+ content=data_bytes,
780
+ headers={"Content-Type": content_type},
781
+ timeout=300.0,
782
+ )
783
+ resp.raise_for_status()
784
+ return True
785
+ except (httpx.ConnectError, httpx.ConnectTimeout):
786
+ # Presigned URL unreachable — use API proxy
787
+ if run_id and key:
788
+ import os
789
+ fname = os.path.basename(key) or key.replace("/", "_")
790
+ proxy_resp = self._request(
791
+ "PUT",
792
+ f"/runs/{run_id}/files/media/{fname}",
793
+ content=data_bytes,
794
+ headers={"Content-Type": content_type},
795
+ )
796
+ return proxy_resp.status_code == 200
797
+ return False
702
798
  except Exception as e:
703
799
  logger.warning("upload_media_bytes failed: %s", e)
704
800
  return False
@@ -1154,18 +1154,28 @@ class Run:
1154
1154
  name: str,
1155
1155
  version: int | None = None,
1156
1156
  alias: str | None = None,
1157
- ):
1158
- """Convenience: download a model artifact.
1157
+ dest_dir: str = "./artifacts",
1158
+ ) -> str | None:
1159
+ """Download a model artifact to disk.
1159
1160
 
1160
1161
  Args:
1161
1162
  name: Model artifact name (supports "name:alias" syntax).
1162
1163
  version: Specific version number, or None for latest.
1163
1164
  alias: Alias name to resolve.
1165
+ dest_dir: Local directory for downloaded files.
1164
1166
 
1165
1167
  Returns:
1166
1168
  Path to the local artifact directory, or None on failure.
1167
1169
  """
1168
- return self.use_artifact(name, version=version, alias=alias)
1170
+ if not self._client:
1171
+ return None
1172
+ return self._client.download_artifact(
1173
+ run_id=self._run_id,
1174
+ artifact_name=name,
1175
+ dest_dir=dest_dir,
1176
+ version=version,
1177
+ alias=alias,
1178
+ )
1169
1179
 
1170
1180
  def link_model(
1171
1181
  self,
@@ -265,7 +265,8 @@ class Sender:
265
265
  if result and result.get("presigned_url"):
266
266
  # Upload the bytes to the presigned URL
267
267
  self._client.upload_media_bytes(
268
- result["presigned_url"], img_bytes, content_type
268
+ result["presigned_url"], img_bytes, content_type,
269
+ run_id=self._run_id, key=item["key"],
269
270
  )
270
271
 
271
272
  elif media_type == "table":
@@ -293,7 +294,8 @@ class Sender:
293
294
  )
294
295
  if result and result.get("presigned_url"):
295
296
  self._client.upload_media_bytes(
296
- result["presigned_url"], audio_bytes, content_type
297
+ result["presigned_url"], audio_bytes, content_type,
298
+ run_id=self._run_id, key=item["key"],
297
299
  )
298
300
 
299
301
  elif media_type == "video":
@@ -311,7 +313,8 @@ class Sender:
311
313
  )
312
314
  if result and result.get("presigned_url"):
313
315
  self._client.upload_media_bytes(
314
- result["presigned_url"], video_bytes, content_type
316
+ result["presigned_url"], video_bytes, content_type,
317
+ run_id=self._run_id, key=item["key"],
315
318
  )
316
319
 
317
320
  elif media_type == "histogram":
@@ -408,7 +411,8 @@ class Sender:
408
411
  )
409
412
  if result and result.get("presigned_url"):
410
413
  self._client.upload_media_bytes(
411
- result["presigned_url"], img_bytes, content_type
414
+ result["presigned_url"], img_bytes, content_type,
415
+ run_id=self._run_id, key=item["key"],
412
416
  )
413
417
 
414
418
  elif media_type == "molecule":
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openrunner-sdk"
3
- version = "2.2.0"
3
+ version = "2.4.0"
4
4
  description = "OpenRunner SDK - W&B-compatible ML experiment tracking client"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -192,7 +192,7 @@ class TestModuleLevelAlert:
192
192
  result = openrunner.alert("Title", text="Body", level="WARN")
193
193
  assert result == {"id": "x"}
194
194
  mock_run.alert.assert_called_once_with(
195
- title="Title", text="Body", level="WARN"
195
+ title="Title", text="Body", level="WARN", wait_duration=None
196
196
  )
197
197
  finally:
198
198
  openrunner._active_run = original
@@ -70,15 +70,15 @@ class TestLogModel:
70
70
  from openrunner.integration.sklearn import log_model
71
71
  import openrunner
72
72
 
73
- mock_log = MagicMock()
74
- monkeypatch.setattr("openrunner.log", mock_log)
73
+ mock_config = MagicMock()
74
+ monkeypatch.setattr("openrunner.config", mock_config)
75
75
  monkeypatch.setattr("openrunner._active_run", MagicMock())
76
76
 
77
77
  model = _make_model(params={"n_estimators": 100, "max_depth": 5, "random_state": 42})
78
78
  log_model(model)
79
79
 
80
- mock_log.assert_called_once()
81
- logged = mock_log.call_args[0][0]
80
+ mock_config.update.assert_called_once()
81
+ logged = mock_config.update.call_args[0][0]
82
82
  assert logged["model/n_estimators"] == 100
83
83
  assert logged["model/max_depth"] == 5
84
84
  assert logged["model/random_state"] == 42
@@ -88,12 +88,12 @@ class TestLogModel:
88
88
  from openrunner.integration.sklearn import log_model
89
89
  import openrunner
90
90
 
91
- mock_log = MagicMock()
92
- monkeypatch.setattr("openrunner.log", mock_log)
91
+ mock_config = MagicMock()
92
+ monkeypatch.setattr("openrunner.config", mock_config)
93
93
  monkeypatch.setattr("openrunner._active_run", None)
94
94
 
95
95
  log_model(_make_model())
96
- mock_log.assert_not_called()
96
+ mock_config.update.assert_not_called()
97
97
 
98
98
 
99
99
  class TestLogClassificationReport:
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import pytest
5
6
  import io
6
7
  import json
7
8
  import struct
@@ -586,16 +587,18 @@ class TestHtml:
586
587
  """Tests for the Html class."""
587
588
 
588
589
  def test_html_basic(self) -> None:
589
- """Html with raw string serializes correctly."""
590
+ """Html with raw string serializes correctly (inject=True prepends style)."""
590
591
  html = Html("<div>Hello World</div>")
591
592
  result = html._serialize()
592
- assert result == {"html": "<div>Hello World</div>"}
593
+ assert result["html"].endswith("<div>Hello World</div>")
594
+ assert "<style>" in result["html"]
593
595
 
594
596
  def test_html_with_caption(self) -> None:
595
597
  """Html with caption includes it in serialized output."""
596
598
  html = Html("<p>Report</p>", caption="Training Report")
597
599
  result = html._serialize()
598
- assert result == {"html": "<p>Report</p>", "caption": "Training Report"}
600
+ assert result["html"].endswith("<p>Report</p>")
601
+ assert result["caption"] == "Training Report"
599
602
  assert html.caption == "Training Report"
600
603
 
601
604
  def test_html_no_caption(self) -> None:
@@ -616,7 +619,7 @@ class TestHtml:
616
619
  )
617
620
  html = Html(content, caption="complex")
618
621
  result = html._serialize()
619
- assert result["html"] == content
622
+ assert result["html"].endswith(content)
620
623
  assert result["caption"] == "complex"
621
624
 
622
625
 
@@ -627,6 +630,11 @@ class TestHtml:
627
630
  class TestMatplotlibFigure:
628
631
  """Tests for the MatplotlibFigure class."""
629
632
 
633
+ pytestmark = pytest.mark.skipif(
634
+ not __import__("importlib").util.find_spec("matplotlib"),
635
+ reason="matplotlib not installed",
636
+ )
637
+
630
638
  def test_matplotlib_explicit_figure(self) -> None:
631
639
  """MatplotlibFigure from explicit figure serializes to PNG bytes."""
632
640
  import matplotlib
@@ -208,6 +208,11 @@ class TestLineSeries:
208
208
  # pr_curve()
209
209
  # ---------------------------------------------------------------------------
210
210
 
211
+ import importlib.util
212
+ _has_sklearn = importlib.util.find_spec("sklearn") is not None
213
+
214
+
215
+ @pytest.mark.skipif(not _has_sklearn, reason="scikit-learn not installed")
211
216
  class TestPRCurve:
212
217
  """Tests for plot.pr_curve()."""
213
218
 
@@ -272,6 +277,7 @@ class TestPRCurve:
272
277
  # roc_curve()
273
278
  # ---------------------------------------------------------------------------
274
279
 
280
+ @pytest.mark.skipif(not _has_sklearn, reason="scikit-learn not installed")
275
281
  class TestROCCurve:
276
282
  """Tests for plot.roc_curve()."""
277
283
 
@@ -336,6 +342,7 @@ class TestROCCurve:
336
342
  # confusion_matrix()
337
343
  # ---------------------------------------------------------------------------
338
344
 
345
+ @pytest.mark.skipif(not _has_sklearn, reason="scikit-learn not installed")
339
346
  class TestConfusionMatrix:
340
347
  """Tests for plot.confusion_matrix()."""
341
348
 
@@ -82,8 +82,8 @@ class TestGPUMetrics:
82
82
  assert result["system.gpu.0.gpu"] == 75.0
83
83
  assert "system.gpu.0.memory" in result
84
84
  assert result["system.gpu.0.memory"] == 50.0
85
- assert "system.gpu.0.memoryAllocatedBytes" in result
86
- assert result["system.gpu.0.memoryAllocatedBytes"] == 4_000_000_000.0
85
+ assert "system.gpu.0.memoryAllocatedMB" in result
86
+ assert result["system.gpu.0.memoryAllocatedMB"] == 4_000_000_000.0 / (1024 * 1024)
87
87
 
88
88
 
89
89
  # ---------------------------------------------------------------------------
File without changes
File without changes
File without changes