lit-mlflow 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 twsl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.1
2
+ Name: lit-mlflow
3
+ Version: 0.1.0
4
+ Summary: An improved Lightning mlflow logger
5
+ Home-page: https://twsl.github.io/lit-mlflow/
6
+ License: MIT
7
+ Author: twsl
8
+ Author-email: 45483159+twsl@users.noreply.github.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: databricks-cli (>=0.18.0,<0.19.0)
17
+ Requires-Dist: lightning (>=2.4.0,<3.0.0)
18
+ Requires-Dist: mlflow (>=2.18.0,<3.0.0)
19
+ Project-URL: Repository, https://github.com/twsl/lit-mlflow
20
+ Description-Content-Type: text/markdown
21
+
22
+ # lit-mlflow
23
+
24
+
25
+ [![Build](https://github.com/twsl/lit-mlflow/actions/workflows/build.yaml/badge.svg)](https://github.com/twsl/lit-mlflow/actions/workflows/build.yaml)
26
+ [![Documentation](https://github.com/twsl/lit-mlflow/actions/workflows/docs.yaml/badge.svg)](https://github.com/twsl/lit-mlflow/actions/workflows/docs.yaml)
27
+ ![GitHub Release](https://img.shields.io/github/v/release/twsl/lit-mlflow?include_prereleases)
28
+ [![PyPI - Package Version](https://img.shields.io/pypi/v/lit-mlflow?logo=pypi&style=flat&color=orange)](https://pypi.org/project/lit-mlflow/)
29
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lit-mlflow?logo=pypi&style=flat&color=blue)](https://pypi.org/project/lit-mlflow/)
30
+ [![Dependencies Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg)](https://github.com/twsl/lit-mlflow/pulls?utf8=%E2%9C%93&q=is:pr%20author:app/dependabot)
31
+ [![Docs with MkDocs](https://img.shields.io/badge/MkDocs-docs?style=flat&logo=materialformkdocs&logoColor=white&color=%23526CFE)](https://squidfunk.github.io/mkdocs-material/)
32
+ [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
33
+ [![linting: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
34
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](.pre-commit-config.yaml)
35
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
36
+ [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
37
+ [![Semantic Versions](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--versions-e10079.svg)](https://github.com/twsl/lit-mlflow/releases)
38
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-border.json)](https://github.com/copier-org/copier)
39
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
40
+
41
+
42
+ An improved Lightning mlflow logger. Works seamlessly with PyTorch Lightning on Databricks and offers more control compared to the `mlflow.pytorch.autolog` function.
43
+
44
+
45
+ ## Features
46
+
47
+ - Makes `MLflow` logging work with `lightning` and [Databricks](https://www.databricks.com/)
48
+
49
+
50
+ ## Installation
51
+
52
+ With `pip`:
53
+ ```bash
54
+ python -m pip install lit-mlflow
55
+ ```
56
+
57
+ With [`poetry`](https://python-poetry.org/):
58
+ ```bash
59
+ poetry add lit-mlflow
60
+ ```
61
+
62
+
63
+ ## How to use it
64
+ Replace `mlflow.autolog()` with the `MlFlowAutoCallback`:
65
+
66
+ ```python
67
+ from lit_mlflow import MlFlowAutoCallback
68
+ import lightning.pytorch as pl
69
+
70
+ trainer = pl.Trainer(
71
+ callbacks=[
72
+ MlFlowAutoCallback()
73
+ ]
74
+ )
75
+ ```
76
+
77
+ To support Databricks mlflow, use the `DbxMLFlowLogger` instead of the `MlFlowLogger`:
78
+
79
+ ```python
80
+ from lit_mlflow import DbxMLFlowLogger
81
+ import lightning.pytorch as pl
82
+
83
+ trainer = pl.Trainer(
84
+ logger=[
85
+ DbxMLFlowLogger()
86
+ ]
87
+ )
88
+ ```
89
+
90
+ ## Docs
91
+
92
+ ```bash
93
+ poetry run mkdocs build -f ./docs/mkdocs.yml -d ./_build/
94
+ ```
95
+
96
+
97
+ ## Update template
98
+
99
+ ```bash
100
+ copier update --trust -A --vcs-ref=HEAD
101
+ ```
102
+
103
+
104
+ ## Credits
105
+
106
+ This project was generated with [![🚀 A generic python project template.](https://img.shields.io/badge/python--project--template-%F0%9F%9A%80-brightgreen)](https://github.com/twsl/python-project-template)
107
+
@@ -0,0 +1,85 @@
1
+ # lit-mlflow
2
+
3
+
4
+ [![Build](https://github.com/twsl/lit-mlflow/actions/workflows/build.yaml/badge.svg)](https://github.com/twsl/lit-mlflow/actions/workflows/build.yaml)
5
+ [![Documentation](https://github.com/twsl/lit-mlflow/actions/workflows/docs.yaml/badge.svg)](https://github.com/twsl/lit-mlflow/actions/workflows/docs.yaml)
6
+ ![GitHub Release](https://img.shields.io/github/v/release/twsl/lit-mlflow?include_prereleases)
7
+ [![PyPI - Package Version](https://img.shields.io/pypi/v/lit-mlflow?logo=pypi&style=flat&color=orange)](https://pypi.org/project/lit-mlflow/)
8
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lit-mlflow?logo=pypi&style=flat&color=blue)](https://pypi.org/project/lit-mlflow/)
9
+ [![Dependencies Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg)](https://github.com/twsl/lit-mlflow/pulls?utf8=%E2%9C%93&q=is:pr%20author:app/dependabot)
10
+ [![Docs with MkDocs](https://img.shields.io/badge/MkDocs-docs?style=flat&logo=materialformkdocs&logoColor=white&color=%23526CFE)](https://squidfunk.github.io/mkdocs-material/)
11
+ [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
12
+ [![linting: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
13
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](.pre-commit-config.yaml)
14
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
15
+ [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
16
+ [![Semantic Versions](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--versions-e10079.svg)](https://github.com/twsl/lit-mlflow/releases)
17
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-border.json)](https://github.com/copier-org/copier)
18
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
19
+
20
+
21
+ An improved Lightning mlflow logger. Works seamlessly with PyTorch Lightning on Databricks and offers more control compared to the `mlflow.pytorch.autolog` function.
22
+
23
+
24
+ ## Features
25
+
26
+ - Makes `MLflow` logging work with `lightning` and [Databricks](https://www.databricks.com/)
27
+
28
+
29
+ ## Installation
30
+
31
+ With `pip`:
32
+ ```bash
33
+ python -m pip install lit-mlflow
34
+ ```
35
+
36
+ With [`poetry`](https://python-poetry.org/):
37
+ ```bash
38
+ poetry add lit-mlflow
39
+ ```
40
+
41
+
42
+ ## How to use it
43
+ Replace `mlflow.autolog()` with the `MlFlowAutoCallback`:
44
+
45
+ ```python
46
+ from lit_mlflow import MlFlowAutoCallback
47
+ import lightning.pytorch as pl
48
+
49
+ trainer = pl.Trainer(
50
+ callbacks=[
51
+ MlFlowAutoCallback()
52
+ ]
53
+ )
54
+ ```
55
+
56
+ To support Databricks mlflow, use the `DbxMLFlowLogger` instead of the `MlFlowLogger`:
57
+
58
+ ```python
59
+ from lit_mlflow import DbxMLFlowLogger
60
+ import lightning.pytorch as pl
61
+
62
+ trainer = pl.Trainer(
63
+ logger=[
64
+ DbxMLFlowLogger()
65
+ ]
66
+ )
67
+ ```
68
+
69
+ ## Docs
70
+
71
+ ```bash
72
+ poetry run mkdocs build -f ./docs/mkdocs.yml -d ./_build/
73
+ ```
74
+
75
+
76
+ ## Update template
77
+
78
+ ```bash
79
+ copier update --trust -A --vcs-ref=HEAD
80
+ ```
81
+
82
+
83
+ ## Credits
84
+
85
+ This project was generated with [![🚀 A generic python project template.](https://img.shields.io/badge/python--project--template-%F0%9F%9A%80-brightgreen)](https://github.com/twsl/python-project-template)
@@ -0,0 +1,226 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
3
+ build-backend = "poetry_dynamic_versioning.backend"
4
+
5
+ [project]
6
+ name = "lit-mlflow"
7
+ requires-python = ">=3.10"
8
+
9
+ # TODO: https://github.com/python-poetry/poetry/issues/3332
10
+ [tool.poetry]
11
+ name = "lit-mlflow"
12
+ version = "0.1.0"
13
+ description = "An improved Lightning mlflow logger"
14
+ authors = ["twsl <45483159+twsl@users.noreply.github.com>"]
15
+ license = "MIT"
16
+ readme = "README.md"
17
+ repository = "https://github.com/twsl/lit-mlflow"
18
+ homepage = "https://twsl.github.io/lit-mlflow/"
19
+ packages = [ { include = "lit_mlflow", from = "src" } ]
20
+ keywords = []
21
+
22
+ [tool.poetry.dependencies]
23
+ python = "^3.10"
24
+ mlflow = "^2.18.0"
25
+ lightning = "^2.4.0"
26
+ databricks-cli = "^0.18.0"
27
+
28
+ [tool.poetry.group.dev.dependencies]
29
+ ruff = "^0.8.2"
30
+ bandit = "^1.8.0"
31
+ pre-commit = "^4.0.1"
32
+ pyright = "^1.1.390"
33
+ ipykernel = "^6.29.5"
34
+ ipywidgets = "^8.1.5"
35
+ ipython = "^8.30.0"
36
+ jupytext = " ^1.16.4"
37
+ nbconvert = "^7.16.3"
38
+
39
+ [tool.poetry.group.test.dependencies]
40
+ pytest = "^8.3.4"
41
+ pytest-cov = "^6.0.0"
42
+ pytest-mock = "^3.12.0"
43
+ pytest-cases = "^3.8.6"
44
+ pytest-benchmark = "^5.1.0"
45
+
46
+ [tool.poetry.group.docs.dependencies]
47
+ mkdocs = "^1.6.1"
48
+ mkdocs-material = "^9.5.48"
49
+ mkdocs-git-revision-date-localized-plugin = "^1.2.9"
50
+ mkdocs-awesome-pages-plugin = "^2.9.3"
51
+ mkdocs-git-authors-plugin = "^0.9.2"
52
+ mkapi = "^3.0.22"
53
+ markdown-callouts = "^0.4.0"
54
+ pygments = "^2.18.0"
55
+ mkdocs-jupyter = "^0.25.1"
56
+
57
+ [tool.poetry.group.debug]
58
+ optional = true
59
+
60
+ [tool.poetry.group.debug.dependencies]
61
+ ipdb = "^0.13.9"
62
+ line_profiler = "^4.2.0"
63
+
64
+ [tool.poetry.scripts]
65
+ lit-mlflow = "lit_mlflow.cli:main"
66
+
67
+ [tool.poetry-dynamic-versioning]
68
+ enable = false
69
+ vcs = "git"
70
+ style = "semver"
71
+ pattern = "default-unprefixed"
72
+
73
+ [tool.poetry-dynamic-versioning.substitution]
74
+ folders = [
75
+ { path = "src/lit_mlflow" }
76
+ ]
77
+
78
+ [tool.poetry-dynamic-versioning.files."src/lit_mlflow/__about__.py"]
79
+ persistent-substitution = false
80
+ initial-content = """
81
+ # file generated by poetry dynamic versioning during poetry build/install
82
+ # don't change, don't track in version control
83
+ __version__ = "0.0.0"
84
+ __version_tuple__ = (0, 0, 0)
85
+ """
86
+
87
+ [tool.pytest.ini_options]
88
+ minversion = "8.0"
89
+ # https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
90
+ addopts = "--doctest-glob='test_*.md' --cov=src/lit_mlflow --cov-report xml"
91
+ testpaths = [
92
+ "tests",
93
+ ]
94
+
95
+ [tool.coverage.run]
96
+ branch = true
97
+ omit = [
98
+ "tests/*",
99
+ ]
100
+
101
+ [tool.coverage.report]
102
+ exclude_lines = [
103
+ # Have to re-enable the standard pragma
104
+ "pragma: no cover",
105
+
106
+ # Don't complain about missing debug-only code:
107
+ "def __repr__",
108
+ "if self.debug:",
109
+ "if debug:",
110
+ "if DEBUG:",
111
+
112
+ # Don't complain if tests don't hit defensive assertion code:
113
+ "raise AssertionError",
114
+ "raise NotImplementedError",
115
+
116
+ # Don't complain if non-runnable code isn't run:
117
+ "if 0:",
118
+ "if False:",
119
+ "if __name__ == .__main__.:",
120
+ ]
121
+
122
+ [tool.ruff]
123
+ line-length = 120 # Must agree with Black
124
+ extend-include = ["*.ipynb"]
125
+ exclude = [
126
+ "migrations",
127
+ "__pycache__",
128
+ "env",
129
+ ".env",
130
+ "venv",
131
+ ".venv",
132
+ ]
133
+
134
+ [tool.ruff.lint]
135
+ select = [
136
+ "B", # flake8-bugbear
137
+ "C4", # flake8-comprehensions
138
+ "D", # pydocstyle
139
+ "E", # Error
140
+ "F", # pyflakes
141
+ "I", # isort
142
+ "ISC", # flake8-implicit-str-concat
143
+ "N", # pep8-naming
144
+ "PGH", # pygrep-hooks
145
+ "PTH", # flake8-use-pathlib
146
+ "Q", # flake8-quotes
147
+ "S", # bandit
148
+ "SIM", # flake8-simplify
149
+ "TRY", # tryceratops
150
+ "UP", # pyupgrade
151
+ "W", # Warning
152
+ "YTT", # flake8-2020
153
+ ]
154
+ extend-select = [
155
+ "I",
156
+ ]
157
+ ignore = [
158
+ "D100",
159
+ "D101",
160
+ "D102",
161
+ "D103",
162
+ "D104",
163
+ "D105",
164
+ "D106",
165
+ "D107",
166
+ "D200",
167
+ "D401",
168
+ "E402",
169
+ "E501",
170
+ "F401",
171
+ "TRY002", # Allow default exceptions
172
+ "TRY003", # Avoid specifying messages outside exception class; overly strict, especially for ValueError
173
+ ]
174
+
175
+ [tool.ruff.lint.flake8-bugbear]
176
+ extend-immutable-calls = [
177
+ "chr",
178
+ "typer.Argument",
179
+ "typer.Option",
180
+ ]
181
+
182
+ [tool.ruff.lint.pydocstyle]
183
+ convention = "google"
184
+
185
+ [tool.ruff.lint.pycodestyle]
186
+ max-doc-length = 120
187
+ max-line-length = 120
188
+
189
+ [tool.ruff.lint.per-file-ignores]
190
+ "test*/**/*.py" = [
191
+ "D100",
192
+ "D101",
193
+ "D102",
194
+ "D103",
195
+ "D104",
196
+ "D105",
197
+ "D106",
198
+ "D107",
199
+ "S101", # use of "assert"
200
+ "S102", # use of "exec"
201
+ "S106", # possible hardcoded password.
202
+ "PGH001", # use of "eval"
203
+ ]
204
+
205
+ [tool.ruff.lint.pep8-naming]
206
+ staticmethod-decorators = [
207
+ "pydantic.validator",
208
+ "pydantic.root_validator",
209
+ ]
210
+
211
+ [tool.ruff.lint.isort]
212
+ force-sort-within-sections = true
213
+
214
+ [tool.ruff.format]
215
+ docstring-code-format = true
216
+ quote-style = "double"
217
+
218
+ [tool.bandit.assert_used]
219
+ skips = ['*_test.py', '*/test_*.py']
220
+
221
+ [tool.pyright]
222
+ pythonVersion = "3.10"
223
+ pythonPlatform = "Linux"
224
+ include = ["src"]
225
+ reportMissingImports = true
226
+ reportMissingTypeStubs = false
@@ -0,0 +1,4 @@
1
+ # file generated by poetry dynamic versioning during poetry build/install
2
+ # don't change, don't track in version control
3
+ __version__ = "0.1.0"
4
+ __version_tuple__ = (0, 1, 0)
@@ -0,0 +1,5 @@
1
+ from lit_mlflow.__about__ import __version__, __version_tuple__
2
+ from lit_mlflow.callback import MlFlowAutoCallback
3
+ from lit_mlflow.logger import DbxMLFlowLogger
4
+
5
+ __all__ = ["__version__", "__version_tuple__", "MlFlowAutoCallback", "DbxMLFlowLogger"]
@@ -0,0 +1,498 @@
1
+ import os
2
+ from pathlib import Path
3
+ import tempfile
4
+ from typing import Any, cast
5
+
6
+ from lightning.fabric.utilities.rank_zero import (
7
+ rank_zero_info, # type: ignore # noqa: PGH003
8
+ rank_zero_only,
9
+ rank_zero_warn, # type: ignore # noqa: PGH003
10
+ )
11
+ import lightning.pytorch as pl
12
+ from lightning.pytorch.callbacks import Callback, DeviceStatsMonitor, EarlyStopping
13
+ from lightning.pytorch.core.optimizer import LightningOptimizer
14
+ from lightning.pytorch.loggers import Logger, MLFlowLogger
15
+ from lightning.pytorch.loggers.mlflow import _get_resolve_tags
16
+ from lightning.pytorch.trainer.states import TrainerFn
17
+ from lightning.pytorch.utilities.model_summary.model_summary import ModelSummary
18
+ from lightning.pytorch.utilities.types import STEP_OUTPUT
19
+ import mlflow
20
+ from mlflow import MlflowClient, MlflowException
21
+ from mlflow.entities.dataset import Dataset
22
+ from mlflow.entities.dataset_input import DatasetInput
23
+ from mlflow.entities.input_tag import InputTag
24
+ from mlflow.entities.run import Run
25
+ from mlflow.entities.run_status import RunStatus
26
+ from mlflow.models import Model
27
+ from mlflow.pytorch import pickle_module as mlflow_pytorch_pickle_module
28
+ from torch.optim import Optimizer # pyright: ignore[reportPrivateImportUsage]
29
+
30
+ from lit_mlflow.logger import DbxMLFlowLogger
31
+ from lit_mlflow.utils.dbx import get_databricks_tags
32
+
33
+
34
+ class MlFlowAutoCallback(Callback):
35
+ def __init__(self, verbose: bool = True, patch_device_monitor: bool = True) -> None:
36
+ self.supported_loggers = (MLFlowLogger, DbxMLFlowLogger)
37
+ self.verbose = verbose
38
+ self.logger: MLFlowLogger | DbxMLFlowLogger | None = None
39
+ self.autologging_disabled = False
40
+ self.patch_device_monitor = patch_device_monitor
41
+
42
+ @property
43
+ def client(self) -> MlflowClient | None:
44
+ if self.logger:
45
+ return self.logger.experiment
46
+ return None
47
+
48
+ def _get_logger(self, loggers: list[Logger]) -> MLFlowLogger | DbxMLFlowLogger | None:
49
+ if isinstance(loggers, list):
50
+ if len(loggers) == 0:
51
+ rank_zero_warn("Cannot log artifacts because Trainer has no logger.")
52
+ return None
53
+ else:
54
+ rank_zero_info(f" Supported loggers are: {', '.join(str(x.__name__) for x in self.supported_loggers)}")
55
+ for logger in loggers:
56
+ if isinstance(logger, self.supported_loggers):
57
+ return logger
58
+ else:
59
+ rank_zero_warn(
60
+ f"{self.__class__.__name__} does not support logging with {logger.__class__.__name__}."
61
+ )
62
+ return None
63
+
64
+ def _prevent_entry(self, trainer: "pl.Trainer") -> bool:
65
+ return self.logger is None or not trainer.is_global_zero
66
+
67
+ def _get_optimizer(self, optimizer: LightningOptimizer | Optimizer) -> Optimizer:
68
+ return optimizer._optimizer if isinstance(optimizer, LightningOptimizer) else optimizer
69
+
70
+ def _get_optimizer_name(self, optimizer: LightningOptimizer | Optimizer) -> str:
71
+ opt = self._get_optimizer(optimizer)
72
+ return opt.__class__.__name__
73
+
74
+ def _get_optimizer_defaults(self, optimizer: LightningOptimizer | Optimizer) -> dict[str, Any]:
75
+ opt = self._get_optimizer(optimizer)
76
+ return opt.defaults if hasattr(opt, "defaults") else {}
77
+
78
+ def _log_early_stop_params(self, early_stop_callback: EarlyStopping) -> None:
79
+ """Logs early stopping configuration parameters to MLflow."""
80
+ if self.logger is None:
81
+ return None
82
+
83
+ params = {
84
+ p: getattr(early_stop_callback, p)
85
+ for p in ["monitor", "mode", "patience", "min_delta", "stopped_epoch"]
86
+ if hasattr(early_stop_callback, p)
87
+ }
88
+ self.logger.log_hyperparams(params)
89
+
90
+ def _log_early_stop_metrics(self, early_stop_callback: EarlyStopping) -> None:
91
+ """Logs early stopping behavior results (e.g. stopped epoch) as metrics to MLflow."""
92
+ if self.logger is None:
93
+ return None
94
+
95
+ if early_stop_callback is None or early_stop_callback.stopped_epoch == 0:
96
+ return None
97
+
98
+ metrics: dict[str, float] = {
99
+ "stopped_epoch": early_stop_callback.stopped_epoch,
100
+ "restored_epoch": early_stop_callback.stopped_epoch - max(1, early_stop_callback.patience),
101
+ }
102
+
103
+ if hasattr(early_stop_callback, "best_score"):
104
+ metrics["best_score"] = float(early_stop_callback.best_score)
105
+
106
+ if hasattr(early_stop_callback, "wait_count"):
107
+ metrics["wait_count"] = early_stop_callback.wait_count
108
+
109
+ self.logger.log_metrics(metrics)
110
+
111
+ def _resolve_early_stopping_callback(self, trainer: "pl.Trainer") -> EarlyStopping | None:
112
+ if hasattr(trainer, "callbacks"):
113
+ for callback in cast(list[Callback], trainer.callbacks): # pyright: ignore[reportAttributeAccessIssue]
114
+ if isinstance(callback, EarlyStopping):
115
+ return callback
116
+ return None
117
+
118
+ def _log_model_summary(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
119
+ summary = str(ModelSummary(pl_module, max_depth=-1))
120
+ artifact_path = "model_summary.txt"
121
+ if self.logger and self.logger._run_id and self.client:
122
+ with tempfile.TemporaryDirectory(prefix="test", suffix="test", dir=Path.cwd()) as tmp_dir:
123
+ with Path.open(Path(f"{tmp_dir}/{artifact_path}"), "w") as tmp_file_summary:
124
+ tmp_file_summary.write(summary)
125
+ run_id = str(self.logger.run_id)
126
+ self.client.log_artifacts(run_id, tmp_dir, artifact_path)
127
+
128
+ def _log_model(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
129
+ if self.logger and self.logger.run_id and self.client:
130
+ rank_zero_info("Saving the model and uploading to MLFlow!")
131
+ with tempfile.TemporaryDirectory(prefix="test", suffix="test", dir=Path.cwd()) as tmp_dir:
132
+ local_path = Path(tmp_dir) / "model"
133
+ artifact_path = "model"
134
+ mlflow_model = Model(artifact_path=artifact_path, run_id=self.logger.run_id)
135
+ mlflow.pytorch.save_model(
136
+ pytorch_model=pl_module,
137
+ path=local_path,
138
+ conda_env=None,
139
+ mlflow_model=mlflow_model,
140
+ code_paths=None,
141
+ pickle_module=mlflow_pytorch_pickle_module,
142
+ signature=None,
143
+ input_example=None,
144
+ requirements_file=None,
145
+ extra_files=None,
146
+ pip_requirements=None,
147
+ extra_pip_requirements=None,
148
+ )
149
+ self.client.log_artifacts(
150
+ run_id=self.logger.run_id,
151
+ local_dir=tmp_dir,
152
+ artifact_path=artifact_path,
153
+ )
154
+ try:
155
+ self.client._record_logged_model(run_id=self.logger.run_id, mlflow_model=mlflow_model)
156
+ except MlflowException:
157
+ rank_zero_warn(
158
+ f"Logging model metadata to the tracking server {self.logger._tracking_uri} has failed"
159
+ )
160
+
161
+ # info = mlflow_model.get_model_info()
162
+ return None
163
+
164
+ def _print_auto_logged_info(self) -> None:
165
+ if self.logger and self.logger.run_id and self.client:
166
+ run = mlflow.get_run(run_id=self.logger.run_id)
167
+ if run:
168
+ artifacts = [f.path for f in self.client.list_artifacts(run.info.run_id, "model")]
169
+ tags = {k: v for k, v in run.data.tags.items() if not k.startswith("mlflow.")}
170
+ rank_zero_info(f"run_id: {run.info.run_id}")
171
+ rank_zero_info(f"artifacts: {artifacts}")
172
+ rank_zero_info(f"params: {run.data.params}")
173
+ rank_zero_info(f"metrics: {run.data.metrics}")
174
+ rank_zero_info(f"tags: {tags}")
175
+
176
+ def _log_cluster_tags(self) -> None:
177
+ tags = get_databricks_tags()
178
+
179
+ if self.logger and self.logger.run_id and self.client:
180
+ for tag, value in tags.items():
181
+ self.client.set_tag(self.logger.run_id, key=tag, value=value)
182
+
183
+ def _log_dataset_info(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
184
+ if hasattr(trainer, "datamodule") and trainer.datamodule: # pyright: ignore[reportAttributeAccessIssue]
185
+ dm = trainer.datamodule.train_dataloader() # pyright: ignore[reportAttributeAccessIssue]
186
+ dataset = dm.dataset # pyright: ignore[reportAttributeAccessIssue]
187
+ if self.logger and self.logger.run_id and self.client:
188
+ meta_ds = Dataset(
189
+ name=dataset.__class__.__name__,
190
+ digest="",
191
+ source=dm.__class__.__name__,
192
+ source_type="",
193
+ )
194
+ ds_input = DatasetInput(
195
+ dataset=meta_ds,
196
+ tags=[
197
+ InputTag("class", dataset.__class__.__name__),
198
+ ],
199
+ )
200
+ self.client.log_inputs(run_id=self.logger.run_id, datasets=[ds_input])
201
+
202
+ def _patch_device_stats_monitor(self, trainer: "pl.Trainer") -> None:
203
+ def _patched_prefix_metric_keys(
204
+ metrics_dict: dict[str, float], prefix: str, separator: str
205
+ ) -> dict[str, float]:
206
+ return {prefix + separator + k: v for k, v in metrics_dict.items()}
207
+
208
+ def _patched_get_and_log_device_stats(self, trainer: "pl.Trainer", key: str) -> None:
209
+ if not trainer._logger_connector.should_update_logs:
210
+ return
211
+
212
+ device = trainer.strategy.root_device
213
+ if self._cpu_stats is False and device.type == "cpu":
214
+ # cpu stats are disabled
215
+ return
216
+
217
+ device_stats = trainer.accelerator.get_device_stats(device)
218
+
219
+ if self._cpu_stats and device.type != "cpu":
220
+ # Don't query CPU stats twice if CPU is accelerator
221
+ from lightning.pytorch.accelerators.cpu import get_cpu_stats
222
+
223
+ device_stats.update(get_cpu_stats())
224
+
225
+ for logger in trainer.loggers:
226
+ separator = logger.group_separator
227
+ prefixed_device_stats = _patched_prefix_metric_keys(device_stats, f"system/{key}", separator)
228
+ logger.log_metrics(prefixed_device_stats, step=trainer.fit_loop.epoch_loop._batches_that_stepped)
229
+
230
+ patched = False
231
+ if hasattr(trainer, "callbacks"):
232
+ for callback in cast(list[Callback], trainer.callbacks): # pyright: ignore[reportAttributeAccessIssue]
233
+ if isinstance(callback, DeviceStatsMonitor):
234
+ callback._get_and_log_device_stats = _patched_get_and_log_device_stats.__get__(
235
+ callback, DeviceStatsMonitor
236
+ )
237
+ patched = True
238
+ rank_zero_info("Lightning device stats monitoring enabled!")
239
+
240
+ if not patched:
241
+ rank_zero_info("Lightning device stats monitor has not been added to callbacks!")
242
+
243
+ @rank_zero_only
244
+ def setup(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: str) -> None:
245
+ """Called when fit, validate, test, predict, or tune begins."""
246
+ if not self.autologging_disabled:
247
+ rank_zero_info("Starting MLFlow Databricks logging!")
248
+ rank_zero_info("Default auto logging disabled!")
249
+ mlflow.autolog(disable=True)
250
+ self.autologging_disabled = True
251
+
252
+ if trainer.is_global_zero:
253
+ self.logger = self._get_logger(trainer.loggers)
254
+ self._log_cluster_tags()
255
+ self._log_dataset_info(trainer, pl_module)
256
+
257
+ if self.patch_device_monitor:
258
+ self._patch_device_stats_monitor(trainer)
259
+
260
+ if not self._prevent_entry(trainer) and self.logger and self.logger.run_id and self.client:
261
+ self.client.update_run(run_id=self.logger.run_id, status=RunStatus.to_string(RunStatus.RUNNING))
262
+
263
+ @rank_zero_only
264
+ def teardown(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: str) -> None:
265
+ """Called when fit, validate, test, predict, or tune ends."""
266
+ if self._prevent_entry(trainer):
267
+ return None
268
+
269
+ if self.logger and self.logger.run_id and self.client:
270
+ self.client.update_run(run_id=self.logger.run_id, status=RunStatus.to_string(RunStatus.FINISHED))
271
+
272
+ if stage == TrainerFn.FITTING:
273
+ self.client.set_terminated(run_id=self.logger.run_id, status=RunStatus.to_string(RunStatus.FINISHED))
274
+
275
+ def on_fit_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
276
+ """Called when fit begins."""
277
+
278
+ def on_fit_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
279
+ """Called when fit ends."""
280
+
281
+ def on_sanity_check_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
282
+ """Called when the validation sanity check starts."""
283
+
284
+ def on_sanity_check_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
285
+ """Called when the validation sanity check ends."""
286
+
287
+ def on_train_batch_start(
288
+ self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", batch: Any, batch_idx: int
289
+ ) -> None:
290
+ """Called when the train batch begins."""
291
+
292
+ def on_train_batch_end(
293
+ self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", outputs: STEP_OUTPUT, batch: Any, batch_idx: int
294
+ ) -> None:
295
+ """Called when the train batch ends.
296
+
297
+ Note:
298
+ The value ``outputs["loss"]`` here will be the normalized value w.r.t ``accumulate_grad_batches`` of the
299
+ loss returned from ``training_step``.
300
+
301
+ """
302
+
303
+ def on_train_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
304
+ """Called when the train epoch begins."""
305
+
306
+ def on_train_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
307
+ """Called when the train epoch ends.
308
+
309
+ To access all batch outputs at the end of the epoch, you can cache step outputs as an attribute of the
310
+ :class:`lightning.pytorch.core.LightningModule` and access them in this hook:
311
+
312
+ .. code-block:: python
313
+
314
+ class MyLightningModule(L.LightningModule):
315
+ def __init__(self):
316
+ super().__init__()
317
+ self.training_step_outputs = []
318
+
319
+ def training_step(self):
320
+ loss = ...
321
+ self.training_step_outputs.append(loss)
322
+ return loss
323
+
324
+
325
+ class MyCallback(L.Callback):
326
+ def on_train_epoch_end(self, trainer, pl_module):
327
+ # do something with all training_step outputs, for example:
328
+ epoch_mean = torch.stack(pl_module.training_step_outputs).mean()
329
+ pl_module.log("training_epoch_mean", epoch_mean)
330
+ # free up the memory
331
+ pl_module.training_step_outputs.clear()
332
+
333
+ """
334
+ if self.logger:
335
+ metrics = {str(key): float(value) for key, value in trainer.callback_metrics.items()}
336
+ self.logger.log_metrics(metrics, pl_module.current_epoch)
337
+
338
+ def on_validation_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
339
+ """Called when the val epoch begins."""
340
+
341
+ def on_validation_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
342
+ """Called when the val epoch ends."""
343
+
344
+ def on_test_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
345
+ """Called when the test epoch begins."""
346
+
347
+ def on_test_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
348
+ """Called when the test epoch ends."""
349
+
350
+ def on_predict_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
351
+ """Called when the predict epoch begins."""
352
+
353
+ def on_predict_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
354
+ """Called when the predict epoch ends."""
355
+
356
+ def on_validation_batch_start(
357
+ self,
358
+ trainer: "pl.Trainer",
359
+ pl_module: "pl.LightningModule",
360
+ batch: Any,
361
+ batch_idx: int,
362
+ dataloader_idx: int = 0,
363
+ ) -> None:
364
+ """Called when the validation batch begins."""
365
+
366
+ def on_validation_batch_end(
367
+ self,
368
+ trainer: "pl.Trainer",
369
+ pl_module: "pl.LightningModule",
370
+ outputs: STEP_OUTPUT,
371
+ batch: Any,
372
+ batch_idx: int,
373
+ dataloader_idx: int = 0,
374
+ ) -> None:
375
+ """Called when the validation batch ends."""
376
+
377
+ def on_test_batch_start(
378
+ self,
379
+ trainer: "pl.Trainer",
380
+ pl_module: "pl.LightningModule",
381
+ batch: Any,
382
+ batch_idx: int,
383
+ dataloader_idx: int = 0,
384
+ ) -> None:
385
+ """Called when the test batch begins."""
386
+
387
+ def on_test_batch_end(
388
+ self,
389
+ trainer: "pl.Trainer",
390
+ pl_module: "pl.LightningModule",
391
+ outputs: STEP_OUTPUT,
392
+ batch: Any,
393
+ batch_idx: int,
394
+ dataloader_idx: int = 0,
395
+ ) -> None:
396
+ """Called when the test batch ends."""
397
+
398
+ def on_predict_batch_start(
399
+ self,
400
+ trainer: "pl.Trainer",
401
+ pl_module: "pl.LightningModule",
402
+ batch: Any,
403
+ batch_idx: int,
404
+ dataloader_idx: int = 0,
405
+ ) -> None:
406
+ """Called when the predict batch begins."""
407
+
408
+ def on_predict_batch_end(
409
+ self,
410
+ trainer: "pl.Trainer",
411
+ pl_module: "pl.LightningModule",
412
+ outputs: Any,
413
+ batch: Any,
414
+ batch_idx: int,
415
+ dataloader_idx: int = 0,
416
+ ) -> None:
417
+ """Called when the predict batch ends."""
418
+
419
+ @rank_zero_only
420
+ def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
421
+ """Called when the train begins."""
422
+ if self._prevent_entry(trainer):
423
+ return None
424
+
425
+ if self.logger and self.logger.run_id and self.client:
426
+ run_id = str(self.logger.run_id)
427
+
428
+ self.client.set_tag(run_id=run_id, key="Mode", value="training")
429
+
430
+ self.client.log_param(run_id=run_id, key="epochs", value=trainer.max_epochs)
431
+
432
+ if hasattr(trainer, "optimizers"):
433
+ for i, optimizer in enumerate(trainer.optimizers):
434
+ self.client.log_param(
435
+ self.logger.run_id, key=f"optimizer{i}_name", value=self._get_optimizer_name(optimizer)
436
+ )
437
+ defaults = self._get_optimizer_defaults(optimizer)
438
+ for key, value in defaults.items():
439
+ self.client.log_param(self.logger.run_id, key=f"optimizer{i}_{key}", value=str(value))
440
+ # self.client.log_param(
441
+ # self.logger.run_id, key=f"optimizer{i}_defaults", value=str(optimizer.defaults)
442
+ # )
443
+
444
+ callback = self._resolve_early_stopping_callback(trainer)
445
+ if callback:
446
+ self._log_early_stop_params(callback)
447
+
448
+ @rank_zero_only
449
+ def on_train_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
450
+ """Called when the train ends."""
451
+ if self._prevent_entry(trainer):
452
+ return None
453
+
454
+ callback = self._resolve_early_stopping_callback(trainer)
455
+ if callback:
456
+ self._log_early_stop_metrics(callback)
457
+
458
+ self._log_model_summary(trainer, pl_module)
459
+
460
+ self._log_model(trainer, pl_module)
461
+
462
+ @rank_zero_only
463
+ def on_validation_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
464
+ """Called when the validation loop begins."""
465
+ if self.logger and self.logger.run_id and self.client:
466
+ run_id = str(self.logger.run_id)
467
+
468
+ self.client.set_tag(run_id=run_id, key="Mode", value="validating")
469
+
470
+ @rank_zero_only
471
+ def on_validation_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
472
+ """Called when the validation loop ends."""
473
+
474
+ @rank_zero_only
475
+ def on_test_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
476
+ """Called when the test begins."""
477
+ if self._prevent_entry(trainer):
478
+ return None
479
+ if self.logger and self.logger.run_id and self.client:
480
+ self.client.set_tag(self.logger.run_id, key="Mode", value="testing")
481
+
482
+ def on_test_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
483
+ """Called when the test ends."""
484
+ # originally, mlflow.autolog changes the mode to testing here, but we do it in on_test_start
485
+
486
+ def on_predict_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
487
+ """Called when the predict begins."""
488
+
489
+ def on_predict_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
490
+ """Called when predict ends."""
491
+
492
+ @rank_zero_only
493
+ def on_exception(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", exception: BaseException) -> None:
494
+ """Called when any trainer execution is interrupted by an exception."""
495
+ if self._prevent_entry(trainer):
496
+ return
497
+ if self.logger and self.logger.run_id and self.client:
498
+ self.client.set_terminated(run_id=self.logger.run_id, status=RunStatus.to_string(RunStatus.FAILED))
@@ -0,0 +1,5 @@
1
+ from lit_mlflow.__about__ import __version__
2
+
3
+
4
+ def main():
5
+ print(f"lit-mlflow v{__version__}!")
@@ -0,0 +1,76 @@
1
+ import logging
2
+ import os
3
+ from typing import Any, Literal
4
+
5
+ from lightning.fabric.loggers.logger import Logger, rank_zero_experiment
6
+ from lightning.fabric.utilities.rank_zero import (
7
+ rank_zero_info, # type: ignore # noqa: PGH003
8
+ rank_zero_only,
9
+ rank_zero_warn, # type: ignore # noqa: PGH003
10
+ )
11
+ from lightning.pytorch.loggers import MLFlowLogger
12
+ import mlflow
13
+ from mlflow.tracking import MlflowClient
14
+ from mlflow.utils.databricks_utils import get_databricks_run_url, is_in_databricks_notebook
15
+
16
+ from lit_mlflow.utils.dbx import get_experiment_id, is_in_databricks, patch_dbx_credentials
17
+
18
+
19
+ class DbxMLFlowLogger(MLFlowLogger):
20
+ def __init__(
21
+ self,
22
+ experiment_name: str = "lightning_logs",
23
+ run_name: str | None = None,
24
+ tracking_uri: str | None = mlflow.get_tracking_uri(), # os.getenv("MLFLOW_TRACKING_URI"),
25
+ tags: dict[str, Any] | None = None,
26
+ save_dir: str | None = "./mlruns",
27
+ log_model: Literal[True, False, "all"] = False,
28
+ prefix: str = "",
29
+ artifact_location: str | None = None,
30
+ run_id: str | None = None,
31
+ ):
32
+ super().__init__(
33
+ experiment_name, run_name, tracking_uri, tags, save_dir, log_model, prefix, artifact_location, run_id
34
+ )
35
+ if not is_in_databricks():
36
+ rank_zero_warn(f"You are running `{self.__class__.__name__}` outside of Databricks.")
37
+ else:
38
+ self._fix_logging()
39
+ if patch_dbx_credentials():
40
+ client = self.experiment
41
+ if client:
42
+ rank_zero_info("MLflow client created.")
43
+ else:
44
+ rank_zero_warn("Could not retrieve the MLflow client.")
45
+ else:
46
+ rank_zero_warn("Could not patch Databricks credentials.")
47
+
48
+ def _get_url(self) -> str | None:
49
+ run_url = get_databricks_run_url(self._tracking_uri or "databricks", self.run_id or "")
50
+ return run_url
51
+
52
+ @rank_zero_only
53
+ def _fix_logging(self) -> None:
54
+ """Fix the logging level of the Py4J gateway to ERROR to prevent log spam."""
55
+ logging.getLogger("py4j.java_gateway").setLevel(logging.ERROR)
56
+
57
+ @property
58
+ @rank_zero_experiment
59
+ def experiment(self) -> MlflowClient:
60
+ if is_in_databricks_notebook():
61
+ self._experiment_id = get_experiment_id(None)
62
+ return super().experiment
63
+
64
+ @property
65
+ def run_id(self) -> str | None:
66
+ _ = self.experiment
67
+ active_run = mlflow.active_run()
68
+ if active_run:
69
+ self._run_id = active_run.info.run_id or ""
70
+ return self._run_id
71
+
72
+ # @rank_zero_only
73
+ # def finalize(self, status: str = "success") -> None:
74
+ # final = super().finalize(status)
75
+ # rank_zero_info(f"MLflow run URL: {self._get_url()}")
76
+ # return final
File without changes
@@ -0,0 +1,74 @@
1
+ import json
2
+ import os
3
+
4
+ from mlflow.utils.databricks_utils import (
5
+ _get_property_from_spark_context,
6
+ get_notebook_id,
7
+ is_in_databricks_job,
8
+ is_in_databricks_model_serving_environment,
9
+ is_in_databricks_notebook,
10
+ )
11
+
12
+
13
+ def is_in_databricks() -> bool:
14
+ """Check if the code is running in Databricks."""
15
+ return is_in_databricks_notebook() or is_in_databricks_job() or is_in_databricks_model_serving_environment()
16
+
17
+
18
+ def patch_dbx_credentials() -> bool:
19
+ """Patch/propagate the Databricks credentials to the environment variables, required for DDP.
20
+
21
+ Returns:
22
+ Returns True if the values have been patched.
23
+ """
24
+ if is_in_databricks_notebook():
25
+ from databricks_cli.configure.provider import DatabricksConfig, get_config
26
+
27
+ config: DatabricksConfig = get_config()
28
+ if config:
29
+ for k, v in config.__dict__.items():
30
+ if v:
31
+ os.environ[f"DATABRICKS_{k.upper()}"] = str(v)
32
+ return True
33
+ return False
34
+
35
+
36
+ def get_experiment_id(default_name: str | None = None) -> str | None:
37
+ """Returns the experiment Id if running in Databricks.
38
+
39
+ Args:
40
+ default_name: The optional default name to return if not in Databricks.
41
+
42
+ Returns:
43
+ The experiment Id.
44
+ """
45
+ if is_in_databricks_notebook():
46
+ notebook_id = get_notebook_id()
47
+ if notebook_id:
48
+ return notebook_id
49
+ return default_name
50
+
51
+
52
+ def get_databricks_tags() -> dict[str, str]:
53
+ """Return a selection of Databricks cluster tags.
54
+
55
+ Returns:
56
+ The tags dictionary.
57
+ """
58
+ tags = {}
59
+ if is_in_databricks():
60
+ all_tags_str = _get_property_from_spark_context("spark.databricks.clusterUsageTags.clusterAllTags")
61
+ if all_tags_str:
62
+ tags_list = json.loads(all_tags_str)
63
+ for tag in tags_list:
64
+ tags[tag["key"]] = tags_list["value"]
65
+ node_type = _get_property_from_spark_context("spark.databricks.clusterUsageTags.clusterNodeType")
66
+ if node_type:
67
+ tags["clusterNodeType"] = str(node_type)
68
+ runtime_version = os.environ.get("DATABRICKS_RUNTIME_VERSION")
69
+ if runtime_version:
70
+ tags["runtimeVersion"] = str(runtime_version)
71
+ num_worker = _get_property_from_spark_context("spark.databricks.clusterUsageTags.clusterWorkers")
72
+ if num_worker:
73
+ tags["numWorkers"] = int(num_worker)
74
+ return tags