Skip to content

Python API

airflow.providers.pixi.operators.pixi

Run a Python callable inside a Pixi environment, and the base classes of every Pixi operator.

WORKER_PYTHON_VARIABLES module-attribute

WORKER_PYTHON_VARIABLES = ('PYTHONPATH', 'PYTHONHOME', 'PYTHONUSERBASE', 'VIRTUAL_ENV')

Variables of the worker's Python left out of a pixi run, so the environment's Python never imports the worker's packages; env_vars or env can set them for a run.

PixiOperator

PixiOperator(
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
)

Bases: PixiSubprocessMixin, BasePixiPythonOperator

Run a Python callable inside a Pixi environment on the worker.

Accepts every BasePixiPythonOperator and BasePixiOperator argument, plus those of PixiSubprocessMixin and PixiRunEnvMixin: env_vars, env_from_variables, env_from_connections, pixi_conn_id, the cache directory Variables and skip_on_exit_code. The return value is the task's XCom. The output of the run is streamed to the task log; execution_timeout and killing the task stop pixi and everything it started.

Subclasses and mixins can change what runs, the same way as for @task.virtualenv: override get_python_source, and set op_args, op_kwargs or env_vars, or call add_pypi_dependencies, in execute before calling super().execute.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(
    self,
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.env_vars = env_vars
    self.skip_on_exit_code: Container[int] = (
        skip_on_exit_code
        if isinstance(skip_on_exit_code, Container)
        else [skip_on_exit_code]
        if skip_on_exit_code is not None
        else []
    )
    self._process: subprocess.Popen[bytes] | None = None

PixiBranchOperator

PixiBranchOperator(
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
)

Bases: PixiOperator, BranchMixIn

Choose the tasks to follow with a Python callable run inside a Pixi environment on the worker.

The Pixi counterpart of BranchPythonVirtualenvOperator. The callable returns a task id or task group id directly downstream of this task, a list of them, or None. The tasks directly downstream that it does not name are skipped, and the skips spread to the tasks after them as their trigger rules decide; None skips them all. A task group id stands for the group's root tasks. As for BranchPythonOperator, the return value becomes the task's XCom only when nothing is skipped: otherwise Airflow's task runner handles the skips instead.

Accepts every PixiOperator argument; context parameters such as params reach the callable as for PixiOperator. A task id that is not in the DAG fails the task, and so does a return value that is neither a string, a list of strings nor None.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(
    self,
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.env_vars = env_vars
    self.skip_on_exit_code: Container[int] = (
        skip_on_exit_code
        if isinstance(skip_on_exit_code, Container)
        else [skip_on_exit_code]
        if skip_on_exit_code is not None
        else []
    )
    self._process: subprocess.Popen[bytes] | None = None

PixiShortCircuitOperator

PixiShortCircuitOperator(
    *, ignore_downstream_trigger_rules: bool = True, **kwargs: Any
)

Bases: PixiOperator, SkipMixin

Continue the pipeline only if a Python callable run inside a Pixi environment returns a truthy value.

The Pixi counterpart of ShortCircuitOperator. When the callable returns a falsy value, the tasks downstream of this one are skipped, except teardown tasks. As for ShortCircuitOperator, the return value becomes the task's XCom only when nothing is skipped: a truthy one, or a falsy one without downstream tasks.

Accepts every PixiOperator argument; context parameters such as params reach the callable as for PixiOperator. Plus:

Parameters:

  • ignore_downstream_trigger_rules (bool, default: True ) –

    if True (default), skip every task downstream of this one, whatever its trigger rule. If False, skip only the tasks directly downstream, and let the trigger rules of the tasks after them decide whether they run.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(self, *, ignore_downstream_trigger_rules: bool = True, **kwargs: Any) -> None:
    super().__init__(**kwargs)
    self.ignore_downstream_trigger_rules = ignore_downstream_trigger_rules

BasePixiPythonOperator

BasePixiPythonOperator(
    *,
    python_callable: Callable[..., Any] | str,
    op_args: Sequence[Any] | None = None,
    op_kwargs: dict[str, Any] | None = None,
    serializer: Literal["json", "pickle"] = "json",
    **kwargs: Any,
)

Bases: BasePixiOperator

Base class of the Pixi operators that call a Python function inside the environment.

Accepts every BasePixiOperator argument, plus:

Parameters:

  • python_callable (Callable[..., Any] | str) –

    a function, whose source is shipped to the environment and run there, so it must be self-contained (imports inside it, no variables from enclosing functions); or a "module.path:callable_name" string imported in the environment. The manifest's directory is the working directory, so modules next to it are importable.

  • op_args (Sequence[Any] | None, default: None ) –

    positional arguments for the callable (templated).

  • op_kwargs (dict[str, Any] | None, default: None ) –

    keyword arguments for the callable (templated).

  • serializer (Literal['json', 'pickle'], default: 'json' ) –

    how arguments and the return value travel: "json" (default), or "pickle" for values JSON cannot hold, whose types must then be importable on both sides.

    Parameters of the callable named after a key of CONTEXT_KEYS, such as ds, params or run_id, get the task's value when op_args and op_kwargs leave them unset, and a **kwargs parameter gets all of them, as for @task.virtualenv. Dates and datetimes arrive as ISO 8601 strings.

    Subclasses and mixins can change what runs, the same way as for @task.virtualenv: override get_python_source, and set op_args or op_kwargs, or call add_pypi_dependencies, in execute before calling super().execute.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(
    self,
    *,
    python_callable: Callable[..., Any] | str,
    op_args: Sequence[Any] | None = None,
    op_kwargs: dict[str, Any] | None = None,
    serializer: Literal["json", "pickle"] = "json",
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.python_callable = python_callable
    self.op_args = op_args or []
    self.op_kwargs = op_kwargs or {}
    self.serializer = serializer
    if serializer not in ("json", "pickle"):
        raise ValueError(f"serializer must be 'json' or 'pickle', not {serializer!r}")
    validate_callable(python_callable)
    if not isinstance(python_callable, str) and (free := inspect.unwrap(python_callable).__code__.co_freevars):
        raise ValueError(
            f"{python_callable.__name__} uses {', '.join(free)} from an enclosing function, which does not "
            "exist in the Pixi environment; pass it as an argument instead"
        )

get_python_source

get_python_source() -> str

Return the source shipped to the environment when python_callable is a function.

The runner executes it and calls the name of python_callable in the resulting namespace. Task decorators are removed, including the one named by custom_operator_name (@task.<name> of a decorator built on @task.pixi) and this provider's decorator functions used under their own names (@pixi_task(...)), and line numbers kept, so tracebacks point at the DAG file. Override it to wrap the function, for example by appending code that rebinds the name to a wrapper, as for @task.virtualenv.

Source code in src/airflow/providers/pixi/operators/pixi.py
def get_python_source(self) -> str:
    """Return the source shipped to the environment when ``python_callable`` is a function.

    The runner executes it and calls the name of ``python_callable`` in the resulting namespace. Task
    decorators are removed, including the one named by ``custom_operator_name`` (``@task.<name>`` of a
    decorator built on ``@task.pixi``) and this provider's decorator functions used under their own names
    (``@pixi_task(...)``), and line numbers kept, so tracebacks point at the DAG file. Override it to wrap
    the function, for example by appending code that rebinds the name to a wrapper, as for
    ``@task.virtualenv``.
    """
    stripped = {*STRIPPED_DECORATORS, *_DECORATOR_FUNCTIONS}
    operator_name = getattr(self, "custom_operator_name", "")
    if operator_name.startswith("@"):
        stripped.add(operator_name[1:])
    return function_source(self.python_callable, stripped)[0]  # type: ignore[arg-type]

callable_input

callable_input(context: Mapping[str, Any] | None = None) -> bytes

Return the runner's input: the callable, its arguments and the task context, in the serializer's format.

Parameters:

  • context (Mapping[str, Any] | None, default: None ) –

    the task context, of which the runner passes the JSON-safe part (serializable_context) to parameters named after its keys.

Source code in src/airflow/providers/pixi/operators/pixi.py
def callable_input(self, context: Mapping[str, Any] | None = None) -> bytes:
    """Return the runner's input: the callable, its arguments and the task context, in the ``serializer``'s format.

    :param context: the task context, of which the runner passes the JSON-safe part
        ([`serializable_context`][airflow.providers.pixi.utils.context.serializable_context]) to parameters named
        after its keys.
    """
    if isinstance(self.python_callable, str):
        module, _, name = self.python_callable.partition(":")
        spec: dict[str, Any] = {"module": module, "name": name}
    else:
        fn = inspect.unwrap(self.python_callable)
        spec = {
            "source": self.get_python_source(),
            "filename": inspect.getsourcefile(fn) or "<pixi task>",
            "name": fn.__name__,
        }
    spec.update(args=list(self.op_args), kwargs=dict(self.op_kwargs))
    if context_values := serializable_context(context):
        spec["context"] = context_values
    if self.serializer == "pickle":
        return pickle.dumps(spec, protocol=PICKLE_PROTOCOL)
    try:
        return json.dumps(spec).encode()
    except TypeError as e:
        raise TypeError(f"op_args and op_kwargs must be JSON-serializable, or pass serializer='pickle': {e}") from e

BasePixiOperator

BasePixiOperator(
    *,
    pixi_project_path: str | None = None,
    pixi_toml_path: str | None = None,
    environment: str | None = None,
    lock_mode: Literal["locked", "frozen"] | None = None,
    dependencies: dict[str, Any] | Sequence[str] | None = None,
    pypi_dependencies: dict[str, Any] | Sequence[str] | str | None = None,
    channels: Sequence[str] | None = None,
    platforms: Sequence[str] | None = None,
    env_cache_path: str | None = None,
    cleanup_temp_manifest: bool = True,
    pixi_binary: str = "pixi",
    **kwargs: Any,
)

Bases: BaseOperator

Base class of the Pixi operators: chooses the Pixi environment and prepares it.

The environment comes from exactly one of pixi_project_path (a project directory), pixi_toml_path (a pixi.toml or pyproject.toml) or an inline manifest: a list of packages in dependencies / pypi_dependencies, with channels and platforms. Anything else, such as PyPI indexes, features or several environments, belongs in a pixi.toml.

Parameters:

  • pixi_project_path (str | None, default: None ) –

    a Pixi project directory, with a pixi.toml or pyproject.toml (templated).

  • pixi_toml_path (str | None, default: None ) –

    a pixi.toml or pyproject.toml; pixi uses exactly that file (templated).

  • environment (str | None, default: None ) –

    the environment of a project or manifest file to run in, if it defines several (templated). Not for inline manifests, which have only the default environment.

  • lock_mode (Literal['locked', 'frozen'] | None, default: None ) –

    how pixi treats the pixi.lock of a project or manifest file (templated). None (default) runs plain pixi run, which solves again and rewrites the lock file when it no longer matches the manifest. "locked" passes --locked: the run fails if the lock file is out of date. "frozen" passes --frozen: the environment is installed from the lock file as it is, without checking it against the manifest. Not for inline manifests, which have no lock file.

  • dependencies (dict[str, Any] | Sequence[str] | None, default: None ) –

    conda dependencies of an inline manifest, a dict or a list of MatchSpecs.

  • pypi_dependencies (dict[str, Any] | Sequence[str] | str | None, default: None ) –

    PyPI dependencies of an inline manifest (templated): a dict as under [pypi-dependencies] in a pixi.toml, or pip requirement strings as @task.virtualenv takes them, a list or one string that may hold several lines, such as a rendered requirements file. An inline manifest with PyPI packages but no python dependency gets the worker's Python version.

  • channels (Sequence[str] | None, default: None ) –

    channels of an inline manifest; default ["conda-forge"].

  • platforms (Sequence[str] | None, default: None ) –

    platforms of an inline manifest; default default_platforms, the platform of the machine that runs pixi.

  • env_cache_path (str | None, default: None ) –

    keep inline environments in <env_cache_path>/pixi-<hash> and reuse them across runs, like venv_cache_path of PythonVirtualenvOperator. Otherwise an inline environment is built in a temporary directory, removed after the run unless cleanup_temp_manifest=False.

  • cleanup_temp_manifest (bool, default: True ) –

    remove the temporary directory of an inline environment after the run.

  • pixi_binary (str, default: 'pixi' ) –

    the pixi executable, a name on PATH or a path. It must be installed on the workers, in version MIN_PIXI_VERSION or newer; the provider never installs it.

    Where pixi runs on the worker (local_manifest), a relative pixi_project_path, pixi_toml_path or env_cache_path is relative to the directory of the DAG file there, or to the working directory for a task without a DAG.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(
    self,
    *,
    pixi_project_path: str | None = None,
    pixi_toml_path: str | None = None,
    environment: str | None = None,
    lock_mode: Literal["locked", "frozen"] | None = None,
    dependencies: dict[str, Any] | Sequence[str] | None = None,
    pypi_dependencies: dict[str, Any] | Sequence[str] | str | None = None,
    channels: Sequence[str] | None = None,
    platforms: Sequence[str] | None = None,
    env_cache_path: str | None = None,
    cleanup_temp_manifest: bool = True,
    pixi_binary: str = "pixi",
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.pixi_project_path = pixi_project_path
    self.pixi_toml_path = pixi_toml_path
    self.environment = environment
    self.lock_mode = lock_mode
    self.dependencies = dependencies
    self.pypi_dependencies = pypi_dependencies
    self.channels = channels
    self.platforms = platforms
    self.env_cache_path = env_cache_path
    self.cleanup_temp_manifest = cleanup_temp_manifest
    self.pixi_binary = pixi_binary
    self._held_manifest: tuple[str, str] | None = None

    has_inline = bool(dependencies) or bool(pypi_dependencies)
    if sum([pixi_project_path is not None, pixi_toml_path is not None, has_inline]) != 1:
        raise ValueError(
            "Exactly one of pixi_project_path, pixi_toml_path, or inline "
            "(dependencies / pypi_dependencies) must be provided."
        )
    if not _needs_rendering(pypi_dependencies):
        pypi_dependencies_table(pypi_dependencies)
    conda_dependencies(dependencies)
    if not _needs_rendering(environment):
        self._checked_environment(ValueError)
    if not _needs_rendering(lock_mode):
        self._checked_lock_mode(ValueError)

inline_manifest property

inline_manifest: bool

Whether the environment comes from an inline manifest.

Only an inline manifest can be extended with add_pypi_dependencies.

add_pypi_dependencies

add_pypi_dependencies(*requirements: str) -> None

Add pip requirement strings, such as "mytracker>=1", to the PyPI dependencies of the inline manifest.

For subclasses and mixins that need a package in the environment, whichever form the DAG gave pypi_dependencies in: a dict gets the [pypi-dependencies] entries of requirements, pip strings become a list with requirements at the end. pypi_dependencies is assigned a new value, so a value saved before stays as it was and can be restored afterwards.

Raises:

  • AirflowException –

    if the environment comes from pixi_project_path or pixi_toml_path, whose manifest has to list the packages itself.

  • ValueError –

    for a requirement pypi_dependencies does not accept, or a package listed twice.

Source code in src/airflow/providers/pixi/operators/pixi.py
def add_pypi_dependencies(self, *requirements: str) -> None:
    """Add pip requirement strings, such as ``"mytracker>=1"``, to the PyPI dependencies of the inline manifest.

    For subclasses and mixins that need a package in the environment, whichever form the DAG gave
    ``pypi_dependencies`` in: a dict gets the ``[pypi-dependencies]`` entries of ``requirements``, pip
    strings become a list with ``requirements`` at the end. ``pypi_dependencies`` is assigned a new value,
    so a value saved before stays as it was and can be restored afterwards.

    :raises AirflowException: if the environment comes from ``pixi_project_path`` or ``pixi_toml_path``,
        whose manifest has to list the packages itself.
    :raises ValueError: for a requirement ``pypi_dependencies`` does not accept, or a package listed twice.
    """
    if not self.inline_manifest:
        raise AirflowException(
            self._project_pypi_error(list(requirements), self.pixi_project_path or self.pixi_toml_path)
        )
    added = pypi_dependencies_table(list(requirements))
    current = self.pypi_dependencies
    if isinstance(current, dict):
        listed = {canonicalize_name(name) for name in current}
        for name in added:
            if canonicalize_name(name) in listed:
                raise ValueError(f"{name} is listed twice in pypi_dependencies")
        self.pypi_dependencies = {**current, **added}
        return
    extended = [*([current] if isinstance(current, str) else current or []), *requirements]
    if not _needs_rendering(extended):
        pypi_dependencies_table(extended)
    self.pypi_dependencies = extended

inline_manifest_toml

inline_manifest_toml() -> str

Return the pixi.toml of an inline manifest, from the rendered arguments.

Source code in src/airflow/providers/pixi/operators/pixi.py
def inline_manifest_toml(self) -> str:
    """Return the ``pixi.toml`` of an inline manifest, from the rendered arguments."""
    pypi_dependencies = pypi_dependencies_table(self.pypi_dependencies)
    dependencies = conda_dependencies(self.dependencies)
    if pypi_dependencies and "python" not in dependencies:
        dependencies["python"] = f"{sys.version_info.major}.{sys.version_info.minor}.*"
    return build_pixi_toml(
        channels=self.channels or DEFAULT_CHANNELS,
        platforms=self.platforms or self.default_platforms(),
        dependencies=dependencies,
        pypi_dependencies=pypi_dependencies,
    )

default_platforms

default_platforms() -> list[str]

Return the platforms of an inline manifest without platforms: the one of the machine running pixi.

Called when the task runs. Pixi solves the environment for every platform of the manifest, and a package missing on one of them fails the solve, so only the platform that runs it is listed.

Source code in src/airflow/providers/pixi/operators/pixi.py
def default_platforms(self) -> list[str]:
    """Return the platforms of an inline manifest without ``platforms``: the one of the machine running pixi.

    Called when the task runs. Pixi solves the environment for every platform of the manifest, and a package
    missing on one of them fails the solve, so only the platform that runs it is listed.
    """
    return [local_platform()]

pixi_run_options

pixi_run_options() -> list[str]

Return the options of pixi run that follow --manifest-path: the environment and the lock mode.

Called when the task runs, as templated fields are then rendered.

Source code in src/airflow/providers/pixi/operators/pixi.py
def pixi_run_options(self) -> list[str]:
    """Return the options of ``pixi run`` that follow ``--manifest-path``: the environment and the lock mode.

    Called when the task runs, as templated fields are then rendered.
    """
    environment = self._checked_environment(AirflowException)
    options = ["--environment", environment] if environment else []
    lock_mode = self._checked_lock_mode(AirflowException)
    if lock_mode:
        options.append(f"--{lock_mode}")
    return options

pixi_run_command

pixi_run_command(pixi: str, manifest: str) -> list[str]

Return pixi run with the manifest and pixi_run_options.

The command to run in the environment goes after it.

Source code in src/airflow/providers/pixi/operators/pixi.py
def pixi_run_command(self, pixi: str, manifest: str) -> list[str]:
    """Return ``pixi run`` with the manifest and [`pixi_run_options`][pixi_run_options].

    The command to run in the environment goes after it.
    """
    return [pixi, "run", "--manifest-path", manifest, *self.pixi_run_options()]

local_manifest

local_manifest() -> Iterator[tuple[str, str]]

Yield the manifest to pass to pixi on the worker and the directory to run in.

An inline manifest is written to env_cache_path or to a temporary directory, removed afterwards unless cleanup_temp_manifest=False. Runs at execution time, so templated paths are rendered. Relative paths are resolved against the directory of the DAG file.

Source code in src/airflow/providers/pixi/operators/pixi.py
@contextlib.contextmanager
def local_manifest(self) -> Iterator[tuple[str, str]]:
    """Yield the manifest to pass to pixi on the worker and the directory to run in.

    An inline manifest is written to ``env_cache_path`` or to a temporary directory, removed afterwards
    unless ``cleanup_temp_manifest=False``. Runs at execution time, so templated paths are rendered. Relative
    paths are resolved against the directory of the DAG file.
    """
    if self._held_manifest is not None:
        yield self._held_manifest
        return
    if self.pixi_project_path is not None:
        project = self._local_path(self.pixi_project_path)
        self._check_project_manifest(project)
        yield project, project
        return
    if self.pixi_toml_path is not None:
        manifest = self._local_path(self.pixi_toml_path)
        self._check_project_manifest(manifest)
        yield manifest, os.path.dirname(manifest)
        return
    toml = self.inline_manifest_toml()
    if self.env_cache_path:
        digest = hashlib.sha256(toml.encode()).hexdigest()[:16]
        env_dir = os.path.join(self._local_path(self.env_cache_path), f"pixi-{digest}")
        os.makedirs(env_dir, exist_ok=True)
        manifest = os.path.join(env_dir, "pixi.toml")
        if not os.path.exists(manifest):
            _write_atomic(manifest, toml)
        yield manifest, env_dir
        return
    env_dir = tempfile.mkdtemp(prefix="airflow_pixi_")
    try:
        manifest = os.path.join(env_dir, "pixi.toml")
        Path(manifest).write_text(toml)
        yield manifest, env_dir
    finally:
        if self.cleanup_temp_manifest:
            shutil.rmtree(env_dir, ignore_errors=True)

holding_manifest

holding_manifest() -> Iterator[None]

Keep one local_manifest for every use inside, such as the pokes of one sensor run.

Source code in src/airflow/providers/pixi/operators/pixi.py
@contextlib.contextmanager
def holding_manifest(self) -> Iterator[None]:
    """Keep one [`local_manifest`][local_manifest] for every use inside, such as the pokes of one sensor run."""
    with self.local_manifest() as held:
        self._held_manifest = held
        try:
            yield
        finally:
            self._held_manifest = None

PixiSubprocessMixin

PixiSubprocessMixin(
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
)

Bases: PixiRunEnvMixin

Run the runner with pixi on the worker, streaming its output to the task log.

Accepts every PixiRunEnvMixin argument, plus:

Parameters:

  • env_vars (dict[str, str] | None, default: None ) –

    environment variables for the run, on top of all others. Not templated, so secrets set here are never rendered into the UI.

  • skip_on_exit_code (int | Container[int] | None, default: None ) –

    exit codes of pixi run that skip the task instead of failing it, as for PythonVirtualenvOperator. The callable chooses one with sys.exit(code); an exception it raises exits with 1, and so does pixi when it cannot prepare the environment.

    An exception raised by the callable fails the task with PixiCallableError, which names the callable and the exception; other failures of the run fail it with pixi's exit code and the last lines of output. execution_timeout and killing the task stop pixi and everything it started.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(
    self,
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.env_vars = env_vars
    self.skip_on_exit_code: Container[int] = (
        skip_on_exit_code
        if isinstance(skip_on_exit_code, Container)
        else [skip_on_exit_code]
        if skip_on_exit_code is not None
        else []
    )
    self._process: subprocess.Popen[bytes] | None = None

run_callable

run_callable(context: Mapping[str, Any] | None = None) -> Any

Call python_callable in the environment and return its result.

Parameters:

  • context (Mapping[str, Any] | None, default: None ) –

    the task context, for the callable's parameters named after its keys.

Source code in src/airflow/providers/pixi/operators/pixi.py
def run_callable(self: Any, context: Mapping[str, Any] | None = None) -> Any:
    """Call ``python_callable`` in the environment and return its result.

    :param context: the task context, for the callable's parameters named after its keys.
    """
    pixi = resolve_pixi(self.pixi_binary)
    with (
        self.local_manifest() as (manifest, cwd),
        tempfile.TemporaryDirectory(prefix="airflow_pixi_io_") as io,
        self.pixi_run_env(os.environ, self.env_vars) as env,
    ):
        env.setdefault("PYTHONUNBUFFERED", "1")  # the callable's output reaches the log as it prints
        input_path, output_path = os.path.join(io, "input"), os.path.join(io, "output")
        Path(input_path).write_bytes(self.callable_input(context))
        command = self.pixi_run_command(pixi, manifest)
        self.log.info("Running %s in %s", " ".join(command), cwd)
        returncode, tail = self._run(
            [*command, "python", "-c", RUNNER_SCRIPT, self.serializer, input_path, output_path], cwd, env
        )
        if returncode in self.skip_on_exit_code:
            raise AirflowSkipException(f"pixi run exited with code {returncode}, which skips the task")
        if returncode != 0:
            error_path = Path(output_path + ERROR_SUFFIX)
            if error_path.exists():
                raise self._callable_error(json.loads(error_path.read_text()))
            raise AirflowException(f"pixi run exited with code {returncode}:\n" + "\n".join(tail))
        if not os.path.exists(output_path):
            raise self._missing_result()
        if self.serializer == "pickle":
            return pickle.loads(Path(output_path).read_bytes())
        return json.loads(Path(output_path).read_text())

PixiRunEnvMixin

PixiRunEnvMixin(
    *,
    env_from_variables: dict[str, str] | None = None,
    env_from_connections: dict[str, str] | None = None,
    pixi_conn_id: str | Sequence[str] | None = None,
    pixi_cache_dir_variable: str | None = None,
    uv_cache_dir_variable: str | None = None,
    pip_cache_dir_variable: str | None = None,
    **kwargs: Any,
)

Bases: BaseOperator

The environment variables of a pixi run on the worker, from Airflow Variables and Connections.

Everything is read when the task runs, never rendered as a template.

Parameters:

  • env_from_variables (dict[str, str] | None, default: None ) –

    environment variable name to the key of an Airflow Variable, such as {"API_URL": "api_url"}. A missing Variable fails the task.

  • env_from_connections (dict[str, str] | None, default: None ) –

    environment variable name to "conn_id" (the connection's URI), "conn_id.<field>" (host, login, password, schema, port, or extra as a JSON string) or "conn_id.extra.<key>" (one key of the extra), such as {"DB_PASSWORD": "warehouse.password"}. A missing connection or field fails the task. Passwords, URIs and extras are masked in the task log.

  • pixi_conn_id (str | Sequence[str] | None, default: None ) –

    one or several connections of type pixi (PixiHook) with credentials for private conda channels or PyPI indexes. Written to temporary files for the run, which RATTLER_AUTH_FILE and NETRC point at.

  • pixi_cache_dir_variable (str | None, default: None ) –

    key of an Airflow Variable holding PIXI_CACHE_DIR for the run (templated); likewise uv_cache_dir_variable (UV_CACHE_DIR) and pip_cache_dir_variable (PIP_CACHE_DIR). A missing Variable leaves the environment variable unset.

    pixi_run_env builds the run's environment from them.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(
    self,
    *,
    env_from_variables: dict[str, str] | None = None,
    env_from_connections: dict[str, str] | None = None,
    pixi_conn_id: str | Sequence[str] | None = None,
    pixi_cache_dir_variable: str | None = None,
    uv_cache_dir_variable: str | None = None,
    pip_cache_dir_variable: str | None = None,
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    check_env_sources(env_from_variables, env_from_connections)
    self.env_from_variables = env_from_variables
    self.env_from_connections = env_from_connections
    self.pixi_conn_id = pixi_conn_id
    self.pixi_cache_dir_variable = pixi_cache_dir_variable
    self.uv_cache_dir_variable = uv_cache_dir_variable
    self.pip_cache_dir_variable = pip_cache_dir_variable

pixi_run_env

pixi_run_env(
    base: Mapping[str, str], overrides: Mapping[str, str] | None = None
) -> Iterator[dict[str, str]]

Yield the environment of a pixi run, valid until the block exits.

In increasing precedence: base (usually the worker's environment) without the worker's Python settings (WORKER_PYTHON_VARIABLES) and with PYTHONNOUSERSITE=1, so the environment's Python imports only what the Pixi environment has; the cache directory Variables; env_from_variables and env_from_connections; then overrides (such as env_vars), which can set those Python variables again. With pixi_conn_id, RATTLER_AUTH_FILE and NETRC point at temporary files with the credentials, merged with the files these variables named before (see pixi_auth_env), and removed when the block exits.

Source code in src/airflow/providers/pixi/operators/pixi.py
@contextlib.contextmanager
def pixi_run_env(
    self, base: Mapping[str, str], overrides: Mapping[str, str] | None = None
) -> Iterator[dict[str, str]]:
    """Yield the environment of a pixi run, valid until the block exits.

    In increasing precedence: ``base`` (usually the worker's environment) without the worker's Python settings
    ([`WORKER_PYTHON_VARIABLES`][WORKER_PYTHON_VARIABLES]) and with ``PYTHONNOUSERSITE=1``, so the environment's
    Python imports only what the Pixi environment has; the cache directory Variables; ``env_from_variables`` and
    ``env_from_connections``; then ``overrides`` (such as ``env_vars``), which can set those Python variables again.
    With ``pixi_conn_id``, ``RATTLER_AUTH_FILE`` and ``NETRC`` point at temporary files with the credentials, merged
    with the files these variables named before (see
    [`pixi_auth_env`][airflow.providers.pixi.hooks.pixi.pixi_auth_env]), and removed when the block exits.
    """
    env = {name: value for name, value in base.items() if name not in WORKER_PYTHON_VARIABLES}
    env["PYTHONNOUSERSITE"] = "1"
    env.update(
        resolve_env(
            optional_variables={
                "PIXI_CACHE_DIR": self.pixi_cache_dir_variable,
                "UV_CACHE_DIR": self.uv_cache_dir_variable,
                "PIP_CACHE_DIR": self.pip_cache_dir_variable,
            },
            env_from_variables=self.env_from_variables,
            env_from_connections=self.env_from_connections,
        )
    )
    env.update(overrides or {})
    with pixi_auth_env(self.pixi_conn_id, env) as auth:
        env.update(auth)
        yield env

airflow.providers.pixi.operators.bash

Run a Bash command inside a Pixi environment.

PixiBashOperator

PixiBashOperator(*, env_vars: dict[str, str] | None = None, **kwargs: Any)

Bases: PixiRunEnvMixin, BasePixiOperator, BashOperator

Run a Bash command inside a Pixi environment, like BashOperator.

The command runs as pixi run --manifest-path <manifest> [--environment <env>] [--locked | --frozen] bash -c <command>, so every part of it, pipes and && included, sees the activated environment. Accepts every BashOperator argument (bash_command, env, append_env, cwd, skip_on_exit_code, output_processor, ...), every BasePixiOperator argument for the environment, and the PixiRunEnvMixin arguments env_from_variables, env_from_connections, pixi_conn_id and the cache directory Variables, as PixiOperator does. cwd defaults to the manifest's directory. The last line of output is the task's XCom, as for BashOperator.

Parameters:

  • env_vars (dict[str, str] | None, default: None ) –

    environment variables for the run, on top of all others, as for PixiOperator. Not templated, so secrets set here are never rendered into the UI.

    The environment of the run, in increasing precedence: the worker's environment, unless env is set without append_env=True; the cache directory Variables; env_from_variables and env_from_connections; env; env_vars. env replaces the worker's environment unless append_env=True, as for BashOperator; pixi needs at least HOME and PATH, so pass append_env=True with env.

Source code in src/airflow/providers/pixi/operators/bash.py
def __init__(self, *, env_vars: dict[str, str] | None = None, **kwargs: Any) -> None:
    super().__init__(**kwargs)
    self.env_vars = env_vars
    self._env_stack: contextlib.ExitStack | None = None

pixi_command

pixi_command(pixi: str, manifest: str) -> list[str]

Return the command that runs in place of bash_command: pixi run ... bash -c <bash_command>.

Called when the task runs, as templated fields are then rendered. Override it to run something else through pixi, as PixiTaskOperator does.

Source code in src/airflow/providers/pixi/operators/bash.py
def pixi_command(self, pixi: str, manifest: str) -> list[str]:
    """Return the command that runs in place of ``bash_command``: ``pixi run ... bash -c <bash_command>``.

    Called when the task runs, as templated fields are then rendered. Override it to run something else
    through pixi, as [`PixiTaskOperator`][airflow.providers.pixi.operators.task.PixiTaskOperator] does.
    """
    return [*self.pixi_run_command(pixi, manifest), "bash", "-c", str(self.bash_command)]

get_env

get_env(context: Any) -> dict[str, str]

Return the environment of the run: BashOperator's, with the variables of this operator merged in.

Called by execute; see the class docstring for the precedence. Files written for pixi_conn_id exist until execute returns.

Source code in src/airflow/providers/pixi/operators/bash.py
def get_env(self, context: Any) -> dict[str, str]:
    """Return the environment of the run: ``BashOperator``'s, with the variables of this operator merged in.

    Called by ``execute``; see the class docstring for the precedence. Files written for ``pixi_conn_id``
    exist until ``execute`` returns.
    """
    if self._env_stack is None:
        raise AirflowException(f"{type(self).__name__}.get_env can only be called from execute")
    env = super().get_env(context)
    overrides = {**(self.env or {}), **(self.env_vars or {})}
    return self._env_stack.enter_context(self.pixi_run_env(env, overrides))

airflow.providers.pixi.operators.task

Run a task defined in a Pixi manifest.

PixiTaskOperator

PixiTaskOperator(*, task: str, task_args: Sequence[str] | None = None, **kwargs: Any)

Bases: PixiBashOperator

Run a task from the [tasks] of a Pixi manifest, as pixi run <task> [task_args...] does.

The command is pixi run --manifest-path <manifest> [--environment <env>] [--locked | --frozen] <task> <task_args...>, run through BashOperator's bash -c with each argument shell-quoted, so each one reaches pixi unchanged, without word splitting or $ expansion by bash. Pixi appends them to the task's command when it declares no args. A task that declares args gets their values substituted into its cmd without quoting, which pixi's task shell then interprets, so don't pass it values someone else controls, such as dag_run.conf. A name that is not a task is run as a command of the environment, as pixi run does.

Accepts every PixiBashOperator argument except bash_command: env, append_env, env_vars, env_from_variables, env_from_connections, pixi_conn_id, the cache directory Variables, skip_on_exit_code (default 99), output_processor and cwd, which defaults to the manifest's directory. The last line of output is the task's XCom. The environment comes from pixi_project_path or pixi_toml_path: an inline manifest has no tasks.

Parameters:

  • task (str) –

    the name of the task (templated).

  • task_args (Sequence[str] | None, default: None ) –

    arguments for the task, a list of strings (templated), or anything that resolves to one when the task runs, such as the output of another task.

Source code in src/airflow/providers/pixi/operators/task.py
def __init__(self, *, task: str, task_args: Sequence[str] | None = None, **kwargs: Any) -> None:
    super().__init__(bash_command=SET_DURING_EXECUTION, **kwargs)
    if task is None or (isinstance(task, str) and not task.strip()):
        raise ValueError(f"task must be the name of a task of the manifest, not {task!r}")
    if isinstance(task_args, str) and not _needs_rendering(task_args):
        raise TypeError(f"task_args must be a list of arguments, such as [{task_args!r}], not a string")
    if self.inline_manifest:
        raise ValueError(
            "PixiTaskOperator runs a task of the manifest, which an inline manifest cannot define; "
            "use pixi_project_path or pixi_toml_path"
        )
    self.task = task
    self.task_args = list(task_args) if isinstance(task_args, (list, tuple)) else task_args or []

pixi_command

pixi_command(pixi: str, manifest: str) -> list[str]

Return pixi run ... <task> <task_args...>, from the rendered task and task_args.

Source code in src/airflow/providers/pixi/operators/task.py
def pixi_command(self, pixi: str, manifest: str) -> list[str]:
    """Return ``pixi run ... <task> <task_args...>``, from the rendered ``task`` and ``task_args``."""
    task = str(self.task).strip()
    if not task:
        raise AirflowException("task rendered to an empty name")
    if not isinstance(self.task_args, (list, tuple)):
        raise AirflowException(f"task_args must render to a list, not {type(self.task_args).__name__}")
    return [*self.pixi_run_command(pixi, manifest), task, *map(str, self.task_args)]

airflow.providers.pixi.operators.kubernetes

Run a Python callable inside a Pixi environment in a Kubernetes pod.

Requires pip install "apache-airflow-providers-pixi[cncf.kubernetes]".

DEFAULT_IMAGE module-attribute

DEFAULT_IMAGE = f'ghcr.io/prefix-dev/pixi:{MIN_PIXI_VERSION}'

The official pixi image in the minimum supported version.

DEFAULT_POD_PLATFORMS module-attribute

DEFAULT_POD_PLATFORMS = ('linux-64', 'linux-aarch64')

The platforms of an inline manifest without platforms: those of the Linux nodes a pod can land on.

MAX_ENV_VALUE_BYTES module-attribute

MAX_ENV_VALUE_BYTES = 120 * 1024

The largest value one of the pod's environment variables may have.

Linux refuses to start a process with an environment string longer than MAX_ARG_STRLEN (128 KiB), so the pod would fail to start; the margin leaves room for the variable's name.

PixiKubernetesPodOperator

PixiKubernetesPodOperator(
    *, image: str | None = DEFAULT_IMAGE, do_xcom_push: bool = True, **kwargs: Any
)

Bases: BasePixiPythonOperator, KubernetesPodOperator

Run a Python callable inside a Pixi environment in a Kubernetes pod.

Accepts every BasePixiPythonOperator argument and every KubernetesPodOperator argument except cmds and arguments, which run the callable. Paths (pixi_project_path, pixi_toml_path, env_cache_path) are paths in the pod, a relative one relative to the image's working directory, and pixi_binary is the pixi executable of the image.

Parameters:

  • image (str | None, default: DEFAULT_IMAGE ) –

    an image with pixi MIN_PIXI_VERSION or newer, GNU sort and base64, and the project for pixi_project_path. Default DEFAULT_IMAGE.

    The function's source, its arguments, the task context and an inline manifest reach the pod in environment variables, so they are visible in the pod's spec, and each must stay under MAX_ENV_VALUE_BYTES. The return value comes back through the XCom sidecar (do_xcom_push defaults to True here), also with deferrable=True. An inline manifest without platforms is solved for DEFAULT_POD_PLATFORMS, in the pod on each run; mount a volume at env_cache_path to reuse it. An exception raised by the callable fails the task with PixiCallableError.

Source code in src/airflow/providers/pixi/operators/kubernetes.py
def __init__(self, *, image: str | None = DEFAULT_IMAGE, do_xcom_push: bool = True, **kwargs: Any) -> None:
    super().__init__(image=image, do_xcom_push=do_xcom_push, **kwargs)

xcom_dir class-attribute instance-attribute

xcom_dir = PodDefaults.XCOM_MOUNT_PATH

Where the KubernetesPodOperator sidecar reads return.json: the path it mounts its volume at.

termination_message_path class-attribute instance-attribute

termination_message_path = '/dev/termination-log'

Where Kubernetes reads the container's termination message, its default terminationMessagePath.

default_platforms

default_platforms() -> list[str]

Return DEFAULT_POD_PLATFORMS: pods run on Linux nodes, not on the worker.

Source code in src/airflow/providers/pixi/operators/kubernetes.py
def default_platforms(self) -> list[str]:
    """Return [`DEFAULT_POD_PLATFORMS`][DEFAULT_POD_PLATFORMS]: pods run on Linux nodes, not on the worker."""
    return list(DEFAULT_POD_PLATFORMS)

pod_script

pod_script() -> str

Return the shell script the pod runs: check pixi, write the inputs, run the callable.

Source code in src/airflow/providers/pixi/operators/kubernetes.py
def pod_script(self) -> str:
    """Return the shell script the pod runs: check pixi, write the inputs, run the callable."""
    min_version = shlex.quote(str(MIN_PIXI_VERSION))
    pixi = shlex.quote(self.pixi_binary)
    lines = [
        "set -eu",
        f'version="$({pixi} --version)"',
        'version="${version##* }"',
        f'if [ "$(printf "%s\\n%s\\n" {min_version} "$version" | sort -V | head -n 1)" != {min_version} ]; then',
        f'  echo "pixi $version in the image is older than {MIN_PIXI_VERSION}, which this provider needs" >&2',
        "  exit 1",
        "fi",
        'work="$(mktemp -d)"',
        f'printf %s "${_INPUT_ENV}" | base64 -d > "$work/input"',
    ]
    if self.pixi_project_path is not None:
        self._check_project_manifest(self.pixi_project_path)
        lines += [f"manifest={_pod_path(self.pixi_project_path)}", 'cd "$manifest"']
    elif self.pixi_toml_path is not None:
        self._check_project_manifest(self.pixi_toml_path)
        directory = posixpath.dirname(self.pixi_toml_path) or "."
        lines += [f"manifest={_pod_path(self.pixi_toml_path)}", f"cd {shlex.quote(directory)}"]
    else:
        if self.env_cache_path:
            digest = hashlib.sha256(self.inline_manifest_toml().encode()).hexdigest()[:16]
            lines.append(f"dir={_pod_path(posixpath.join(self.env_cache_path, f'pixi-{digest}'))}")
        else:
            lines.append('dir="$work/env"')
        lines += [
            'mkdir -p "$dir"',
            'if [ ! -f "$dir/pixi.toml" ]; then',
            f'  printf %s "${_MANIFEST_ENV}" | base64 -d > "$dir/.pixi.toml.$$"',
            '  mv "$dir/.pixi.toml.$$" "$dir/pixi.toml"',
            "fi",
            'manifest="$dir/pixi.toml"',
            'cd "$dir"',
        ]
    options = "".join(f" {shlex.quote(option)}" for option in self.pixi_run_options())
    run = f'{pixi} run --manifest-path "$manifest"{options} python -c "${_RUNNER_ENV}"'
    lines += [
        f'{run} {shlex.quote(self.serializer)} "$work/input" "$work/output" json',
        f'[ -f "$work/output" ] || {{ echo {shlex.quote(str(self._missing_result()))} >&2; exit 1; }}',
    ]
    if self.do_xcom_push:
        xcom_dir = shlex.quote(self.xcom_dir)
        lines.append(f'mkdir -p {xcom_dir} && cp "$work/output" {xcom_dir}/return.json')
    return "\n".join(lines) + "\n"

pod_env_vars

pod_env_vars(context: Mapping[str, Any] | None = None) -> list[V1EnvVar]

Return the environment variables that carry the callable, its arguments and an inline manifest.

Parameters:

  • context (Mapping[str, Any] | None, default: None ) –

    the task context, of which the JSON-safe part reaches the callable.

Raises:

  • AirflowException –

    if the input or the manifest is longer than MAX_ENV_VALUE_BYTES, so the task fails before the pod is created.

Source code in src/airflow/providers/pixi/operators/kubernetes.py
def pod_env_vars(self, context: Mapping[str, Any] | None = None) -> list[k8s.V1EnvVar]:
    """Return the environment variables that carry the callable, its arguments and an inline manifest.

    :param context: the task context, of which the JSON-safe part reaches the callable.
    :raises AirflowException: if the input or the manifest is longer than
        [`MAX_ENV_VALUE_BYTES`][MAX_ENV_VALUE_BYTES], so the task fails before the pod is created.
    """
    env = {
        _INPUT_ENV: base64.b64encode(self.callable_input(context)).decode(),
        _RUNNER_ENV: RUNNER_SCRIPT,
        TERMINATION_LOG_ENV: self.termination_message_path,
    }
    self._check_env_size(
        _INPUT_ENV,
        env[_INPUT_ENV],
        "The callable with its op_args, op_kwargs and the task context",
        "Pass smaller arguments, such as a path or URL instead of the data, or put the code in the image and use "
        "pixi_project_path with a 'module.path:callable_name' callable.",
    )
    if self.inline_manifest:
        env[_MANIFEST_ENV] = base64.b64encode(self.inline_manifest_toml().encode()).decode()
        self._check_env_size(
            _MANIFEST_ENV,
            env[_MANIFEST_ENV],
            "The inline manifest",
            "Put the environment in a project in the image and use pixi_project_path.",
        )
    return [k8s.V1EnvVar(name=name, value=value) for name, value in env.items()]

airflow.providers.pixi.sensors.pixi

Wait for a Python callable running inside a Pixi environment to report success.

PixiSensor

PixiSensor(
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
)

Bases: PixiSubprocessMixin, BasePixiPythonOperator, BaseSensorOperator

Wait for a Python callable, run inside a Pixi environment on each poke, to return a truthy value.

Like PythonSensor, but the callable runs in the Pixi environment, as for PixiOperator, whose arguments it accepts, together with every BaseSensorOperator argument (poke_interval, timeout, mode, soft_fail, ...).

To finish with an XCom value, return {"is_done": True, "xcom_value": value}, the fields of PokeReturnValue, which Airflow cannot pass into the environment itself.

In mode="poke" the environment is prepared once for all pokes of a run. In mode="reschedule" each poke is a new run, so pass env_cache_path to keep an inline environment between them.

Source code in src/airflow/providers/pixi/operators/pixi.py
def __init__(
    self,
    *,
    env_vars: dict[str, str] | None = None,
    skip_on_exit_code: int | Container[int] | None = None,
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.env_vars = env_vars
    self.skip_on_exit_code: Container[int] = (
        skip_on_exit_code
        if isinstance(skip_on_exit_code, Container)
        else [skip_on_exit_code]
        if skip_on_exit_code is not None
        else []
    )
    self._process: subprocess.Popen[bytes] | None = None

airflow.providers.pixi.decorators.pixi

@task.pixi: run the function inside a Pixi environment.

PixiDecoratedOperator

PixiDecoratedOperator(
    *,
    python_callable: Callable[..., Any],
    op_args: Sequence[Any] | None = None,
    op_kwargs: Mapping[str, Any] | None = None,
    **kwargs: Any,
)

Bases: DecoratedOperator, PixiOperator

@task.pixi: run the function inside a Pixi environment.

Source code in src/airflow/providers/pixi/decorators/pixi.py
def __init__(
    self,
    *,
    python_callable: Callable[..., Any],
    op_args: Sequence[Any] | None = None,
    op_kwargs: Mapping[str, Any] | None = None,
    **kwargs: Any,
) -> None:
    # DecoratedOperator sets op_args/op_kwargs itself, then calls the upstream
    # __init__ with kwargs_to_upstream; PixiOperator must see the same values
    # or it resets them and XComArg dependencies are lost.
    super().__init__(
        kwargs_to_upstream={
            "python_callable": python_callable,
            "op_args": op_args,
            "op_kwargs": op_kwargs,
        },
        python_callable=python_callable,
        op_args=op_args,
        op_kwargs=op_kwargs,
        **kwargs,
    )

pixi_task

pixi_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
)

@task.pixi: run the function inside a Pixi environment.

Accepts every PixiOperator argument. Like @task.virtualenv, the function's source is shipped to the environment, so it must be self-contained: imports go inside the function.

Source code in src/airflow/providers/pixi/decorators/pixi.py
def pixi_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
):
    """``@task.pixi``: run the function inside a Pixi environment.

    Accepts every ``PixiOperator`` argument. Like ``@task.virtualenv``, the function's source is
    shipped to the environment, so it must be self-contained: imports go inside the function.
    """
    return task_decorator_factory(
        python_callable=python_callable,
        multiple_outputs=multiple_outputs,
        decorated_operator_class=PixiDecoratedOperator,
        **kwargs,
    )

airflow.providers.pixi.decorators.bash

@task.pixi_bash: run the Bash command a function returns inside a Pixi environment.

pixi_bash_task

pixi_bash_task(python_callable: Callable[..., Any] | None = None, **kwargs: Any)

@task.pixi_bash: run the Bash command the function returns inside a Pixi environment.

Like @task.bash: the function runs on the worker and returns the command, which is rendered as a template and then run with pixi run ... bash -c. Accepts every PixiBashOperator argument.

Source code in src/airflow/providers/pixi/decorators/bash.py
def pixi_bash_task(python_callable: Callable[..., Any] | None = None, **kwargs: Any):
    """``@task.pixi_bash``: run the Bash command the function returns inside a Pixi environment.

    Like ``@task.bash``: the function runs on the worker and returns the command, which is rendered as a
    template and then run with ``pixi run ... bash -c``. Accepts every ``PixiBashOperator`` argument.
    """
    return task_decorator_factory(
        python_callable=python_callable,
        decorated_operator_class=PixiBashDecoratedOperator,
        **kwargs,
    )

airflow.providers.pixi.decorators.kubernetes

@task.pixi_kubernetes: run the function inside a Pixi environment in a Kubernetes pod.

pixi_kubernetes_task

pixi_kubernetes_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
)

@task.pixi_kubernetes: run the function inside a Pixi environment in a Kubernetes pod.

Accepts every PixiKubernetesPodOperator argument. As for @task.pixi, the function's source is shipped, so it must be self-contained. Requires pip install "apache-airflow-providers-pixi[cncf.kubernetes]".

Source code in src/airflow/providers/pixi/decorators/kubernetes.py
def pixi_kubernetes_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
):
    """``@task.pixi_kubernetes``: run the function inside a Pixi environment in a Kubernetes pod.

    Accepts every ``PixiKubernetesPodOperator`` argument. As for ``@task.pixi``, the function's source is
    shipped, so it must be self-contained. Requires
    ``pip install "apache-airflow-providers-pixi[cncf.kubernetes]"``.
    """
    from airflow.sdk.bases.decorator import task_decorator_factory

    try:
        operator_class = _decorated_operator_class()
    except ImportError as e:
        raise ImportError(
            "@task.pixi_kubernetes requires apache-airflow-providers-cncf-kubernetes. "
            'Install with: pip install "apache-airflow-providers-pixi[cncf.kubernetes]"'
        ) from e
    return task_decorator_factory(
        python_callable=python_callable,
        multiple_outputs=multiple_outputs,
        decorated_operator_class=operator_class,
        **kwargs,
    )

airflow.providers.pixi.decorators.sensor

@task.pixi_sensor: a sensor whose function runs inside a Pixi environment on each poke.

pixi_sensor_task

pixi_sensor_task(python_callable: Callable[..., Any] | None = None, **kwargs: Any)

@task.pixi_sensor: a sensor whose function runs inside a Pixi environment on each poke.

Like @task.sensor. The function returns a truthy value when the condition is met, or {"is_done": ..., "xcom_value": ...} to also push an XCom. Accepts every PixiSensor argument.

Source code in src/airflow/providers/pixi/decorators/sensor.py
def pixi_sensor_task(python_callable: Callable[..., Any] | None = None, **kwargs: Any):
    """``@task.pixi_sensor``: a sensor whose function runs inside a Pixi environment on each poke.

    Like ``@task.sensor``. The function returns a truthy value when the condition is met, or
    ``{"is_done": ..., "xcom_value": ...}`` to also push an XCom. Accepts every ``PixiSensor`` argument.
    """
    return task_decorator_factory(
        python_callable=python_callable,
        multiple_outputs=False,
        decorated_operator_class=PixiDecoratedSensorOperator,
        **kwargs,
    )

airflow.providers.pixi.decorators.branch

@task.pixi_branch: choose the tasks to follow with a function run inside a Pixi environment.

pixi_branch_task

pixi_branch_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
)

@task.pixi_branch: choose the tasks to follow with a function run inside a Pixi environment.

Like @task.branch_virtualenv. The function returns a task id or task group id directly downstream, a list of them, or None to skip them all. Accepts every PixiBranchOperator argument; as for @task.pixi, the function's source is shipped, so it must be self-contained.

Source code in src/airflow/providers/pixi/decorators/branch.py
def pixi_branch_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
):
    """``@task.pixi_branch``: choose the tasks to follow with a function run inside a Pixi environment.

    Like ``@task.branch_virtualenv``. The function returns a task id or task group id directly downstream, a
    list of them, or ``None`` to skip them all. Accepts every ``PixiBranchOperator`` argument; as for
    ``@task.pixi``, the function's source is shipped, so it must be self-contained.
    """
    return task_decorator_factory(
        python_callable=python_callable,
        multiple_outputs=multiple_outputs,
        decorated_operator_class=PixiBranchDecoratedOperator,
        **kwargs,
    )

airflow.providers.pixi.decorators.short_circuit

@task.pixi_short_circuit: continue only if a function run inside a Pixi environment returns a truthy value.

pixi_short_circuit_task

pixi_short_circuit_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
)

