| #!/usr/bin/python3 | |
| # | |
| # 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 | |
| # This tool is based on the Superset send_email script: | |
| # https://github.com/apache/incubator-superset/blob/master/RELEASING/send_email.py | |
| import os | |
| import shutil | |
| import smtplib | |
| import ssl | |
| import sys | |
| try: | |
| import jinja2 | |
| except ModuleNotFoundError: | |
| sys.exit("Jinja2 is a required dependency for this script") | |
| try: | |
| import rich_click as click | |
| except ModuleNotFoundError: | |
| sys.exit("Click is a required dependency for this script") | |
| SMTP_PORT = 587 | |
| SMTP_SERVER = "mail-relay.apache.org" | |
| MAILING_LIST = {"dev": "dev@airflow.apache.org", "users": "users@airflow.apache.org"} | |
| def string_comma_to_list(message: str) -> list[str]: | |
| """ | |
| Split string to list | |
| """ | |
| return message.split(",") if message else [] | |
| def send_email( | |
| smtp_server: str, | |
| smtp_port: int, | |
| username: str, | |
| password: str, | |
| sender_email: str, | |
| receiver_email: str | list, | |
| message: str, | |
| ): | |
| """ | |
| Send a simple text email (SMTP) | |
| """ | |
| context = ssl.create_default_context() | |
| with smtplib.SMTP(smtp_server, smtp_port) as server: | |
| server.starttls(context=context) | |
| server.login(username, password) | |
| server.sendmail(sender_email, receiver_email, message) | |
| def render_template(template_file: str, **kwargs) -> str: | |
| """ | |
| Simple render template based on named parameters | |
| :param template_file: The template file location | |
| :kwargs: Named parameters to use when rendering the template | |
| :return: Rendered template | |
| """ | |
| dir_path = os.path.dirname(os.path.realpath(__file__)) | |
| template = jinja2.Template(open(os.path.join(dir_path, template_file)).read()) | |
| return template.render(kwargs) | |
| def show_message(entity: str, message: str): | |
| """ | |
| Show message on the Command Line | |
| """ | |
| width, _ = shutil.get_terminal_size() | |
| click.secho("-" * width, fg="blue") | |
| click.secho(f"{entity} Message:", fg="bright_red", bold=True) | |
| click.secho("-" * width, fg="blue") | |
| click.echo(message) | |
| click.secho("-" * width, fg="blue") | |
| def inter_send_email( | |
| username: str, password: str, sender_email: str, receiver_email: str | list, message: str | |
| ): | |
| """ | |
| Send email using SMTP | |
| """ | |
| show_message("SMTP", message) | |
| click.confirm("Is the Email message ok?", abort=True) | |
| try: | |
| send_email( | |
| SMTP_SERVER, | |
| SMTP_PORT, | |
| username, | |
| password, | |
| sender_email, | |
| receiver_email, | |
| message, | |
| ) | |
| click.secho("✅ Email sent successfully", fg="green") | |
| except smtplib.SMTPAuthenticationError: | |
| sys.exit("SMTP User authentication error, Email not sent!") | |
| except Exception as e: | |
| sys.exit(f"SMTP exception {e}") | |
| class BaseParameters: | |
| """ | |
| Base Class to send emails using Apache Creds and for Jinja templating | |
| """ | |
| def __init__(self, name=None, email=None, username=None, password=None, version=None, version_rc=None): | |
| self.name = name | |
| self.email = email | |
| self.username = username | |
| self.password = password | |
| self.version = version | |
| self.version_rc = version_rc | |
| self.template_arguments = {} | |
| def __repr__(self): | |
| return f"Apache Credentials: {self.email}/{self.username}/{self.version}/{self.version_rc}" | |
| @click.group(context_settings=dict(help_option_names=["-h", "--help"])) | |
| @click.pass_context | |
| @click.option( | |
| "-e", | |
| "--apache_email", | |
| prompt="Apache Email", | |
| envvar="APACHE_EMAIL", | |
| show_envvar=True, | |
| help="Your Apache email will be used for SMTP From", | |
| required=True, | |
| ) | |
| @click.option( | |
| "-u", | |
| "--apache_username", | |
| prompt="Apache Username", | |
| envvar="APACHE_USERNAME", | |
| show_envvar=True, | |
| help="Your LDAP Apache username", | |
| required=True, | |
| ) | |
| @click.password_option( | |
| "-p", | |
| "--apache_password", | |
| prompt="Apache Password", | |
| envvar="APACHE_PASSWORD", | |
| show_envvar=True, | |
| help="Your LDAP Apache password", | |
| required=True, | |
| ) | |
| @click.option( | |
| "-v", | |
| "--version", | |
| prompt="Version", | |
| envvar="AIRFLOW_VERSION", | |
| show_envvar=True, | |
| help="Release Version", | |
| required=True, | |
| ) | |
| @click.option( | |
| "-rc", | |
| "--version_rc", | |
| prompt="Version (with RC)", | |
| envvar="AIRFLOW_VERSION_RC", | |
| show_envvar=True, | |
| help="Release Candidate Version", | |
| required=True, | |
| ) | |
| @click.option( | |
| "-n", | |
| "--name", | |
| prompt="Your Name", | |
| default=lambda: os.environ.get("USER", ""), | |
| show_default="Current User", | |
| help="Name of the Release Manager", | |
| type=click.STRING, | |
| required=True, | |
| ) | |
| def cli( | |
| ctx, | |
| apache_email: str, | |
| apache_username: str, | |
| apache_password: str, | |
| version: str, | |
| version_rc: str, | |
| name: str, | |
| ): | |
| """ | |
| 🚀 CLI to send emails for the following: | |
| \b | |
| * Voting thread for the rc | |
| * Result of the voting for the rc | |
| * Announcing that the new version has been released | |
| """ | |
| base_parameters = BaseParameters( | |
| name, apache_email, apache_username, apache_password, version, version_rc | |
| ) | |
| base_parameters.template_arguments["version"] = base_parameters.version | |
| base_parameters.template_arguments["version_rc"] = base_parameters.version_rc | |
| base_parameters.template_arguments["sender_email"] = base_parameters.email | |
| base_parameters.template_arguments["release_manager"] = base_parameters.name | |
| ctx.obj = base_parameters | |
| @cli.command("vote") | |
| @click.option( | |
| "--receiver_email", | |
| default=MAILING_LIST.get("dev"), | |
| type=click.STRING, | |
| prompt="The receiver email (To:)", | |
| ) | |
| @click.pass_obj | |
| def vote(base_parameters, receiver_email: str): | |
| """ | |
| Send email calling for Votes on RC | |
| """ | |
| template_file = "templates/vote_email.j2" | |
| base_parameters.template_arguments["receiver_email"] = receiver_email | |
| message = render_template(template_file, **base_parameters.template_arguments) | |
| inter_send_email( | |
| base_parameters.username, | |
| base_parameters.password, | |
| base_parameters.template_arguments["sender_email"], | |
| base_parameters.template_arguments["receiver_email"], | |
| message, | |
| ) | |
| if click.confirm("Show Slack message for announcement?", default=True): | |
| base_parameters.template_arguments["slack_rc"] = False | |
| slack_msg = render_template("templates/slack.j2", **base_parameters.template_arguments) | |
| show_message("Slack", slack_msg) | |
| @cli.command("result") | |
| @click.option( | |
| "-re", | |
| "--receiver_email", | |
| default=MAILING_LIST.get("dev"), | |
| type=click.STRING, | |
| prompt="The receiver email (To:)", | |
| ) | |
| @click.option( | |
| "--vote_bindings", | |
| default="", | |
| type=click.STRING, | |
| prompt="A List of people with +1 binding vote (ex: Max,Grace,Krist)", | |
| ) | |
| @click.option( | |
| "--vote_nonbindings", | |
| default="", | |
| type=click.STRING, | |
| prompt="A List of people with +1 non binding vote (ex: Ville)", | |
| ) | |
| @click.option( | |
| "--vote_negatives", | |
| default="", | |
| type=click.STRING, | |
| prompt="A List of people with -1 vote (ex: John)", | |
| ) | |
| @click.pass_obj | |
| def result( | |
| base_parameters, | |
| receiver_email: str, | |
| vote_bindings: str, | |
| vote_nonbindings: str, | |
| vote_negatives: str, | |
| ): | |
| """ | |
| Send email with results of voting on RC | |
| """ | |
| template_file = "templates/result_email.j2" | |
| base_parameters.template_arguments["receiver_email"] = receiver_email | |
| base_parameters.template_arguments["vote_bindings"] = string_comma_to_list(vote_bindings) | |
| base_parameters.template_arguments["vote_nonbindings"] = string_comma_to_list(vote_nonbindings) | |
| base_parameters.template_arguments["vote_negatives"] = string_comma_to_list(vote_negatives) | |
| message = render_template(template_file, **base_parameters.template_arguments) | |
| inter_send_email( | |
| base_parameters.username, | |
| base_parameters.password, | |
| base_parameters.template_arguments["sender_email"], | |
| base_parameters.template_arguments["receiver_email"], | |
| message, | |
| ) | |
| @cli.command("announce") | |
| @click.option( | |
| "--receiver_email", | |
| default=",".join(MAILING_LIST.values()), | |
| prompt="The receiver email (To:)", | |
| help="Receiver's email address. If more than 1, separate them by comma", | |
| ) | |
| @click.pass_obj | |
| def announce(base_parameters, receiver_email: str): | |
| """ | |
| Send email to announce release of the new version | |
| """ | |
| receiver_emails: list[str] = string_comma_to_list(receiver_email) | |
| template_file = "templates/announce_email.j2" | |
| base_parameters.template_arguments["receiver_email"] = receiver_emails | |
| message = render_template(template_file, **base_parameters.template_arguments) | |
| inter_send_email( | |
| base_parameters.username, | |
| base_parameters.password, | |
| base_parameters.template_arguments["sender_email"], | |
| base_parameters.template_arguments["receiver_email"], | |
| message, | |
| ) | |
| if click.confirm("Show Slack message for announcement?", default=True): | |
| base_parameters.template_arguments["slack_rc"] = False | |
| slack_msg = render_template("templates/slack.j2", **base_parameters.template_arguments) | |
| show_message("Slack", slack_msg) | |
| if click.confirm("Show Twitter message for announcement?", default=True): | |
| twitter_msg = render_template("templates/twitter.j2", **base_parameters.template_arguments) | |
| show_message("Twitter", twitter_msg) | |
| if __name__ == "__main__": | |
| cli() |