Source code for mlrun.client
# Copyright 2026 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Per-session MLRun client for multi-user / multi-session usage."""
from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field
import mlrun.errors
# No top-level runtime ``import mlrun.*`` — would race with
# ``mlrun.config._populate()``'s deferred ``from mlrun.db import get_run_db``.
# ``mlrun.errors`` is a leaf module (stdlib + aiohttp + requests only) and safe.
[docs]
@dataclass(frozen=True)
class Credentials:
"""User credentials for MLRun API access.
One of: ``token=``, ``username=/password=``, or ``use_env=True``
for legacy env/config/file resolution.
``extra_headers`` adds default headers for this client's requests
(for example ``{"X-IGZ-Authenticator-Kind": "sa"}``). Per-call
``headers=`` and ``Authorization`` always override defaults.
Excluded from equality/hashing to keep the frozen dataclass hashable.
"""
token: str | None = None
username: str | None = None
password: str | None = None
use_env: bool = False
extra_headers: dict[str, str] | None = field(
default=None, compare=False, hash=False
)
def __post_init__(self):
if (self.username is None) != (self.password is None):
raise mlrun.errors.MLRunInvalidArgumentError(
"Basic auth requires both username and password."
)
active = [
name
for name, on in (
("token", self.token is not None),
(
"basic_auth",
self.username is not None or self.password is not None,
),
("env", self.use_env),
)
if on
]
if not active:
raise mlrun.errors.MLRunInvalidArgumentError(
"Credentials need an auth mode. Use Credentials(token=...), "
"Credentials(username=..., password=...), "
"or Credentials(use_env=True)."
)
if len(active) > 1:
raise mlrun.errors.MLRunInvalidArgumentError(
f"Credentials require exactly one auth mode; got: {active}."
)
_active_client: ContextVar[Client | None] = ContextVar("_active_client", default=None)
def get_active_client() -> Client | None:
"""Return the active ``Client`` for this task/thread, or ``None``."""
return _active_client.get()
[docs]
class Client:
"""A per-session MLRun client owning its own ``HTTPRunDB``.
The backend URL is taken from ``mlrun.mlconf.dbpath`` (already populated
by ``import mlrun``); a single MLRun cluster per Python process is
assumed. Only credentials vary per ``Client``.
Example::
client = mlrun.Client(credentials=mlrun.Credentials(token="..."))
with client.session():
project = mlrun.get_or_create_project("my-proj")
"""
def __init__(self, credentials: Credentials):
# Deferred imports — see module-level comment.
from mlrun.config import config
from mlrun.db.httpdb import HTTPRunDB
from mlrun.projects.pipelines import _PipelineContext
self._http_db = HTTPRunDB(config.dbpath, credentials=credentials)
self._pipeline_context = _PipelineContext()
[docs]
@contextmanager
def session(self) -> Iterator[Client]:
"""Bind this client as active for the current contextvars scope."""
token = _active_client.set(self)
try:
yield self
finally:
_active_client.reset(token)