## Licensed to the Apache Software Foundation (ASF) under one# or more contributor license agreements. See the NOTICE file# distributed with this work for additional information# regarding copyright ownership. The ASF licenses this file# to you under the Apache License, Version 2.0 (the# "License"); you may not use this file except in compliance# with the License. You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing,# software distributed under the License is distributed on an# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY# KIND, either express or implied. See the License for the# specific language governing permissions and limitations# under the License.importdatetimeimportjsonimportloggingimportpicklefromtypingimportTYPE_CHECKING,Any,Iterable,Optional,Type,Union,cast,overloadimportpendulumfromsqlalchemyimportColumn,LargeBinary,Stringfromsqlalchemy.ext.associationproxyimportassociation_proxyfromsqlalchemy.ormimportQuery,Session,reconstructor,relationshipfromairflow.configurationimportconffromairflow.models.baseimportCOLLATION_ARGS,ID_LEN,Basefromairflow.utilsimporttimezonefromairflow.utils.helpersimportis_containerfromairflow.utils.log.logging_mixinimportLoggingMixinfromairflow.utils.sessionimportNEW_SESSION,provide_sessionfromairflow.utils.sqlalchemyimportUtcDateTime
[docs]definit_on_load(self):""" Called by the ORM after the instance has been loaded from the DB or otherwise reconstituted i.e automatically deserialize Xcom value when loading from DB. """self.value=self.orm_deserialize_value()
@overload@classmethoddefset(cls,key:str,value:Any,*,dag_id:str,task_id:str,run_id:str,session:Session=NEW_SESSION,)->None:"""Store an XCom value. A deprecated form of this function accepts ``execution_date`` instead of ``run_id``. The two arguments are mutually exclusive. :param key: Key to store the XCom. :param value: XCom value to store. :param dag_id: DAG ID. :param task_id: Task ID. :param run_id: DAG run ID for the task. :param session: Database session. If not given, a new session will be created for this function. :type session: sqlalchemy.orm.session.Session """@overload@classmethoddefset(cls,key:str,value:Any,task_id:str,dag_id:str,execution_date:datetime.datetime,session:Session=NEW_SESSION,)->None:""":sphinx-autoapi-skip:"""@classmethod@provide_sessiondefset(cls,key:str,value:Any,task_id:str,dag_id:str,execution_date:Optional[datetime.datetime]=None,session:Session=NEW_SESSION,*,run_id:Optional[str]=None,)->None:""":sphinx-autoapi-skip:"""ifnot(execution_dateisNone)^(run_idisNone):raiseValueError("Exactly one of execution_date or run_id must be passed")ifrun_id:fromairflow.models.dagrunimportDagRundag_run=session.query(DagRun).filter_by(dag_id=dag_id,run_id=run_id).one()execution_date=dag_run.execution_date# Remove duplicate XComs and insert a new one.session.query(cls).filter(cls.key==key,cls.execution_date==execution_date,cls.task_id==task_id,cls.dag_id==dag_id,).delete()new=cast(Any,cls)(# Work around Mypy complaining model not defining '__init__'.key=key,value=cls.serialize_value(value),execution_date=execution_date,task_id=task_id,dag_id=dag_id,)session.add(new)session.flush()@overload@classmethoddefget_one(cls,*,run_id:str,key:Optional[str]=None,task_id:Optional[str]=None,dag_id:Optional[str]=None,include_prior_dates:bool=False,session:Session=NEW_SESSION,)->Optional[Any]:"""Retrieve an XCom value, optionally meeting certain criteria. This method returns "full" XCom values (i.e. uses ``deserialize_value`` from the XCom backend). Use :meth:`get_many` if you want the "shortened" value via ``orm_deserialize_value``. If there are no results, *None* is returned. A deprecated form of this function accepts ``execution_date`` instead of ``run_id``. The two arguments are mutually exclusive. :param run_id: DAG run ID for the task. :param key: A key for the XCom. If provided, only XCom with matching keys will be returned. Pass *None* (default) to remove the filter. :param task_id: Only XCom from task with matching ID will be pulled. Pass *None* (default) to remove the filter. :param dag_id: Only pull XCom from this DAG. If *None* (default), the DAG of the calling task is used. :param include_prior_dates: If *False* (default), only XCom from the specified DAG run is returned. If *True*, the latest matching XCom is returned regardless of the run it belongs to. :param session: Database session. If not given, a new session will be created for this function. :type session: sqlalchemy.orm.session.Session """@overload@classmethoddefget_one(cls,execution_date:pendulum.DateTime,key:Optional[str]=None,task_id:Optional[str]=None,dag_id:Optional[str]=None,include_prior_dates:bool=False,session:Session=NEW_SESSION,)->Optional[Any]:""":sphinx-autoapi-skip:"""@classmethod@provide_sessiondefget_one(cls,execution_date:Optional[pendulum.DateTime]=None,key:Optional[str]=None,task_id:Optional[Union[str,Iterable[str]]]=None,dag_id:Optional[Union[str,Iterable[str]]]=None,include_prior_dates:bool=False,session:Session=NEW_SESSION,*,run_id:Optional[str]=None,)->Optional[Any]:""":sphinx-autoapi-skip:"""ifnot(execution_dateisNone)^(run_idisNone):raiseValueError("Exactly one of execution_date or run_id must be passed")ifrun_idisnotNone:query=cls.get_many(run_id=run_id,key=key,task_ids=task_id,dag_ids=dag_id,include_prior_dates=include_prior_dates,session=session,)elifexecution_dateisnotNone:query=cls.get_many(execution_date=execution_date,key=key,task_ids=task_id,dag_ids=dag_id,include_prior_dates=include_prior_dates,session=session,)else:raiseRuntimeError("Should not happen?")result=query.with_entities(cls.value).first()ifresult:returncls.deserialize_value(result)returnNone@overload@classmethoddefget_many(cls,*,run_id:str,key:Optional[str]=None,task_ids:Union[str,Iterable[str],None]=None,dag_ids:Union[str,Iterable[str],None]=None,include_prior_dates:bool=False,limit:Optional[int]=None,session:Session=NEW_SESSION,)->Query:"""Composes a query to get one or more XCom entries. This function returns an SQLAlchemy query of full XCom objects. If you just want one stored value, use :meth:`get_one` instead. A deprecated form of this function accepts ``execution_date`` instead of ``run_id``. The two arguments are mutually exclusive. :param run_id: DAG run ID for the task. :param key: A key for the XComs. If provided, only XComs with matching keys will be returned. Pass *None* (default) to remove the filter. :param task_ids: Only XComs from task with matching IDs will be pulled. Pass *None* (default) to remove the filter. :param dag_id: Only pulls XComs from this DAG. If *None* (default), the DAG of the calling task is used. :param include_prior_dates: If *False* (default), only XComs from the specified DAG run are returned. If *True*, all matching XComs are returned regardless of the run it belongs to. :param session: Database session. If not given, a new session will be created for this function. :type session: sqlalchemy.orm.session.Session """@overload@classmethoddefget_many(cls,execution_date:pendulum.DateTime,key:Optional[str]=None,task_ids:Union[str,Iterable[str],None]=None,dag_ids:Union[str,Iterable[str],None]=None,include_prior_dates:bool=False,limit:Optional[int]=None,session:Session=NEW_SESSION,)->Query:""":sphinx-autoapi-skip:"""@classmethod@provide_sessiondefget_many(cls,execution_date:Optional[pendulum.DateTime]=None,key:Optional[str]=None,task_ids:Optional[Union[str,Iterable[str]]]=None,dag_ids:Optional[Union[str,Iterable[str]]]=None,include_prior_dates:bool=False,limit:Optional[int]=None,session:Session=NEW_SESSION,*,run_id:Optional[str]=None,)->Query:""":sphinx-autoapi-skip:"""ifnot(execution_dateisNone)^(run_idisNone):raiseValueError("Exactly one of execution_date or run_id must be passed")filters=[]ifkey:filters.append(cls.key==key)iftask_ids:ifis_container(task_ids):filters.append(cls.task_id.in_(task_ids))else:filters.append(cls.task_id==task_ids)ifdag_ids:ifis_container(dag_ids):filters.append(cls.dag_id.in_(dag_ids))else:filters.append(cls.dag_id==dag_ids)ifinclude_prior_dates:ifexecution_dateisNone:# In theory it would be possible to build a subquery that joins to DagRun and then gets the# execution dates. Lets do that for 2.3raiseValueError("Using include_prior_dates needs an execution_date to be passed")filters.append(cls.execution_date<=execution_date)elifexecution_dateisnotNone:filters.append(cls.execution_date==execution_date)query=session.query(cls).filter(*filters)ifrun_id:fromairflow.models.dagrunimportDagRunquery=query.join(cls.dag_run).filter(DagRun.run_id==run_id)query=query.order_by(cls.execution_date.desc(),cls.timestamp.desc())iflimit:returnquery.limit(limit)else:returnquery@classmethod@provide_session
[docs]defdelete(cls,xcoms:Union["XCom",Iterable["XCom"]],session:Session)->None:"""Delete one or multiple XCom entries."""ifisinstance(xcoms,XCom):xcoms=[xcoms]forxcominxcoms:ifnotisinstance(xcom,XCom):raiseTypeError(f'Expected XCom; received {xcom.__class__.__name__}')session.delete(xcom)session.commit()
@overload@classmethoddefclear(cls,*,dag_id:str,task_id:str,run_id:str,session:Optional[Session]=None)->None:"""Clear all XCom data from the database for the given task instance. A deprecated form of this function accepts ``execution_date`` instead of ``run_id``. The two arguments are mutually exclusive. :param dag_id: ID of DAG to clear the XCom for. :param task_id: ID of task to clear the XCom for. :param run_id: ID of DAG run to clear the XCom for. :param session: Database session. If not given, a new session will be created for this function. :type session: sqlalchemy.orm.session.Session """@overload@classmethoddefclear(cls,execution_date:pendulum.DateTime,dag_id:str,task_id:str,session:Session=NEW_SESSION,)->None:""":sphinx-autoapi-skip:"""@classmethod@provide_sessiondefclear(cls,execution_date:Optional[pendulum.DateTime]=None,dag_id:Optional[str]=None,task_id:Optional[str]=None,run_id:Optional[str]=None,session:Session=NEW_SESSION,)->None:""":sphinx-autoapi-skip:"""# Given the historic order of this function (execution_date was first argument) to add a new optional# param we need to add default values for everything :(ifdag_idisNone:raiseTypeError("clear() missing required argument: dag_id")iftask_idisNone:raiseTypeError("clear() missing required argument: task_id")ifnot(execution_dateisNone)^(run_idisNone):raiseValueError("Exactly one of execution_date or run_id must be passed")query=session.query(cls).filter(cls.dag_id==dag_id,cls.task_id==task_id,)ifexecution_dateisnotNone:query=query.filter(cls.execution_date==execution_date)else:fromairflow.models.dagrunimportDagRunquery=query.join(cls.dag_run).filter(DagRun.run_id==run_id)returnquery.delete()@staticmethod
[docs]defserialize_value(value:Any):"""Serialize Xcom value to str or pickled object"""ifconf.getboolean('core','enable_xcom_pickling'):returnpickle.dumps(value)try:returnjson.dumps(value).encode('UTF-8')except(ValueError,TypeError):log.error("Could not serialize the XCom value into JSON."" If you are using pickle instead of JSON for XCom,"" then you need to enable pickle support for XCom"" in your airflow config.")raise
@staticmethod
[docs]defdeserialize_value(result:"XCom")->Any:"""Deserialize XCom value from str or pickle object"""ifconf.getboolean('core','enable_xcom_pickling'):try:returnpickle.loads(result.value)exceptpickle.UnpicklingError:returnjson.loads(result.value.decode('UTF-8'))else:try:returnjson.loads(result.value.decode('UTF-8'))except(json.JSONDecodeError,UnicodeDecodeError):returnpickle.loads(result.value)
[docs]deform_deserialize_value(self)->Any:""" Deserialize method which is used to reconstruct ORM XCom object. This method should be overridden in custom XCom backends to avoid unnecessary request or other resource consuming operations when creating XCom orm model. This is used when viewing XCom listing in the webserver, for example. """returnBaseXCom.deserialize_value(self)
[docs]defresolve_xcom_backend()->Type[BaseXCom]:"""Resolves custom XCom class"""clazz=conf.getimport("core","xcom_backend",fallback=f"airflow.models.xcom.{BaseXCom.__name__}")ifclazz:ifnotissubclass(clazz,BaseXCom):raiseTypeError(f"Your custom XCom class `{clazz.__name__}` is not a subclass of `{BaseXCom.__name__}`.")returnclazzreturnBaseXCom
ifTYPE_CHECKING:
[docs]XCom=BaseXCom# Hack to avoid Mypy "Variable 'XCom' is not valid as a type".