This is an automated email from the ASF dual-hosted git repository. husseinawala pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push: new 42db67b57cc BREAKING_CHANGE: remove airflow.operators.email.EmailOperator in favor of SMTP provider operator (#46573) 42db67b57cc is described below commit 42db67b57ccfe0348f87cf451be080561b12d188 Author: Hussein Awala <huss...@awala.fr> AuthorDate: Sat Feb 8 21:21:49 2025 +0100 BREAKING_CHANGE: remove airflow.operators.email.EmailOperator in favor of SMTP provider operator (#46573) * BREAKING_CHANGE: remove airflow.operators.email.EmailOperator in favor of SMTP provider operator * Update newsfragments/46572.significant.rst Co-authored-by: Wei Lee <weilee...@gmail.com> * fix static checks --------- Co-authored-by: Wei Lee <weilee...@gmail.com> --- airflow/example_dags/example_dag_decorator.py | 21 +++--- airflow/operators/email.py | 91 ------------------------- docs/apache-airflow/core-concepts/operators.rst | 2 +- docs/apache-airflow/core-concepts/taskflow.rst | 2 +- docs/apache-airflow/operators-and-hooks-ref.rst | 3 - newsfragments/46572.significant.rst | 22 ++++++ tests/operators/test_email.py | 62 ----------------- 7 files changed, 33 insertions(+), 170 deletions(-) diff --git a/airflow/example_dags/example_dag_decorator.py b/airflow/example_dags/example_dag_decorator.py index 0fed70fa2e9..995bdcb3687 100644 --- a/airflow/example_dags/example_dag_decorator.py +++ b/airflow/example_dags/example_dag_decorator.py @@ -24,7 +24,7 @@ import pendulum from airflow.decorators import dag, task from airflow.models.baseoperator import BaseOperator -from airflow.operators.email import EmailOperator +from airflow.providers.standard.operators.bash import BashOperator if TYPE_CHECKING: from airflow.sdk.definitions.context import Context @@ -48,27 +48,24 @@ class GetRequestOperator(BaseOperator): catchup=False, tags=["example"], ) -def example_dag_decorator(email: str = "exam...@example.com"): +def example_dag_decorator(url: str = "http://httpbin.org/get"): """ - DAG to send server IP to email. + DAG to get IP address and echo it via BashOperator. - :param email: Email to send IP to. Defaults to exam...@example.com. + :param url: URL to get IP address from. Defaults to "http://httpbin.org/get". """ - get_ip = GetRequestOperator(task_id="get_ip", url="http://httpbin.org/get") + get_ip = GetRequestOperator(task_id="get_ip", url=url) @task(multiple_outputs=True) - def prepare_email(raw_json: dict[str, Any]) -> dict[str, str]: + def prepare_command(raw_json: dict[str, Any]) -> dict[str, str]: external_ip = raw_json["origin"] return { - "subject": f"Server connected from {external_ip}", - "body": f"Seems like today your server executing Airflow is connected from IP {external_ip}<br>", + "command": f"echo 'Seems like today your server executing Airflow is connected from IP {external_ip}'", } - email_info = prepare_email(get_ip.output) + command_info = prepare_command(get_ip.output) - EmailOperator( - task_id="send_email", to=email, subject=email_info["subject"], html_content=email_info["body"] - ) + BashOperator(task_id="echo_ip_info", bash_command=command_info["command"]) example_dag = example_dag_decorator() diff --git a/airflow/operators/email.py b/airflow/operators/email.py deleted file mode 100644 index 85ac709e2bd..00000000000 --- a/airflow/operators/email.py +++ /dev/null @@ -1,91 +0,0 @@ -# -# 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. -from __future__ import annotations - -from collections.abc import Sequence -from typing import TYPE_CHECKING, Any - -from airflow.models.baseoperator import BaseOperator -from airflow.utils.email import send_email - -if TYPE_CHECKING: - from airflow.sdk.definitions.context import Context - - -class EmailOperator(BaseOperator): - """ - Sends an email. - - :param to: list of emails to send the email to. (templated) - :param subject: subject line for the email. (templated) - :param html_content: content of the email, html markup - is allowed. (templated) - :param files: file names to attach in email (templated) - :param cc: list of recipients to be added in CC field - :param bcc: list of recipients to be added in BCC field - :param mime_subtype: MIME sub content type - :param mime_charset: character set parameter added to the Content-Type - header. - :param custom_headers: additional headers to add to the MIME message. - """ - - template_fields: Sequence[str] = ("to", "subject", "html_content", "files") - template_fields_renderers = {"html_content": "html"} - template_ext: Sequence[str] = (".html",) - ui_color = "#e6faf9" - - def __init__( - self, - *, - to: list[str] | str, - subject: str, - html_content: str, - files: list | None = None, - cc: list[str] | str | None = None, - bcc: list[str] | str | None = None, - mime_subtype: str = "mixed", - mime_charset: str = "utf-8", - conn_id: str | None = None, - custom_headers: dict[str, Any] | None = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.to = to - self.subject = subject - self.html_content = html_content - self.files = files or [] - self.cc = cc - self.bcc = bcc - self.mime_subtype = mime_subtype - self.mime_charset = mime_charset - self.conn_id = conn_id - self.custom_headers = custom_headers - - def execute(self, context: Context): - send_email( - self.to, - self.subject, - self.html_content, - files=self.files, - cc=self.cc, - bcc=self.bcc, - mime_subtype=self.mime_subtype, - mime_charset=self.mime_charset, - conn_id=self.conn_id, - custom_headers=self.custom_headers, - ) diff --git a/docs/apache-airflow/core-concepts/operators.rst b/docs/apache-airflow/core-concepts/operators.rst index e721faa9756..f6385ddc1b0 100644 --- a/docs/apache-airflow/core-concepts/operators.rst +++ b/docs/apache-airflow/core-concepts/operators.rst @@ -30,7 +30,6 @@ Airflow has a very extensive set of operators available, with some built-in to t - :class:`~airflow.providers.standard.operators.bash.BashOperator` - executes a bash command - :class:`~airflow.providers.standard.operators.python.PythonOperator` - calls an arbitrary Python function -- :class:`~airflow.operators.email.EmailOperator` - sends an email - Use the ``@task`` decorator to execute an arbitrary Python function. It doesn't support rendering jinja templates passed as arguments. .. note:: @@ -41,6 +40,7 @@ For a list of all core operators, see: :doc:`Core Operators and Hooks Reference If the operator you need isn't installed with Airflow by default, you can probably find it as part of our huge set of community :doc:`provider packages <apache-airflow-providers:index>`. Some popular operators from here include: +- :class:`~airflow.providers.smtp.operators.smtp.EmailOperator` - :class:`~airflow.providers.http.operators.http.HttpOperator` - :class:`~airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` - :class:`~airflow.providers.docker.operators.docker.DockerOperator` diff --git a/docs/apache-airflow/core-concepts/taskflow.rst b/docs/apache-airflow/core-concepts/taskflow.rst index b31494c1558..5eef4373d33 100644 --- a/docs/apache-airflow/core-concepts/taskflow.rst +++ b/docs/apache-airflow/core-concepts/taskflow.rst @@ -25,7 +25,7 @@ If you write most of your DAGs using plain Python code rather than Operators, th TaskFlow takes care of moving inputs and outputs between your Tasks using XComs for you, as well as automatically calculating dependencies - when you call a TaskFlow function in your DAG file, rather than executing it, you will get an object representing the XCom for the result (an ``XComArg``), that you can then use as inputs to downstream tasks or operators. For example:: from airflow.decorators import task - from airflow.operators.email import EmailOperator + from airflow.providers.email import EmailOperator @task def get_ip(): diff --git a/docs/apache-airflow/operators-and-hooks-ref.rst b/docs/apache-airflow/operators-and-hooks-ref.rst index a0307f51f06..511775d117e 100644 --- a/docs/apache-airflow/operators-and-hooks-ref.rst +++ b/docs/apache-airflow/operators-and-hooks-ref.rst @@ -56,9 +56,6 @@ For details see: :doc:`apache-airflow-providers:operators-and-hooks-ref/index`. * - :mod:`airflow.providers.standard.operators.empty` - - * - :mod:`airflow.operators.email` - - - * - :mod:`airflow.operators.generic_transfer` - diff --git a/newsfragments/46572.significant.rst b/newsfragments/46572.significant.rst new file mode 100644 index 00000000000..12d3feb0b4f --- /dev/null +++ b/newsfragments/46572.significant.rst @@ -0,0 +1,22 @@ +In Airflow 3.0, ``airflow.operators.email.EmailOperator`` is removed. + +Instead, users can install ``smtp`` provider and import ``EmailOperator`` from the the module ``airflow.providers.smtp.operators.smtp``. + +* Types of change + + * [ ] Dag changes + * [ ] Config changes + * [ ] API changes + * [ ] CLI changes + * [ ] Behaviour changes + * [ ] Plugin changes + * [ ] Dependency changes + * [x] Code interface changes + +* Migration rules needed + + * ruff + + * <RULE_ID> + + * [ ] ``airflow.operators.email`` → ``airflow.providers.smtp.operators.smtp.EmailOperator`` diff --git a/tests/operators/test_email.py b/tests/operators/test_email.py deleted file mode 100644 index df772beb95f..00000000000 --- a/tests/operators/test_email.py +++ /dev/null @@ -1,62 +0,0 @@ -# -# 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. -from __future__ import annotations - -import datetime -from unittest import mock - -import pytest - -from airflow.operators.email import EmailOperator -from airflow.utils import timezone - -from tests_common.test_utils.config import conf_vars - -pytestmark = pytest.mark.db_test - -DEFAULT_DATE = timezone.datetime(2016, 1, 1) -END_DATE = timezone.datetime(2016, 1, 2) -INTERVAL = datetime.timedelta(hours=12) -FROZEN_NOW = timezone.datetime(2016, 1, 2, 12, 1, 1) - -send_email_test = mock.Mock() - - -class TestEmailOperator: - def test_execute(self, dag_maker): - with conf_vars({("email", "email_backend"): "tests.operators.test_email.send_email_test"}): - with dag_maker( - "test_dag", - default_args={"owner": "airflow", "start_date": DEFAULT_DATE}, - schedule=INTERVAL, - serialized=True, - ): - task = EmailOperator( - to="airf...@example.com", - subject="Test Run", - html_content="The quick brown fox jumps over the lazy dog", - task_id="task", - files=["/tmp/Report-A-{{ ds }}.csv"], - custom_headers={"Reply-To": "reply...@example.com"}, - ) - dag_maker.create_dagrun() - task.run(start_date=DEFAULT_DATE, end_date=DEFAULT_DATE) - assert send_email_test.call_count == 1 - call_args = send_email_test.call_args.kwargs - assert call_args["files"] == ["/tmp/Report-A-2016-01-01.csv"] - assert call_args["custom_headers"] == {"Reply-To": "reply...@example.com"}