|
17 | 17 | # under the License.
|
18 | 18 | from __future__ import annotations
|
19 | 19 |
|
20 |
| -from typing import TYPE_CHECKING |
| 20 | +from functools import cached_property |
| 21 | +from typing import TYPE_CHECKING, Any |
21 | 22 |
|
22 | 23 | import attrs
|
23 | 24 | import structlog
|
24 | 25 |
|
| 26 | +from airflow.exceptions import AirflowException |
25 | 27 | from airflow.sdk.definitions.mappedoperator import MappedOperator as TaskSDKMappedOperator
|
26 | 28 | from airflow.triggers.base import StartTriggerArgs
|
27 | 29 | from airflow.utils.helpers import prevent_duplicates
|
28 | 30 |
|
29 | 31 | if TYPE_CHECKING:
|
30 | 32 | from sqlalchemy.orm.session import Session
|
31 | 33 |
|
| 34 | + from airflow.models import TaskInstance |
| 35 | + from airflow.sdk import BaseOperatorLink |
32 | 36 | from airflow.sdk.definitions.context import Context
|
33 | 37 |
|
34 | 38 | log = structlog.get_logger(__name__)
|
@@ -118,3 +122,54 @@ def expand_start_trigger_args(self, *, context: Context, session: Session) -> St
|
118 | 122 | next_kwargs=next_kwargs,
|
119 | 123 | timeout=timeout,
|
120 | 124 | )
|
| 125 | + |
| 126 | + @cached_property |
| 127 | + def operator_extra_link_dict(self) -> dict[str, BaseOperatorLink]: |
| 128 | + """Returns dictionary of all extra links for the operator.""" |
| 129 | + op_extra_links_from_plugin: dict[str, Any] = {} |
| 130 | + from airflow import plugins_manager |
| 131 | + |
| 132 | + plugins_manager.initialize_extra_operators_links_plugins() |
| 133 | + if plugins_manager.operator_extra_links is None: |
| 134 | + raise AirflowException("Can't load operators") |
| 135 | + operator_class_type = self.operator_class["task_type"] # type: ignore |
| 136 | + for ope in plugins_manager.operator_extra_links: |
| 137 | + if ope.operators and any(operator_class_type in cls.__name__ for cls in ope.operators): |
| 138 | + op_extra_links_from_plugin.update({ope.name: ope}) |
| 139 | + |
| 140 | + operator_extra_links_all = {link.name: link for link in self.operator_extra_links} |
| 141 | + # Extra links defined in Plugins overrides operator links defined in operator |
| 142 | + operator_extra_links_all.update(op_extra_links_from_plugin) |
| 143 | + |
| 144 | + return operator_extra_links_all |
| 145 | + |
| 146 | + @cached_property |
| 147 | + def global_operator_extra_link_dict(self) -> dict[str, Any]: |
| 148 | + """Returns dictionary of all global extra links.""" |
| 149 | + from airflow import plugins_manager |
| 150 | + |
| 151 | + plugins_manager.initialize_extra_operators_links_plugins() |
| 152 | + if plugins_manager.global_operator_extra_links is None: |
| 153 | + raise AirflowException("Can't load operators") |
| 154 | + return {link.name: link for link in plugins_manager.global_operator_extra_links} |
| 155 | + |
| 156 | + @cached_property |
| 157 | + def extra_links(self) -> list[str]: |
| 158 | + return sorted(set(self.operator_extra_link_dict).union(self.global_operator_extra_link_dict)) |
| 159 | + |
| 160 | + def get_extra_links(self, ti: TaskInstance, name: str) -> str | None: |
| 161 | + """ |
| 162 | + For an operator, gets the URLs that the ``extra_links`` entry points to. |
| 163 | +
|
| 164 | + :meta private: |
| 165 | +
|
| 166 | + :raise ValueError: The error message of a ValueError will be passed on through to |
| 167 | + the fronted to show up as a tooltip on the disabled link. |
| 168 | + :param ti: The TaskInstance for the URL being searched for. |
| 169 | + :param name: The name of the link we're looking for the URL for. Should be |
| 170 | + one of the options specified in ``extra_links``. |
| 171 | + """ |
| 172 | + link = self.operator_extra_link_dict.get(name) or self.global_operator_extra_link_dict.get(name) |
| 173 | + if not link: |
| 174 | + return None |
| 175 | + return link.get_link(self, ti_key=ti.key) # type: ignore[arg-type] |
0 commit comments