@task.pixi_short_circuit: continue only if a function run inside a Pixi environment returns a truthy value.

Like @task.short_circuit. A falsy return value skips the tasks downstream. Accepts every PixiShortCircuitOperator argument, such as ignore_downstream_trigger_rules; as for @task.pixi, the function's source is shipped, so it must be self-contained.

Source code in src/airflow/providers/pixi/decorators/short_circuit.py
def pixi_short_circuit_task(
    python_callable: Callable[..., Any] | None = None,
    multiple_outputs: bool | None = None,
    **kwargs: Any,
):
    """``@task.pixi_short_circuit``: continue only if a function run inside a Pixi environment returns a truthy value.

    Like ``@task.short_circuit``. A falsy return value skips the tasks downstream. Accepts every
    ``PixiShortCircuitOperator`` argument, such as ``ignore_downstream_trigger_rules``; as for ``@task.pixi``, the
    function's source is shipped, so it must be self-contained.
    """
    return task_decorator_factory(
        python_callable=python_callable,
        multiple_outputs=multiple_outputs,
        decorated_operator_class=PixiShortCircuitDecoratedOperator,
        **kwargs,
    )

airflow.providers.pixi.hooks.pixi

Credentials of private conda channels and PyPI indexes for pixi, from Airflow connections of type pixi.

