Source code for airflow_pydantic.extras.balancer.host
from collections.abc import Mapping, Sequence
from logging import getLogger
from typing import TYPE_CHECKING, Any
from pydantic import Field, JsonValue, field_validator
from ...core import BaseModel
from ...utils import Pool, Variable
if TYPE_CHECKING:
from airflow_pydantic.airflow import SSHHook
__all__ = ("Host",)
_log = getLogger(__name__)
# Stand-in for a Variable password that cannot be resolved offline. Rendering always
# replaces it with a Variable.get call, so it must never reach a generated DAG.
UNRESOLVED_PASSWORD = "<unresolved airflow-pydantic variable>"
[docs]
class Host(BaseModel):
name: str
username: str | None = None
# Password
password: str | Variable | None = None
# Or get key file
key_file: str | None = None
ssh_hook_args: dict[str, JsonValue] = Field(default_factory=dict, description="Default SSHHook arguments; call-time arguments take precedence")
os: str | None = None
# Airflow / balance
pool: Pool | None = None
size: int | None = None
queues: list[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
@field_validator("ssh_hook_args", mode="before")
@classmethod
def _normalize_hook_args(cls, value):
def plain(item):
if isinstance(item, Mapping):
return {key: plain(value) for key, value in item.items()}
if isinstance(item, Sequence) and not isinstance(item, (str, bytes, bytearray)):
return [plain(value) for value in item]
return item
return plain(value)
[docs]
def override(self, **kwargs) -> "Host":
return Host(**{**self.model_dump(), **kwargs})
[docs]
def hook(self, username: str | None = None, use_local: bool = True, **hook_kwargs) -> "SSHHook":
from airflow_pydantic.airflow import SSHHook
def _hook(**kwargs) -> "SSHHook":
options: dict[str, Any] = {**self.ssh_hook_args, **kwargs, **hook_kwargs}
hook = SSHHook(**options)
if self.key_file is None and "key_file" not in self.ssh_hook_args and "key_file" not in hook_kwargs:
hook.key_file = None
return hook
if use_local and not self.name.count(".") > 0:
name = f"{self.name}.local"
else:
name = self.name
username = username or self.username
if username and self.password:
if isinstance(self.password, str):
return _hook(remote_host=name, username=username, password=self.password)
try:
password = self.password.get()
except Exception: # noqa: BLE001
# No Airflow metadata database available, e.g. when generating DAG files offline.
# Rendering rewrites this into a Variable.get call, so the value itself is unused.
_log.info("Could not resolve variable %s, falling back to an unresolved password", self.password.key)
password = UNRESOLVED_PASSWORD
if isinstance(password, dict):
# TODO
# Assume "password"
password = password["password"]
return _hook(remote_host=name, username=username, password=password)
elif username and self.key_file:
return _hook(remote_host=name, username=username, key_file=self.key_file)
elif username:
return _hook(remote_host=name, username=username)
else:
return _hook(remote_host=name)
def __lt__(self, other):
return self.name < other.name
def __le__(self, other):
return self.name <= other.name
def __hash__(self):
return hash(self.name)