openrunner-sdk 0.1.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 (50) hide show
  1. openrunner_sdk-0.1.0/.gitignore +226 -0
  2. openrunner_sdk-0.1.0/=6.0 +5 -0
  3. openrunner_sdk-0.1.0/=8.1 +0 -0
  4. openrunner_sdk-0.1.0/PKG-INFO +91 -0
  5. openrunner_sdk-0.1.0/README.md +53 -0
  6. openrunner_sdk-0.1.0/openrunner/__init__.py +273 -0
  7. openrunner_sdk-0.1.0/openrunner/api_client.py +368 -0
  8. openrunner_sdk-0.1.0/openrunner/artifact.py +88 -0
  9. openrunner_sdk-0.1.0/openrunner/buffer.py +46 -0
  10. openrunner_sdk-0.1.0/openrunner/cache.py +92 -0
  11. openrunner_sdk-0.1.0/openrunner/cli.py +210 -0
  12. openrunner_sdk-0.1.0/openrunner/config.py +91 -0
  13. openrunner_sdk-0.1.0/openrunner/git_info.py +66 -0
  14. openrunner_sdk-0.1.0/openrunner/integration/__init__.py +2 -0
  15. openrunner_sdk-0.1.0/openrunner/integration/huggingface.py +83 -0
  16. openrunner_sdk-0.1.0/openrunner/integration/lightning.py +111 -0
  17. openrunner_sdk-0.1.0/openrunner/integration/pytorch.py +49 -0
  18. openrunner_sdk-0.1.0/openrunner/media.py +120 -0
  19. openrunner_sdk-0.1.0/openrunner/offline.py +259 -0
  20. openrunner_sdk-0.1.0/openrunner/run.py +399 -0
  21. openrunner_sdk-0.1.0/openrunner/sender.py +239 -0
  22. openrunner_sdk-0.1.0/openrunner/settings.py +105 -0
  23. openrunner_sdk-0.1.0/openrunner/summary.py +70 -0
  24. openrunner_sdk-0.1.0/openrunner/system_metrics.py +97 -0
  25. openrunner_sdk-0.1.0/openrunner/wandb_compat/__init__.py +25 -0
  26. openrunner_sdk-0.1.0/openrunner/wandb_compat/_shim.py +6 -0
  27. openrunner_sdk-0.1.0/pyproject.toml +44 -0
  28. openrunner_sdk-0.1.0/tests/__init__.py +0 -0
  29. openrunner_sdk-0.1.0/tests/conftest.py +79 -0
  30. openrunner_sdk-0.1.0/tests/test_api_client.py +162 -0
  31. openrunner_sdk-0.1.0/tests/test_artifact.py +111 -0
  32. openrunner_sdk-0.1.0/tests/test_buffer.py +115 -0
  33. openrunner_sdk-0.1.0/tests/test_cache.py +82 -0
  34. openrunner_sdk-0.1.0/tests/test_cli.py +204 -0
  35. openrunner_sdk-0.1.0/tests/test_config.py +136 -0
  36. openrunner_sdk-0.1.0/tests/test_finish.py +125 -0
  37. openrunner_sdk-0.1.0/tests/test_git_info.py +74 -0
  38. openrunner_sdk-0.1.0/tests/test_init.py +171 -0
  39. openrunner_sdk-0.1.0/tests/test_integration_huggingface.py +168 -0
  40. openrunner_sdk-0.1.0/tests/test_integration_lightning.py +185 -0
  41. openrunner_sdk-0.1.0/tests/test_integration_pytorch.py +123 -0
  42. openrunner_sdk-0.1.0/tests/test_log.py +159 -0
  43. openrunner_sdk-0.1.0/tests/test_media.py +127 -0
  44. openrunner_sdk-0.1.0/tests/test_offline.py +210 -0
  45. openrunner_sdk-0.1.0/tests/test_offline_sync.py +229 -0
  46. openrunner_sdk-0.1.0/tests/test_resume.py +257 -0
  47. openrunner_sdk-0.1.0/tests/test_sender.py +165 -0
  48. openrunner_sdk-0.1.0/tests/test_summary.py +95 -0
  49. openrunner_sdk-0.1.0/tests/test_system_metrics.py +138 -0
  50. openrunner_sdk-0.1.0/tests/test_wandb_compat.py +49 -0