Pixi reads conda channel credentials from the JSON file named by RATTLER_AUTH_FILE and PyPI index credentials from the netrc file named by NETRC (https://pixi.sh/latest/deployment/authentication/). For a run, pixi_auth_env writes both to temporary files and removes them afterwards.

AUTH_TYPES module-attribute

AUTH_TYPES = ('auto', 'bearer_token', 'conda_token', 'basic_http', 'netrc')

Values of the auth_type extra of a pixi connection.

PixiHook

PixiHook(pixi_conn_id: str = default_conn_name, **kwargs: Any)

Bases: BaseHook

Credentials of a private conda channel or PyPI index, from an Airflow connection of type pixi.

The connection's host is the host of the channel or index, such as repo.prefix.dev (a URL is reduced to its host name; *.prefix.dev matches every subdomain of a channel). password is the token or the password, and login the username for basic_http and netrc. auth_type in the extra chooses how pixi sends them:

  • "auto" (default): basic_http if the connection has a login, otherwise bearer_token.
  • "bearer_token": an Authorization: Bearer header, as prefix.dev uses.
  • "conda_token": a token in the channel URL, as anaconda.org and quetz use.
  • "basic_http": HTTP basic authentication, as Artifactory and Nexus use.
  • "netrc": a netrc entry, for a PyPI index named in [pypi-options] of a project's pixi.toml.

Parameters:

  • pixi_conn_id (str, default: default_conn_name ) –

    the connection id.

Source code in src/airflow/providers/pixi/hooks/pixi.py
def __init__(self, pixi_conn_id: str = default_conn_name, **kwargs: Any) -> None:
    super().__init__(**kwargs)
    self.pixi_conn_id = pixi_conn_id

get_ui_field_behaviour classmethod

get_ui_field_behaviour() -> dict[str, Any]

Return the connection form's fields, for Airflow 3.1.

Source code in src/airflow/providers/pixi/hooks/pixi.py
@classmethod
def get_ui_field_behaviour(cls) -> dict[str, Any]:
    """Return the connection form's fields, for Airflow 3.1."""
    return UI_FIELD_BEHAVIOUR

get_conn

get_conn() -> Any

Return the connection.

Source code in src/airflow/providers/pixi/hooks/pixi.py
def get_conn(self) -> Any:
    """Return the connection."""
    return self.get_connection(self.pixi_conn_id)

get_credentials

get_credentials() -> PixiCredentials

Return the connection's credentials; the token or password is masked in the task log.

Source code in src/airflow/providers/pixi/hooks/pixi.py
def get_credentials(self) -> PixiCredentials:
    """Return the connection's credentials; the token or password is masked in the task log."""
    conn = self.get_conn()
    extra = conn.extra_dejson
    auth_type = extra.get("auth_type") or extra.get("extra__pixi__auth_type") or "auto"
    where = f"Pixi connection {self.pixi_conn_id!r}"
    if auth_type not in AUTH_TYPES:
        raise AirflowException(f"{where}: auth_type must be one of {', '.join(AUTH_TYPES)}, not {auth_type!r}")
    if auth_type == "auto":
        auth_type = "basic_http" if conn.login else "bearer_token"
    host = _host_name(conn.host or "")
    if not host:
        raise AirflowException(f"{where} has no host; set it to the host of the channel or index")
    if not conn.password:
        raise AirflowException(f"{where} has no password; set it to the token or password")
    if auth_type in ("basic_http", "netrc") and not conn.login:
        raise AirflowException(f"{where}: {auth_type} needs a login")
    if auth_type == "netrc":
        if "*" in host:
            raise AirflowException(f"{where}: a netrc entry needs one host, not {host!r}")
        if any(not value.split() or len(value.split()) > 1 for value in (conn.login, conn.password)):
            raise AirflowException(f"{where}: netrc cannot hold a login or password with whitespace")
    mask_secret(conn.password)
    return PixiCredentials(
        conn_id=self.pixi_conn_id,
        auth_type=auth_type,
        host=host,
        login=conn.login or None,
        secret=conn.password,
    )

PixiCredentials dataclass

PixiCredentials(
    conn_id: str, auth_type: str, host: str, login: str | None, secret: str
)

Credentials for one host, from a pixi connection.

Parameters:

  • conn_id (str) –

    the connection they come from.

  • auth_type (str) –

    one of AUTH_TYPES except "auto".

  • host (str) –

    the host name pixi looks them up by, such as repo.prefix.dev or *.prefix.dev.

  • login (str | None) –

    the username of basic_http and netrc.

  • secret (str) –

    the token or password.

rattler_entry

rattler_entry() -> dict[str, Any]

Return the value for host in pixi's credentials file, for a conda channel.

Source code in src/airflow/providers/pixi/hooks/pixi.py
def rattler_entry(self) -> dict[str, Any]:
    """Return the value for ``host`` in pixi's credentials file, for a conda channel."""
    if self.auth_type == "bearer_token":
        return {"BearerToken": self.secret}
    if self.auth_type == "conda_token":
        return {"CondaToken": self.secret}
    if self.auth_type == "basic_http":
        return {"BasicHTTP": {"username": self.login, "password": self.secret}}
    raise ValueError(f"{self.auth_type} credentials of {self.conn_id!r} do not go into the credentials file")

netrc_entry

netrc_entry() -> str

Return the netrc lines for host, for a PyPI index.

Source code in src/airflow/providers/pixi/hooks/pixi.py
def netrc_entry(self) -> str:
    """Return the netrc lines for ``host``, for a PyPI index."""
    return f"machine {self.host}\nlogin {self.login}\npassword {self.secret}\n"

pixi_auth_env

pixi_auth_env(
    conn_ids: str | Sequence[str] | None, env: Mapping[str, str]
) -> Iterator[dict[str, str]]

Yield RATTLER_AUTH_FILE and NETRC for the credentials of the pixi connections conn_ids.

Each points at a temporary file, readable only by the worker's user and removed when the block exits; only the variables the connections need are set, and none without connections. While RATTLER_AUTH_FILE is set, pixi reads no other credentials, so the file also holds those of the file pixi would read otherwise: the one env names in RATTLER_AUTH_FILE, or ~/.rattler/credentials.json. Likewise the netrc file starts with the connections' entries, followed by the one env names in NETRC, or ~/.netrc. Credentials in the system keychain are not copied. In the credentials file, a connection's entry replaces one for the same host; netrc readers differ in which entry of a host they use, so keep a host in one place.

Parameters:

  • conn_ids (str | Sequence[str] | None) –

    a connection id, or several, each for another host.

  • env (Mapping[str, str]) –

    the environment pixi will run with, for HOME and the files it names.

Source code in src/airflow/providers/pixi/hooks/pixi.py
@contextlib.contextmanager
def pixi_auth_env(conn_ids: str | Sequence[str] | None, env: Mapping[str, str]) -> Iterator[dict[str, str]]:
    """Yield ``RATTLER_AUTH_FILE`` and ``NETRC`` for the credentials of the ``pixi`` connections ``conn_ids``.

    Each points at a temporary file, readable only by the worker's user and removed when the block exits; only
    the variables the connections need are set, and none without connections. While ``RATTLER_AUTH_FILE`` is
    set, pixi reads no other credentials, so the file also holds those of the file pixi would read otherwise:
    the one ``env`` names in ``RATTLER_AUTH_FILE``, or ``~/.rattler/credentials.json``. Likewise the netrc file
    starts with the connections' entries, followed by the one ``env`` names in ``NETRC``, or ``~/.netrc``.
    Credentials in the system keychain are not copied. In the credentials file, a connection's entry replaces
    one for the same host; netrc readers differ in which entry of a host they use, so keep a host in one place.

    :param conn_ids: a connection id, or several, each for another host.
    :param env: the environment pixi will run with, for ``HOME`` and the files it names.
    """
    ids = [conn_ids] if isinstance(conn_ids, str) else list(conn_ids or [])
    if not ids:
        yield {}
        return
    credentials = [PixiHook(conn_id).get_credentials() for conn_id in ids]
    seen: dict[tuple[bool, str], str] = {}
    for c in credentials:
        key = (c.auth_type == "netrc", c.host)
        if key in seen:
            raise AirflowException(
                f"Pixi connections {seen[key]!r} and {c.conn_id!r} both hold credentials for {c.host}"
            )
        seen[key] = c.conn_id
    home = env.get("HOME") or os.path.expanduser("~")
    rattler = [c for c in credentials if c.auth_type != "netrc"]
    netrc = [c for c in credentials if c.auth_type == "netrc"]
    directory = tempfile.mkdtemp(prefix="airflow_pixi_auth_")
    try:
        result: dict[str, str] = {}
        if rattler:
            existing = env.get("RATTLER_AUTH_FILE") or os.path.join(home, ".rattler", "credentials.json")
            entries = {**_existing_credentials(existing), **{c.host: c.rattler_entry() for c in rattler}}
            result["RATTLER_AUTH_FILE"] = os.path.join(directory, "credentials.json")
            _write_private(result["RATTLER_AUTH_FILE"], json.dumps(entries, indent=2))
        if netrc:
            existing = env.get("NETRC") or os.path.join(home, ".netrc")
            result["NETRC"] = os.path.join(directory, "netrc")
            _write_private(result["NETRC"], "".join(c.netrc_entry() for c in netrc) + _existing_netrc(existing))
        yield result
    finally:
        shutil.rmtree(directory, ignore_errors=True)

airflow.providers.pixi.exceptions

Exceptions raised by the Pixi operators.

PixiCallableError

PixiCallableError(
    callable_name: str, error_type: str, error_message: str, traceback: str
)

Bases: AirflowException

The Python callable raised an exception inside the Pixi environment.

The message names the callable and the exception, such as train raised ValueError: no rows; the traceback is in the task log, where the run printed it, and in traceback.

Parameters:

  • callable_name (str) –

    the function's name, or the "module.path:callable_name" string.

  • error_type (str) –

    the exception's class, with its module unless it is a built-in, such as ValueError or requests.exceptions.HTTPError.

  • error_message (str) –

    str() of the exception.

  • traceback (str) –

    the traceback, formatted inside the environment.

Source code in src/airflow/providers/pixi/exceptions.py
def __init__(self, callable_name: str, error_type: str, error_message: str, traceback: str) -> None:
    super().__init__(f"{callable_name} raised {error_type}: {error_message}")
    self.callable_name = callable_name
    self.error_type = error_type
    self.error_message = error_message
    self.traceback = traceback

airflow.providers.pixi.utils.pixi

The pixi binary on the worker: where it is, whether it is new enough, and the platform it installs for.

Pixi is never installed here.

MIN_PIXI_VERSION module-attribute

MIN_PIXI_VERSION = Version('0.81.0')

Oldest pixi this provider supports. The integration and system tests run against it.

local_platform

local_platform() -> str

Return the pixi platform of this machine, such as linux-64 or osx-arm64.

Source code in src/airflow/providers/pixi/utils/pixi.py
def local_platform() -> str:
    """Return the pixi platform of this machine, such as ``linux-64`` or ``osx-arm64``."""
    system = "linux" if sys.platform.startswith("linux") else sys.platform
    machine = platform.machine()
    try:
        return _PIXI_PLATFORMS[system, machine.lower()]
    except KeyError:
        raise AirflowException(
            f"No pixi platform is known for {sys.platform} on {machine or 'an unknown CPU'}; "
            "pass platforms explicitly, for example platforms=['linux-64']"
        ) from None

airflow.providers.pixi.utils.context

The part of the Airflow context a callable in a Pixi environment receives, as JSON values.

The environment may not have Airflow or pendulum, so the context travels as plain JSON: dates and datetimes as ISO 8601 strings, params and the DAG run's conf as dicts.

CONTEXT_KEYS module-attribute

CONTEXT_KEYS: tuple[str, ...] = (
    *dict.fromkeys((*_PLAIN_KEYS, *_TASK_INSTANCE_KEYS, "params", "conf")),
)

The keys a callable can receive, when the task's context has them.

serializable_context

serializable_context(context: Mapping[str, Any] | None) -> dict[str, Any]

Return the context values of CONTEXT_KEYS that context has, as JSON values.

Dates and datetimes become ISO 8601 strings, params a plain dict and the DAG run's conf conf. dag_id, task_id, map_index and try_number come from the task instance. A value JSON cannot hold is left out, as is a key the context does not have.

Source code in src/airflow/providers/pixi/utils/context.py
def serializable_context(context: Mapping[str, Any] | None) -> dict[str, Any]:
    """Return the context values of [`CONTEXT_KEYS`][CONTEXT_KEYS] that ``context`` has, as JSON values.

    Dates and datetimes become ISO 8601 strings, ``params`` a plain dict and the DAG run's ``conf`` ``conf``.
    ``dag_id``, ``task_id``, ``map_index`` and ``try_number`` come from the task instance. A value JSON cannot
    hold is left out, as is a key the context does not have.
    """
    if not context:
        return {}
    result: dict[str, Any] = {}

    def add(key: str, value: Any) -> None:
        try:
            result[key] = _json_value(value)
        except _NotJson:
            pass

    for key in _PLAIN_KEYS:
        if key in context:
            add(key, context[key])
    ti = context.get("ti") or context.get("task_instance")
    for key in _TASK_INSTANCE_KEYS:
        if key not in result and ti is not None and hasattr(ti, key):
            add(key, getattr(ti, key))
    if "dag_id" not in result and hasattr(context.get("dag"), "dag_id"):
        add("dag_id", context["dag"].dag_id)
    if "task_id" not in result and hasattr(context.get("task"), "task_id"):
        add("task_id", context["task"].task_id)
    params = context.get("params")
    if isinstance(params, Mapping):
        result["params"] = _json_items(params.dump() if callable(getattr(type(params), "dump", None)) else params)
    conf = getattr(context.get("dag_run"), "conf", None)
    if isinstance(conf, Mapping):
        result["conf"] = _json_items(conf)
    return result

airflow.providers.pixi.utils.env

Environment variables of a pixi run from Airflow Variables and Connections, read on the worker when it runs.

Values never pass through templates. Passwords, connection URIs and extras are masked in the task log.

CONNECTION_FIELDS module-attribute

CONNECTION_FIELDS = ('host', 'login', 'password', 'schema', 'port', 'extra')

The fields env_from_connections can name after the connection id, as "<conn_id>.<field>".

resolve_env

resolve_env(
    *,
    env_from_variables: Mapping[str, str] | None = None,
    env_from_connections: Mapping[str, str] | None = None,
    optional_variables: Mapping[str, str | None] | None = None,
) -> dict[str, str]

Return the environment variables read from Airflow Variables and Connections, for the worker's run.

Parameters:

  • env_from_variables (Mapping[str, str] | None, default: None ) –

    environment variable name to the key of a Variable, which must exist.

  • env_from_connections (Mapping[str, str] | None, default: None ) –

    environment variable name to "conn_id" (the connection's URI), "conn_id.<field>" (host, login, password, schema, port, or extra as a JSON string) or "conn_id.extra.<key>" (one key of the extra; a value that is not a string as JSON). The connection and the field must exist.

  • optional_variables (Mapping[str, str | None] | None, default: None ) –

    environment variable name to the key of a Variable whose value, stripped of surrounding whitespace, is used if the Variable exists; None or a missing Variable leaves it out. For the cache directory Variables.

    env_from_variables and env_from_connections win over optional_variables. Passwords, URIs and extras of connections are masked in the task log; Variables are masked as Airflow masks them, by their key.

Source code in src/airflow/providers/pixi/utils/env.py
def resolve_env(
    *,
    env_from_variables: Mapping[str, str] | None = None,
    env_from_connections: Mapping[str, str] | None = None,
    optional_variables: Mapping[str, str | None] | None = None,
) -> dict[str, str]:
    """Return the environment variables read from Airflow Variables and Connections, for the worker's run.

    :param env_from_variables: environment variable name to the key of a Variable, which must exist.
    :param env_from_connections: environment variable name to ``"conn_id"`` (the connection's URI),
        ``"conn_id.<field>"`` (``host``, ``login``, ``password``, ``schema``, ``port``, or ``extra`` as a JSON
        string) or ``"conn_id.extra.<key>"`` (one key of the extra; a value that is not a string as JSON). The
        connection and the field must exist.
    :param optional_variables: environment variable name to the key of a Variable whose value, stripped of
        surrounding whitespace, is used if the Variable exists; ``None`` or a missing Variable leaves it out. For
        the cache directory Variables.

    ``env_from_variables`` and ``env_from_connections`` win over ``optional_variables``. Passwords, URIs and
    extras of connections are masked in the task log; Variables are masked as Airflow masks them, by their key.
    """
    check_env_sources(env_from_variables, env_from_connections)
    env: dict[str, str] = {}
    for name, key in (optional_variables or {}).items():
        if key:
            value = Variable.get(key, default=None)
            if value is not None:
                env[name] = str(value).strip()
    for name, key in (env_from_variables or {}).items():
        value = Variable.get(key, default=None)
        if value is None:
            raise AirflowException(f"Variable {key!r} for {name} in env_from_variables does not exist")
        env[name] = str(value)
    connections: dict[str, Any] = {}
    for name, reference in (env_from_connections or {}).items():
        conn_id, field, key = parse_connection_reference(reference)
        if conn_id not in connections:
            try:
                connections[conn_id] = Connection.get(conn_id)
            except AirflowNotFoundException:
                raise AirflowException(
                    f"Connection {conn_id!r} for {name} in env_from_connections does not exist"
                ) from None
        value, secret = _connection_value(connections[conn_id], field, key)
        if value is None:
            what = f"extra key {key!r}" if key is not None else field
            raise AirflowException(f"Connection {conn_id!r} has no {what}, which env_from_connections sets as {name}")
        if secret:
            mask_secret(value)
        env[name] = value
    return env

parse_connection_reference

parse_connection_reference(reference: str) -> tuple[str, str | None, str | None]

Split an env_from_connections value into the connection id, the field and the key of extra.

"conn_id" is the connection's URI (field None), "conn_id.password" one field, and "conn_id.extra.key" one key of the extra. The first .<field> names the field, so a connection id cannot itself contain .host, .login, .password, .schema, .port or .extra. Only extra takes a key: "conn_id.port.internal" is rejected rather than read as the whole port.

Source code in src/airflow/providers/pixi/utils/env.py
def parse_connection_reference(reference: str) -> tuple[str, str | None, str | None]:
    """Split an ``env_from_connections`` value into the connection id, the field and the key of ``extra``.

    ``"conn_id"`` is the connection's URI (field ``None``), ``"conn_id.password"`` one field, and
    ``"conn_id.extra.key"`` one key of the extra. The first ``.<field>`` names the field, so a connection id
    cannot itself contain ``.host``, ``.login``, ``.password``, ``.schema``, ``.port`` or ``.extra``. Only
    ``extra`` takes a key: ``"conn_id.port.internal"`` is rejected rather than read as the whole port.
    """
    match = _REFERENCE.match(reference) if isinstance(reference, str) else None
    if match is None:
        raise ValueError(f"{reference!r} is not a connection id, '<conn_id>.<field>' or '<conn_id>.extra.<key>'")
    if match["key"] is not None and match["field"] != "extra":
        raise ValueError(
            f"{reference!r}: only extra has keys, as in '<conn_id>.extra.<key>'; {match['field']} is a single value, "
            f"'{match['conn_id']}.{match['field']}'"
        )
    return match["conn_id"], match["field"], match["key"]

airflow.providers.pixi.utils.manifest

Inline Pixi manifests: pixi.toml written from operator arguments.

pypi_dependencies_table

pypi_dependencies_table(
    pypi_dependencies: dict[str, Any] | Sequence[str] | str | None,
) -> dict[str, Any]

[pypi-dependencies] from a dict as in pixi.toml, or from pip requirement strings.

The strings are a list, or one string, as @task.virtualenv takes them. A string may hold several lines, such as a rendered requirements file; blank lines and comments are skipped. Versions, extras, git and URL requirements are supported.

Raises:

  • ValueError –

    for a pip option, such as --index-url, or an environment marker, which have no equivalent in an inline manifest and belong in a pixi.toml; for an invalid requirement; and for a package listed twice.

Source code in src/airflow/providers/pixi/utils/manifest.py
def pypi_dependencies_table(pypi_dependencies: dict[str, Any] | Sequence[str] | str | None) -> dict[str, Any]:
    """``[pypi-dependencies]`` from a dict as in ``pixi.toml``, or from pip requirement strings.

    The strings are a list, or one string, as ``@task.virtualenv`` takes them. A string may hold several lines,
    such as a rendered requirements file; blank lines and comments are skipped. Versions, extras, git and URL
    requirements are supported.

    :raises ValueError: for a pip option, such as ``--index-url``, or an environment marker, which have no
        equivalent in an inline manifest and belong in a ``pixi.toml``; for an invalid requirement; and for a
        package listed twice.
    """
    if pypi_dependencies is None or isinstance(pypi_dependencies, dict):
        return dict(pypi_dependencies or {})
    result: dict[str, Any] = {}
    for item in [pypi_dependencies] if isinstance(pypi_dependencies, str) else pypi_dependencies:
        for raw in str(item).splitlines():
            line = _REQUIREMENT_COMMENT.sub("", raw).strip()
            if not line:
                continue
            if line.startswith("-"):
                raise ValueError(
                    f"pypi_dependencies cannot hold pip options such as {line!r}; set indexes and other PyPI "
                    "options under [pypi-options] in a pixi.toml and use pixi_project_path or pixi_toml_path"
                )
            try:
                requirement = Requirement(line)
            except InvalidRequirement as e:
                raise ValueError(f"invalid requirement {line!r}: {e}") from None
            if requirement.marker is not None:
                raise ValueError(f"requirement {line!r}: environment markers are not supported")
            if any(canonicalize_name(name) == canonicalize_name(requirement.name) for name in result):
                raise ValueError(f"{requirement.name} is listed twice in pypi_dependencies")
            spec: Any = str(requirement.specifier) or "*"
            if requirement.url:
                spec = _pypi_spec_from_url(requirement.url)
            if requirement.extras:
                spec = {**(spec if isinstance(spec, dict) else {"version": spec}), "extras": sorted(requirement.extras)}
            result[requirement.name] = spec
    return result

conda_dependencies

conda_dependencies(
    dependencies: dict[str, Any] | Sequence[str] | None,
) -> dict[str, Any]

[dependencies] from a dict, or from a list of MatchSpecs such as "numpy>=2".

A MatchSpec is [channel::]name [version [build]], such as "conda-forge::pytorch 2.* cuda*".

Raises:

  • TypeError –

    for a string, which would otherwise be read one character at a time.

  • ValueError –

    for a MatchSpec of another form, or a package listed twice.

Source code in src/airflow/providers/pixi/utils/manifest.py
def conda_dependencies(dependencies: dict[str, Any] | Sequence[str] | None) -> dict[str, Any]:
    """``[dependencies]`` from a dict, or from a list of MatchSpecs such as ``"numpy>=2"``.

    A MatchSpec is ``[channel::]name [version [build]]``, such as ``"conda-forge::pytorch 2.* cuda*"``.

    :raises TypeError: for a string, which would otherwise be read one character at a time.
    :raises ValueError: for a MatchSpec of another form, or a package listed twice.
    """
    if isinstance(dependencies, str):
        raise TypeError(
            f"dependencies must be a dict or a list of MatchSpecs, such as [{dependencies!r}], not a string"
        )
    if dependencies is None or isinstance(dependencies, dict):
        return dict(dependencies or {})
    result: dict[str, Any] = {}
    for spec in dependencies:
        name, value = _conda_spec(spec)
        if any(listed.lower() == name.lower() for listed in result):
            raise ValueError(f"{name} is listed twice in dependencies")
        result[name] = value
    return result

airflow.providers.pixi.runtime.runner

Call a function inside the Pixi environment and hand its result back to the worker.

The operators run this module's source as python -c <source> <serializer> <input> <output> [json].

The input file holds the callable, either "module" and "name" or a function's "source", "filename" and "name", with its "args", "kwargs" and an optional JSON-safe "context", in the serializer's format. The return value goes to the output file, which keeps stdout and stderr free for the task log. With json as the fourth argument, a pickled result is written as a base64 JSON string, for a pod's XCom file.

An exception from the callable, or an environment Python older than MIN_PYTHON, is described in <output> + ERROR_SUFFIX and, when the environment variable TERMINATION_LOG_ENV names a file, there too, shortened to fit a Kubernetes termination message. Either exits with 1.

ERROR_SUFFIX module-attribute

ERROR_SUFFIX = '.error'

Appended to the output path for the file describing an exception from the callable.

TERMINATION_LOG_ENV module-attribute

TERMINATION_LOG_ENV = 'PIXI_AIRFLOW_TERMINATION_LOG'

Environment variable naming the file Kubernetes reads as the container's termination message.

TERMINATION_KEY module-attribute

TERMINATION_KEY = 'pixi_callable_error'

Key of the exception's description in the termination message.

TERMINATION_MESSAGE_BYTES module-attribute

TERMINATION_MESSAGE_BYTES = 4000

The longest termination message written; Kubernetes keeps up to 4 KiB of it.

PICKLE_PROTOCOL module-attribute

PICKLE_PROTOCOL = 4

Pickle protocol of the input and the result, readable by every Python the runner supports.

MIN_PYTHON module-attribute

MIN_PYTHON = (3, 10)

The oldest Python the runner supports in the Pixi environment.

SHIPPED_MODULE module-attribute

SHIPPED_MODULE = '__pixi_airflow_task__'

__name__ of the namespace a shipped function's source runs in.

unsupported_python

unsupported_python(version_info: tuple[int, ...]) -> str | None

Return why the environment's Python cannot run the callable, or None if it is new enough.

Source code in src/airflow/providers/pixi/runtime/runner.py
def unsupported_python(version_info: tuple[int, ...]) -> str | None:
    """Return why the environment's Python cannot run the callable, or ``None`` if it is new enough."""
    if version_info[:2] >= MIN_PYTHON:
        return None
    found = ".".join(str(part) for part in version_info[:3])
    needed = ".".join(str(part) for part in MIN_PYTHON)
    return f"The Pixi environment has Python {found}, but apache-airflow-providers-pixi needs Python {needed} or newer."

load_spec

load_spec(serializer: str, path: str) -> dict[str, Any]

Return the callable's description and arguments from the input file.

Source code in src/airflow/providers/pixi/runtime/runner.py
def load_spec(serializer: str, path: str) -> dict[str, Any]:
    """Return the callable's description and arguments from the input file."""
    spec: dict[str, Any]
    if serializer == "pickle":
        with open(path, "rb") as f:
            spec = pickle.load(f)
    else:
        with open(path) as f:
            spec = json.load(f)
    return spec

load_callable

load_callable(spec: Mapping[str, Any]) -> Callable[..., Any]

Return the callable: the shipped function, compiled under the DAG file's name, or module:name.

Source code in src/airflow/providers/pixi/runtime/runner.py
def load_callable(spec: Mapping[str, Any]) -> Callable[..., Any]:
    """Return the callable: the shipped function, compiled under the DAG file's name, or ``module:name``."""
    if "source" in spec:
        namespace: dict[str, Any] = {"__name__": SHIPPED_MODULE}
        exec(compile(spec["source"], spec["filename"], "exec"), namespace)
        function: Callable[..., Any] = namespace[spec["name"]]
        return function
    attribute: Callable[..., Any] = getattr(importlib.import_module(spec["module"]), spec["name"])
    return attribute

with_context

with_context(
    fn: Callable[..., Any],
    args: Sequence[Any],
    kwargs: dict[str, Any],
    context: Mapping[str, Any],
) -> dict[str, Any]

Return kwargs with the context values for the parameters args and kwargs leave open.

A function with **kwargs gets every context value, as Airflow's determine_kwargs passes them.

Source code in src/airflow/providers/pixi/runtime/runner.py
def with_context(
    fn: Callable[..., Any], args: Sequence[Any], kwargs: dict[str, Any], context: Mapping[str, Any]
) -> dict[str, Any]:
    """Return ``kwargs`` with the context values for the parameters ``args`` and ``kwargs`` leave open.

    A function with ``**kwargs`` gets every context value, as Airflow's ``determine_kwargs`` passes them.
    """
    try:
        signature = inspect.signature(fn)
        bound = signature.bind_partial(*args, **kwargs).arguments
    except (TypeError, ValueError):
        return kwargs
    taken = set(bound)
    names: list[str] = []
    for name, parameter in signature.parameters.items():
        if parameter.kind == parameter.VAR_KEYWORD:
            taken.discard(name)
            taken.update(bound.get(name, ()))
            names = list(context)
            break
        if parameter.kind in (parameter.POSITIONAL_OR_KEYWORD, parameter.KEYWORD_ONLY):
            names.append(name)
    extra = {name: context[name] for name in names if name in context and name not in taken}
    if not extra:
        return kwargs
    extra.update(kwargs)
    return extra

await_result

await_result(result: Any) -> Any

Return result, awaited first if it is awaitable, as the result of a coroutine function.

Source code in src/airflow/providers/pixi/runtime/runner.py
def await_result(result: Any) -> Any:
    """Return ``result``, awaited first if it is awaitable, as the result of a coroutine function."""
    if not inspect.isawaitable(result):
        return result
    if asyncio.iscoroutine(result):
        return asyncio.run(result)
    loop = asyncio.new_event_loop()
    try:
        return loop.run_until_complete(result)
    finally:
        loop.close()

encode_result

encode_result(result: Any, serializer: str, as_json: bool) -> str | bytes

Return the content of the output file for result.

Source code in src/airflow/providers/pixi/runtime/runner.py
def encode_result(result: Any, serializer: str, as_json: bool) -> str | bytes:
    """Return the content of the output file for ``result``."""
    if serializer == "pickle":
        data = pickle.dumps(result, protocol=PICKLE_PROTOCOL)
        return json.dumps(base64.b64encode(data).decode()) if as_json else data
    try:
        return json.dumps(result)
    except TypeError as e:
        raise TypeError(f"{e}; return a JSON-serializable value or pass serializer='pickle'") from None

describe_error

describe_error(error: BaseException) -> dict[str, str]

Return the exception's type (with its module unless built in), message and traceback.

Source code in src/airflow/providers/pixi/runtime/runner.py
def describe_error(error: BaseException) -> dict[str, str]:
    """Return the exception's ``type`` (with its module unless built in), ``message`` and ``traceback``."""
    cls = type(error)
    name = cls.__name__ if "<locals>" in cls.__qualname__ else cls.__qualname__
    if cls.__module__ not in ("builtins", "__main__", SHIPPED_MODULE):
        name = f"{cls.__module__}.{name}"
    try:
        message = str(error)
    except Exception:
        message = "<str() of the exception failed>"
    text = "".join(traceback.format_exception(cls, error, error.__traceback__))
    return {"type": name, "message": message, "traceback": text}

termination_message

termination_message(
    error: Mapping[str, str], limit: int = TERMINATION_MESSAGE_BYTES
) -> bytes

Return error as a termination message of at most limit bytes, keeping the end of the traceback.

Source code in src/airflow/providers/pixi/runtime/runner.py
def termination_message(error: Mapping[str, str], limit: int = TERMINATION_MESSAGE_BYTES) -> bytes:
    """Return ``error`` as a termination message of at most ``limit`` bytes, keeping the end of the traceback."""
    shortened = {**error, "message": error["message"][:500]}
    trace = error["traceback"]
    while True:
        data = json.dumps({TERMINATION_KEY: {**shortened, "traceback": trace}}, ensure_ascii=False).encode()
        if len(data) <= limit or not trace:
            return data
        trace = trace[len(trace) // 4 + 1 :]

report_error

report_error(error: Mapping[str, str], output_path: str) -> None

Print the traceback and write the error file, and the termination message when one is asked for.

Source code in src/airflow/providers/pixi/runtime/runner.py
def report_error(error: Mapping[str, str], output_path: str) -> None:
    """Print the traceback and write the error file, and the termination message when one is asked for."""
    sys.stderr.write(error["traceback"])
    with open(output_path + ERROR_SUFFIX, "w") as f:
        json.dump(error, f)
    termination_log = os.environ.get(TERMINATION_LOG_ENV)
    if termination_log:
        try:
            with open(termination_log, "wb") as f:
                f.write(termination_message(error))
        except OSError:
            pass

main

main(argv: Sequence[str]) -> int

Run the callable described by argv (-c, serializer, input, output, optionally json).

Source code in src/airflow/providers/pixi/runtime/runner.py
def main(argv: Sequence[str]) -> int:
    """Run the callable described by ``argv`` (``-c``, serializer, input, output, optionally ``json``)."""
    serializer, input_path, output_path = argv[1:4]
    problem = unsupported_python(tuple(sys.version_info[:3]))
    if problem:
        report_error(describe_error(RuntimeError(problem)), output_path)
        return 1
    spec = load_spec(serializer, input_path)
    try:
        fn = load_callable(spec)
        kwargs = spec["kwargs"]
        if spec.get("context"):
            kwargs = with_context(fn, spec["args"], kwargs, spec["context"])
        data = encode_result(await_result(fn(*spec["args"], **kwargs)), serializer, list(argv[4:]) == ["json"])
    except Exception as e:
        report_error(describe_error(e), output_path)
        return 1
    with open(output_path, "wb" if isinstance(data, bytes) else "w") as f:
        f.write(data)
    return 0