4040from airflow .callbacks .callback_requests import DagCallbackRequest , TaskCallbackRequest
4141from airflow .exceptions import AirflowException , SerializationError , TaskDeferred
4242from airflow .models .baseoperator import BaseOperator
43+ from airflow .models .baseoperatorlink import BaseOperatorLink , XComOperatorLink
4344from airflow .models .connection import Connection
4445from airflow .models .dag import DAG , _get_model_data_interval
4546from airflow .models .expandinput import (
4647 EXPAND_INPUT_EMPTY ,
4748 create_expand_input ,
4849)
49- from airflow .models .taskinstance import SimpleTaskInstance
50+ from airflow .models .taskinstance import SimpleTaskInstance , TaskInstance
5051from airflow .models .taskinstancekey import TaskInstanceKey
5152from airflow .models .xcom_arg import SchedulerXComArg , deserialize_xcom_arg
5253from airflow .providers_manager import ProvidersManager
9697 from inspect import Parameter
9798
9899 from airflow .models import DagRun
99- from airflow .models .baseoperatorlink import BaseOperatorLink
100100 from airflow .models .expandinput import ExpandInput
101101 from airflow .sdk .definitions ._internal .node import DAGNode
102102 from airflow .sdk .types import Operator
@@ -1167,6 +1167,58 @@ def __init__(self, *args, **kwargs):
11671167 self .template_fields = BaseOperator .template_fields
11681168 self .operator_extra_links = BaseOperator .operator_extra_links
11691169
1170+ @cached_property
1171+ def operator_extra_link_dict (self ) -> dict [str , BaseOperatorLink ]:
1172+ """Returns dictionary of all extra links for the operator."""
1173+ op_extra_links_from_plugin : dict [str , Any ] = {}
1174+ from airflow import plugins_manager
1175+
1176+ plugins_manager .initialize_extra_operators_links_plugins ()
1177+ if plugins_manager .operator_extra_links is None :
1178+ raise AirflowException ("Can't load operators" )
1179+ for ope in plugins_manager .operator_extra_links :
1180+ if ope .operators and self .operator_class in ope .operators :
1181+ op_extra_links_from_plugin .update ({ope .name : ope })
1182+
1183+ operator_extra_links_all = {link .name : link for link in self .operator_extra_links }
1184+ # Extra links defined in Plugins overrides operator links defined in operator
1185+ operator_extra_links_all .update (op_extra_links_from_plugin )
1186+
1187+ return operator_extra_links_all
1188+
1189+ @cached_property
1190+ def global_operator_extra_link_dict (self ) -> dict [str , Any ]:
1191+ """Returns dictionary of all global extra links."""
1192+ from airflow import plugins_manager
1193+
1194+ plugins_manager .initialize_extra_operators_links_plugins ()
1195+ if plugins_manager .global_operator_extra_links is None :
1196+ raise AirflowException ("Can't load operators" )
1197+ return {link .name : link for link in plugins_manager .global_operator_extra_links }
1198+
1199+ @cached_property
1200+ def extra_links (self ) -> list [str ]:
1201+ return sorted (set (self .operator_extra_link_dict ).union (self .global_operator_extra_link_dict ))
1202+
1203+ def get_extra_links (self , ti : TaskInstance , link_name : str ) -> str | None :
1204+ """
1205+ For an operator, gets the URLs that the ``extra_links`` entry points to.
1206+
1207+ :meta private:
1208+
1209+ :raise ValueError: The error message of a ValueError will be passed on through to
1210+ the fronted to show up as a tooltip on the disabled link.
1211+ :param ti: The TaskInstance for the URL being searched for.
1212+ :param link_name: The name of the link we're looking for the URL for. Should be
1213+ one of the options specified in ``extra_links``.
1214+ """
1215+ link = self .operator_extra_link_dict .get (link_name )
1216+ if not link :
1217+ link = self .global_operator_extra_link_dict .get (link_name )
1218+ if not link :
1219+ return None
1220+ return link .get_link (self .unmap (None ), ti_key = ti .key )
1221+
11701222 @property
11711223 def task_type (self ) -> str :
11721224 # Overwrites task_type of BaseOperator to use _task_type instead of
@@ -1504,7 +1556,9 @@ def _is_excluded(cls, var: Any, attrname: str, op: DAGNode):
15041556 return super ()._is_excluded (var , attrname , op )
15051557
15061558 @classmethod
1507- def _deserialize_operator_extra_links (cls , encoded_op_links : list ) -> dict [str , BaseOperatorLink ]:
1559+ def _deserialize_operator_extra_links (
1560+ cls , encoded_op_links : dict [str , str ]
1561+ ) -> dict [str , XComOperatorLink ]:
15081562 """
15091563 Deserialize Operator Links if the Classes are registered in Airflow Plugins.
15101564
@@ -1521,77 +1575,40 @@ def _deserialize_operator_extra_links(cls, encoded_op_links: list) -> dict[str,
15211575 raise AirflowException ("Can't load plugins" )
15221576 op_predefined_extra_links = {}
15231577
1524- for _operator_links_source in encoded_op_links :
1525- # Get the key, value pair as Tuple where key is OperatorLink ClassName
1526- # and value is the dictionary containing the arguments passed to the OperatorLink
1527- #
1528- # Example of a single iteration:
1529- #
1530- # _operator_links_source =
1531- # {
1532- # 'airflow.providers.google.cloud.operators.bigquery.BigQueryConsoleIndexableLink': {
1533- # 'index': 0
1534- # }
1535- # },
1536- #
1537- # list(_operator_links_source.items()) =
1538- # [
1539- # (
1540- # 'airflow.providers.google.cloud.operators.bigquery.BigQueryConsoleIndexableLink',
1541- # {'index': 0}
1542- # )
1543- # ]
1578+ for name , xcom_key in encoded_op_links .items ():
1579+ # Get the name and xcom_key of the encoded operator and use it to create a XComOperatorLink object
1580+ # during deserialization.
15441581 #
1545- # list(_operator_links_source.items())[0] =
1546- # (
1547- # 'airflow.providers.google.cloud.operators.bigquery.BigQueryConsoleIndexableLink',
1548- # {
1549- # 'index': 0
1550- # }
1551- # )
1552-
1553- _operator_link_class_path , data = next (iter (_operator_links_source .items ()))
1554- if _operator_link_class_path in get_operator_extra_links ():
1555- single_op_link_class = import_string (_operator_link_class_path )
1556- elif _operator_link_class_path in plugins_manager .registered_operator_link_classes :
1557- single_op_link_class = plugins_manager .registered_operator_link_classes [
1558- _operator_link_class_path
1559- ]
1560- else :
1561- log .error ("Operator Link class %r not registered" , _operator_link_class_path )
1562- return {}
1563-
1564- op_link_parameters = {param : cls .deserialize (value ) for param , value in data .items ()}
1565- op_predefined_extra_link : BaseOperatorLink = single_op_link_class (** op_link_parameters )
1566-
1582+ # Example:
1583+ # enc_operator['_operator_extra_links'] =
1584+ # {
1585+ # 'airflow': 'airflow_link_key',
1586+ # 'foo-bar': 'link-key',
1587+ # 'no_response': 'key',
1588+ # 'raise_error': 'key'
1589+ # }
1590+
1591+ op_predefined_extra_link = XComOperatorLink (name = name , xcom_key = xcom_key )
15671592 op_predefined_extra_links .update ({op_predefined_extra_link .name : op_predefined_extra_link })
15681593
15691594 return op_predefined_extra_links
15701595
15711596 @classmethod
1572- def _serialize_operator_extra_links (cls , operator_extra_links : Iterable [BaseOperatorLink ]):
1597+ def _serialize_operator_extra_links (
1598+ cls , operator_extra_links : Iterable [BaseOperatorLink ]
1599+ ) -> dict [str , str ]:
15731600 """
15741601 Serialize Operator Links.
15751602
1576- Store the import path of the OperatorLink and the arguments passed to it.
1603+ Store the "name" of the link mapped with the xcom_key which can be later used to retrieve this
1604+ operator extra link from XComs.
15771605 For example:
1578- ``[{'airflow.providers.google.cloud.links.bigquery.BigQueryDatasetLink ': {}}] ``
1606+ ``{'link-name-1 ': 'xcom-key-1'} ``
15791607
15801608 :param operator_extra_links: Operator Link
15811609 :return: Serialized Operator Link
15821610 """
1583- serialize_operator_extra_links = []
1584- for operator_extra_link in operator_extra_links :
1585- op_link_arguments = {
1586- param : cls .serialize (value ) for param , value in attrs .asdict (operator_extra_link ).items ()
1587- }
1588-
1589- module_path = (
1590- f"{ operator_extra_link .__class__ .__module__ } .{ operator_extra_link .__class__ .__name__ } "
1591- )
1592- serialize_operator_extra_links .append ({module_path : op_link_arguments })
1593-
1594- return serialize_operator_extra_links
1611+ return {link .name : link .xcom_key for link in operator_extra_links }
15951612
15961613 @classmethod
15971614 def serialize (cls , var : Any , * , strict : bool = False ) -> Any :
0 commit comments