@@ -0,0 +1,226 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ !src/web/src/lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py.cover
51
+ *.lcov
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ # Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ # uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ # poetry.lock
111
+ # poetry.toml
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
116
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
117
+ # pdm.lock
118
+ # pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # pixi
123
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
124
+ # pixi.lock
125
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
126
+ # in the .venv directory. It is recommended not to include this directory in version control.
127
+ .pixi/*
128
+ !.pixi/config.toml
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ __pypackages__/
132
+
133
+ # Celery stuff
134
+ celerybeat-schedule*
135
+ celerybeat.pid
136
+
137
+ # Redis
138
+ *.rdb
139
+ *.aof
140
+ *.pid
141
+
142
+ # RabbitMQ
143
+ mnesia/
144
+ rabbitmq/
145
+ rabbitmq-data/
146
+
147
+ # ActiveMQ
148
+ activemq-data/
149
+
150
+ # SageMath parsed files
151
+ *.sage.py
152
+
153
+ # Environments
154
+ .env
155
+ .envrc
156
+ .venv
157
+ env/
158
+ venv/
159
+ ENV/
160
+ env.bak/
161
+ venv.bak/
162
+
163
+ # Spyder project settings
164
+ .spyderproject
165
+ .spyproject
166
+
167
+ # Rope project settings
168
+ .ropeproject
169
+
170
+ # mkdocs documentation
171
+ /site
172
+
173
+ # mypy
174
+ .mypy_cache/
175
+ .dmypy.json
176
+ dmypy.json
177
+
178
+ # Pyre type checker
179
+ .pyre/
180
+
181
+ # pytype static type analyzer
182
+ .pytype/
183
+
184
+ # Cython debug symbols
185
+ cython_debug/
186
+
187
+ # PyCharm
188
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
189
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
190
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
191
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
192
+ # .idea/
193
+
194
+ # Abstra
195
+ # Abstra is an AI-powered process automation framework.
196
+ # Ignore directories containing user credentials, local state, and settings.
197
+ # Learn more at https://abstra.io/docs
198
+ .abstra/
199
+
200
+ # Visual Studio Code
201
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
202
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
203
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
204
+ # you could uncomment the following to ignore the entire vscode folder
205
+ # .vscode/
206
+ # Temporary file for partial code execution
207
+ tempCodeRunnerFile.py
208
+
209
+ # Node.js
210
+ node_modules/
211
+ *.tsbuildinfo
212
+
213
+ # Ruff stuff:
214
+ .ruff_cache/
215
+
216
+ # PyPI configuration file
217
+ .pypirc
218
+
219
+ # Marimo
220
+ marimo/_static/
221
+ marimo/_lsp/
222
+ __marimo__/
223
+
224
+ # Streamlit
225
+ .streamlit/secrets.toml
226
+ data/
@@ -0,0 +1,5 @@
1
+ Collecting psutil
2
+ Using cached psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl.metadata (22 kB)
3
+ Using cached psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl (155 kB)
4
+ Installing collected packages: psutil
5
+ Successfully installed psutil-7.2.2
File without changes
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: openrunner-sdk
3
+ Version: 0.1.0
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
8
+ Author-email: JL Queguiner <jl@gladia.io>
9
+ License: MIT
10
+ Keywords: experiment-tracking,machine-learning,ml,mlops,wandb
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: pillow>=10.0
25
+ Requires-Dist: psutil>=6.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Provides-Extra: gpu
30
+ Requires-Dist: nvidia-ml-py>=12.0; extra == 'gpu'
31
+ Provides-Extra: huggingface
32
+ Requires-Dist: transformers>=4.30; extra == 'huggingface'
33
+ Provides-Extra: lightning
34
+ Requires-Dist: lightning>=2.0; extra == 'lightning'
35
+ Provides-Extra: pytorch
36
+ Requires-Dist: torch>=2.0; extra == 'pytorch'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # OpenRunner SDK
40
+
41
+ Open-source, self-hosted ML experiment tracking — a drop-in replacement for Weights & Biases.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install openrunner-sdk
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```bash
52
+ export OPENRUNNER_API_KEY="or_your_key"
53
+ export OPENRUNNER_BASE_URL="https://your-server.com"
54
+ ```
55
+
56
+ ```python
57
+ import openrunner
58
+
59
+ openrunner.init(project="my-project", config={"lr": 0.001})
60
+
61
+ for epoch in range(10):
62
+ loss = train(epoch)
63
+ openrunner.log({"loss": loss, "epoch": epoch})
64
+
65
+ openrunner.finish()
66
+ ```
67
+
68
+ ## Migrating from W&B
69
+
70
+ ```python
71
+ import openrunner as wandb
72
+
73
+ wandb.init(project="my-project")
74
+ wandb.log({"loss": 0.5})
75
+ wandb.finish()
76
+ ```
77
+
78
+ ## Features
79
+
80
+ - **W&B-compatible API** — `init()`, `log()`, `finish()`, `config`, `summary`
81
+ - **Non-blocking** — logging never slows down training
82
+ - **Artifacts** — version datasets, models, checkpoints with SHA-256 dedup
83
+ - **Media** — log images (`Image()`) and tables (`Table()`)
84
+ - **Offline mode** — works without connectivity, sync later
85
+ - **Framework integrations** — PyTorch, HuggingFace, Lightning
86
+ - **CLI** — `openrunner login`, `openrunner sync`, `openrunner ls`
87
+ - **System metrics** — GPU/CPU/memory monitoring via psutil
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,53 @@
1
+ # OpenRunner SDK
2
+
3
+ Open-source, self-hosted ML experiment tracking — a drop-in replacement for Weights & Biases.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install openrunner-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ export OPENRUNNER_API_KEY="or_your_key"
15
+ export OPENRUNNER_BASE_URL="https://your-server.com"
16
+ ```
17
+
18
+ ```python
19
+ import openrunner
20
+
21
+ openrunner.init(project="my-project", config={"lr": 0.001})
22
+
23
+ for epoch in range(10):
24
+ loss = train(epoch)
25
+ openrunner.log({"loss": loss, "epoch": epoch})
26
+
27
+ openrunner.finish()
28
+ ```
29
+
30
+ ## Migrating from W&B
31
+
32
+ ```python
33
+ import openrunner as wandb
34
+
35
+ wandb.init(project="my-project")
36
+ wandb.log({"loss": 0.5})
37
+ wandb.finish()
38
+ ```
39
+
40
+ ## Features
41
+
42
+ - **W&B-compatible API** — `init()`, `log()`, `finish()`, `config`, `summary`
43
+ - **Non-blocking** — logging never slows down training
44
+ - **Artifacts** — version datasets, models, checkpoints with SHA-256 dedup
45
+ - **Media** — log images (`Image()`) and tables (`Table()`)
46
+ - **Offline mode** — works without connectivity, sync later
47
+ - **Framework integrations** — PyTorch, HuggingFace, Lightning
48
+ - **CLI** — `openrunner login`, `openrunner sync`, `openrunner ls`
49
+ - **System metrics** — GPU/CPU/memory monitoring via psutil
50
+
51
+ ## License
52
+
53
+ MIT
@@ -0,0 +1,273 @@
1
+ """OpenRunner SDK - W&B-compatible ML experiment tracking.
2
+
3
+ Public API:
4
+ openrunner.init(project=..., name=..., config=...) -> Run
5
+ openrunner.log({"loss": 0.5}, step=10) -> None
6
+ openrunner.finish(exit_code=0) -> None
7
+ openrunner.config -> Config proxy
8
+ openrunner.summary -> Summary proxy
9
+ openrunner.run -> active Run or None
10
+ openrunner.Image -> Image class for media logging
11
+ openrunner.Table -> Table class for structured data
12
+ openrunner.Artifact -> Artifact class for versioned file collections
13
+ openrunner.log_artifact -> Upload an artifact
14
+ openrunner.use_artifact -> Download an artifact (cached)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from openrunner.artifact import Artifact
24
+ from openrunner.config import Config
25
+ from openrunner.media import Image, Table
26
+ from openrunner.run import Run
27
+ from openrunner.settings import SDKSettings
28
+ from openrunner.summary import Summary
29
+
30
+ __version__ = "0.1.0"
31
+
32
+ logger = logging.getLogger("openrunner")
33
+
34
+ # Active run state
35
+ _active_run: Run | None = None
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Proxy objects for module-level config/summary access
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class _ConfigProxy:
43
+ """Proxy that delegates attribute access to the active run's Config.
44
+
45
+ Since Python modules can't have properties, this proxy object sits
46
+ at ``openrunner.config`` and forwards all access to ``_active_run.config``.
47
+ """
48
+
49
+ def __getattr__(self, name: str) -> Any:
50
+ if _active_run is not None:
51
+ return getattr(_active_run.config, name)
52
+ raise AttributeError(f"No active run -- call openrunner.init() first")
53
+
54
+ def __setattr__(self, name: str, value: Any) -> None:
55
+ if _active_run is not None:
56
+ setattr(_active_run.config, name, value)
57
+ else:
58
+ logger.warning("openrunner.config: no active run -- call openrunner.init() first")
59
+
60
+ def __getitem__(self, key: str) -> Any:
61
+ if _active_run is not None:
62
+ return _active_run.config[key]
63
+ raise KeyError(f"No active run -- call openrunner.init() first")
64
+
65
+ def __setitem__(self, key: str, value: Any) -> None:
66
+ if _active_run is not None:
67
+ _active_run.config[key] = value
68
+ else:
69
+ logger.warning("openrunner.config: no active run -- call openrunner.init() first")
70
+
71
+ def __repr__(self) -> str:
72
+ if _active_run is not None:
73
+ return repr(_active_run.config)
74
+ return "ConfigProxy(no active run)"
75
+
76
+
77
+ class _SummaryProxy:
78
+ """Proxy that delegates attribute access to the active run's Summary."""
79
+
80
+ def __getattr__(self, name: str) -> Any:
81
+ if _active_run is not None:
82
+ return getattr(_active_run.summary, name)
83
+ raise AttributeError(f"No active run -- call openrunner.init() first")
84
+
85
+ def __setattr__(self, name: str, value: Any) -> None:
86
+ if _active_run is not None:
87
+ setattr(_active_run.summary, name, value)
88
+ else:
89
+ logger.warning("openrunner.summary: no active run -- call openrunner.init() first")
90
+
91
+ def __getitem__(self, key: str) -> Any:
92
+ if _active_run is not None:
93
+ return _active_run.summary[key]
94
+ raise KeyError(f"No active run -- call openrunner.init() first")
95
+
96
+ def __setitem__(self, key: str, value: Any) -> None:
97
+ if _active_run is not None:
98
+ _active_run.summary[key] = value
99
+ else:
100
+ logger.warning("openrunner.summary: no active run -- call openrunner.init() first")
101
+
102
+ def __repr__(self) -> str:
103
+ if _active_run is not None:
104
+ return repr(_active_run.summary)
105
+ return "SummaryProxy(no active run)"
106
+
107
+
108
+ # Module-level proxy instances
109
+ config = _ConfigProxy()
110
+ summary = _SummaryProxy()
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Public API functions
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def init(
118
+ project: str | None = None,
119
+ name: str | None = None,
120
+ config: dict[str, Any] | None = None,
121
+ tags: list[str] | None = None,
122
+ notes: str | None = None,
123
+ group: str | None = None,
124
+ job_type: str | None = None,
125
+ id: str | None = None,
126
+ resume: bool | str | None = None,
127
+ **kwargs: Any,
128
+ ) -> Run | None:
129
+ """Initialize a new run.
130
+
131
+ Creates a run on the server, starts the background sender thread,
132
+ and returns the Run object. Never raises -- SDK failures must not
133
+ crash training code.
134
+
135
+ Args:
136
+ project: Project name (or OPENRUNNER_PROJECT env var, or "uncategorized").
137
+ name: Display name for the run.
138
+ config: Hyperparameter dict (sent to server at init time, SDK-12).
139
+ tags: List of tags for the run.
140
+ notes: Notes/description for the run.
141
+ group: Group name for related runs.
142
+ job_type: Job type label.
143
+ id: Custom run ID (8-char alphanumeric generated if not provided).
144
+ resume: Resume mode -- True/"allow" (resume if exists, fresh otherwise)
145
+ or "must" (error if parent not found). The ``id`` parameter
146
+ is treated as the parent run ID when resuming.
147
+
148
+ Returns:
149
+ The Run object, or None if initialization fails.
150
+ """
151
+ global _active_run
152
+
153
+ try:
154
+ settings = SDKSettings()
155
+
156
+ # Resolve project
157
+ resolved_project = project or settings.project or "uncategorized"
158
+
159
+ # Warn if no API key (skip in offline mode)
160
+ if not settings.api_key and settings.mode != "offline":
161
+ logger.warning(
162
+ "No API key set. Set OPENRUNNER_API_KEY or WANDB_API_KEY "
163
+ "environment variable for server communication."
164
+ )
165
+
166
+ # Create run
167
+ run = Run(
168
+ project=resolved_project,
169
+ name=name,
170
+ config_dict=config,
171
+ tags=tags,
172
+ notes=notes,
173
+ group=group,
174
+ job_type=job_type,
175
+ run_id=id,
176
+ settings=settings,
177
+ resume=resume,
178
+ )
179
+
180
+ _active_run = run
181
+ return run
182
+
183
+ except Exception as e:
184
+ logger.warning("openrunner.init() failed: %s", e)
185
+ return None
186
+
187
+
188
+ def log(
189
+ data: dict[str, Any],
190
+ step: int | None = None,
191
+ commit: bool = True,
192
+ ) -> None:
193
+ """Log metrics. Never raises -- training must not be interrupted.
194
+
195
+ Args:
196
+ data: Dict of metric key-value pairs.
197
+ step: Explicit step value.
198
+ commit: If True (default), finalize the current step.
199
+ """
200
+ try:
201
+ if _active_run is None:
202
+ logger.warning("openrunner.log(): no active run -- call openrunner.init() first")
203
+ return
204
+ _active_run.log(data, step=step, commit=commit)
205
+ except Exception as e:
206
+ logger.warning("openrunner.log() failed: %s", e)
207
+
208
+
209
+ def finish(
210
+ exit_code: int | None = None,
211
+ quiet: bool | None = None,
212
+ ) -> None:
213
+ """Finish the active run. Never raises.
214
+
215
+ Args:
216
+ exit_code: Optional exit code.
217
+ quiet: If True, suppress the finish message.
218
+ """
219
+ global _active_run
220
+
221
+ try:
222
+ if _active_run is None:
223
+ return
224
+ _active_run.finish(exit_code=exit_code, quiet=quiet or False)
225
+ _active_run = None
226
+ except Exception as e:
227
+ logger.warning("openrunner.finish() failed: %s", e)
228
+ _active_run = None
229
+
230
+
231
+ def log_artifact(artifact: Artifact) -> dict[str, Any] | None:
232
+ """Upload an artifact to the active run. Never raises.
233
+
234
+ Args:
235
+ artifact: The Artifact object with files added via add_file/add_dir.
236
+
237
+ Returns:
238
+ Server response dict with version info, or None on failure.
239
+ """
240
+ try:
241
+ if _active_run is None:
242
+ logger.warning("openrunner.log_artifact(): no active run")
243
+ return None
244
+ return _active_run.log_artifact(artifact)
245
+ except Exception as e:
246
+ logger.warning("openrunner.log_artifact() failed: %s", e)
247
+ return None
248
+
249
+
250
+ def use_artifact(name: str, version: int | None = None) -> Path | None:
251
+ """Download an artifact and cache locally. Never raises.
252
+
253
+ Args:
254
+ name: Artifact name.
255
+ version: Specific version number, or None for latest.
256
+
257
+ Returns:
258
+ Path to the local artifact directory, or None on failure.
259
+ """
260
+ try:
261
+ if _active_run is None:
262
+ logger.warning("openrunner.use_artifact(): no active run")
263
+ return None
264
+ return _active_run.use_artifact(name, version)
265
+ except Exception as e:
266
+ logger.warning("openrunner.use_artifact() failed: %s", e)
267
+ return None
268
+
269
+
270
+ @property # type: ignore[misc]
271
+ def run() -> Run | None:
272
+ """Return the active run, or None."""
273
+ return _active_run