Add displaying multiple dates in airflow next_execution command (#9072)
The "next_execution" cli sub-command now accepts an optional number of executions to be returned. This is particularly useful for checking non-regular schedule intervals, such as those created by some cron expressions. Co-authored-by: Kamil Breguła <mik-laj@users.noreply.github.com>
This commit is contained in:
Родитель
93b8f3e48d
Коммит
c002b25e37
|
@ -99,6 +99,17 @@ class Arg:
|
||||||
parser.add_argument(*self.flags, **self.kwargs)
|
parser.add_argument(*self.flags, **self.kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def positive_int(value):
|
||||||
|
"""Define a positive int type for an argument."""
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
if value > 0:
|
||||||
|
return value
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
raise argparse.ArgumentTypeError(f"invalid positive int value: '{value}'")
|
||||||
|
|
||||||
|
|
||||||
# Shared
|
# Shared
|
||||||
ARG_DAG_ID = Arg(
|
ARG_DAG_ID = Arg(
|
||||||
("dag_id",),
|
("dag_id",),
|
||||||
|
@ -182,6 +193,13 @@ ARG_LIMIT = Arg(
|
||||||
("--limit",),
|
("--limit",),
|
||||||
help="Return a limited number of records")
|
help="Return a limited number of records")
|
||||||
|
|
||||||
|
# next_execution
|
||||||
|
ARG_NUM_EXECUTIONS = Arg(
|
||||||
|
("-n", "--num-executions"),
|
||||||
|
default=1,
|
||||||
|
type=positive_int,
|
||||||
|
help="The number of next execution datetimes to show")
|
||||||
|
|
||||||
# backfill
|
# backfill
|
||||||
ARG_MARK_SUCCESS = Arg(
|
ARG_MARK_SUCCESS = Arg(
|
||||||
("-m", "--mark-success"),
|
("-m", "--mark-success"),
|
||||||
|
@ -793,9 +811,10 @@ DAGS_COMMANDS = (
|
||||||
),
|
),
|
||||||
ActionCommand(
|
ActionCommand(
|
||||||
name='next_execution',
|
name='next_execution',
|
||||||
help="Get the next execution datetime of a DAG",
|
help="Get the next execution datetimes of a DAG. It returns one execution unless the "
|
||||||
|
"num-executions option is given",
|
||||||
func=lazy_load_command('airflow.cli.commands.dag_command.dag_next_execution'),
|
func=lazy_load_command('airflow.cli.commands.dag_command.dag_next_execution'),
|
||||||
args=(ARG_DAG_ID, ARG_SUBDIR),
|
args=(ARG_DAG_ID, ARG_SUBDIR, ARG_NUM_EXECUTIONS),
|
||||||
),
|
),
|
||||||
ActionCommand(
|
ActionCommand(
|
||||||
name='pause',
|
name='pause',
|
||||||
|
|
|
@ -275,7 +275,7 @@ def dag_next_execution(args):
|
||||||
dag = get_dag(args.subdir, args.dag_id)
|
dag = get_dag(args.subdir, args.dag_id)
|
||||||
|
|
||||||
if dag.get_is_paused():
|
if dag.get_is_paused():
|
||||||
print("[INFO] Please be reminded this DAG is PAUSED now.")
|
print("[INFO] Please be reminded this DAG is PAUSED now.", file=sys.stderr)
|
||||||
|
|
||||||
latest_execution_date = dag.get_latest_execution_date()
|
latest_execution_date = dag.get_latest_execution_date()
|
||||||
if latest_execution_date:
|
if latest_execution_date:
|
||||||
|
@ -283,11 +283,16 @@ def dag_next_execution(args):
|
||||||
|
|
||||||
if next_execution_dttm is None:
|
if next_execution_dttm is None:
|
||||||
print("[WARN] No following schedule can be found. " +
|
print("[WARN] No following schedule can be found. " +
|
||||||
"This DAG may have schedule interval '@once' or `None`.")
|
"This DAG may have schedule interval '@once' or `None`.", file=sys.stderr)
|
||||||
|
print(None)
|
||||||
|
else:
|
||||||
|
print(next_execution_dttm)
|
||||||
|
|
||||||
print(next_execution_dttm)
|
for _ in range(1, args.num_executions):
|
||||||
|
next_execution_dttm = dag.following_schedule(next_execution_dttm)
|
||||||
|
print(next_execution_dttm)
|
||||||
else:
|
else:
|
||||||
print("[WARN] Only applicable when there is execution record found for the DAG.")
|
print("[WARN] Only applicable when there is execution record found for the DAG.", file=sys.stderr)
|
||||||
print(None)
|
print(None)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,10 @@ import io
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
import pytz
|
|
||||||
|
|
||||||
from airflow import settings
|
from airflow import settings
|
||||||
from airflow.cli import cli_parser
|
from airflow.cli import cli_parser
|
||||||
|
@ -249,10 +248,10 @@ class TestCliDags(unittest.TestCase):
|
||||||
dr = session.query(DagRun).filter(DagRun.dag_id.in_(dag_ids))
|
dr = session.query(DagRun).filter(DagRun.dag_id.in_(dag_ids))
|
||||||
dr.delete(synchronize_session=False)
|
dr.delete(synchronize_session=False)
|
||||||
|
|
||||||
|
# Test None output
|
||||||
args = self.parser.parse_args(['dags',
|
args = self.parser.parse_args(['dags',
|
||||||
'next_execution',
|
'next_execution',
|
||||||
dag_ids[0]])
|
dag_ids[0]])
|
||||||
|
|
||||||
with contextlib.redirect_stdout(io.StringIO()) as temp_stdout:
|
with contextlib.redirect_stdout(io.StringIO()) as temp_stdout:
|
||||||
dag_command.dag_next_execution(args)
|
dag_command.dag_next_execution(args)
|
||||||
out = temp_stdout.getvalue()
|
out = temp_stdout.getvalue()
|
||||||
|
@ -262,23 +261,16 @@ class TestCliDags(unittest.TestCase):
|
||||||
|
|
||||||
# The details below is determined by the schedule_interval of example DAGs
|
# The details below is determined by the schedule_interval of example DAGs
|
||||||
now = DEFAULT_DATE
|
now = DEFAULT_DATE
|
||||||
next_execution_time_for_dag1 = pytz.utc.localize(
|
expected_output = [str(now + timedelta(days=1)),
|
||||||
datetime.combine(
|
str(now + timedelta(hours=4)),
|
||||||
now.date() + timedelta(days=1),
|
|
||||||
time(0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
next_execution_time_for_dag2 = now + timedelta(hours=4)
|
|
||||||
expected_output = [str(next_execution_time_for_dag1),
|
|
||||||
str(next_execution_time_for_dag2),
|
|
||||||
"None",
|
"None",
|
||||||
"None"]
|
"None"]
|
||||||
|
expected_output_2 = [str(now + timedelta(days=1)) + os.linesep + str(now + timedelta(days=2)),
|
||||||
|
str(now + timedelta(hours=4)) + os.linesep + str(now + timedelta(hours=8)),
|
||||||
|
"None",
|
||||||
|
"None"]
|
||||||
|
|
||||||
for i, dag_id in enumerate(dag_ids):
|
for i, dag_id in enumerate(dag_ids):
|
||||||
args = self.parser.parse_args(['dags',
|
|
||||||
'next_execution',
|
|
||||||
dag_id])
|
|
||||||
|
|
||||||
dag = self.dagbag.dags[dag_id]
|
dag = self.dagbag.dags[dag_id]
|
||||||
# Create a DagRun for each DAG, to prepare for next step
|
# Create a DagRun for each DAG, to prepare for next step
|
||||||
dag.create_dagrun(
|
dag.create_dagrun(
|
||||||
|
@ -288,11 +280,26 @@ class TestCliDags(unittest.TestCase):
|
||||||
state=State.FAILED
|
state=State.FAILED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test num-executions = 1 (default)
|
||||||
|
args = self.parser.parse_args(['dags',
|
||||||
|
'next_execution',
|
||||||
|
dag_id])
|
||||||
with contextlib.redirect_stdout(io.StringIO()) as temp_stdout:
|
with contextlib.redirect_stdout(io.StringIO()) as temp_stdout:
|
||||||
dag_command.dag_next_execution(args)
|
dag_command.dag_next_execution(args)
|
||||||
out = temp_stdout.getvalue()
|
out = temp_stdout.getvalue()
|
||||||
self.assertIn(expected_output[i], out)
|
self.assertIn(expected_output[i], out)
|
||||||
|
|
||||||
|
# Test num-executions = 2
|
||||||
|
args = self.parser.parse_args(['dags',
|
||||||
|
'next_execution',
|
||||||
|
dag_id,
|
||||||
|
'--num-executions',
|
||||||
|
'2'])
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as temp_stdout:
|
||||||
|
dag_command.dag_next_execution(args)
|
||||||
|
out = temp_stdout.getvalue()
|
||||||
|
self.assertIn(expected_output_2[i], out)
|
||||||
|
|
||||||
# Clean up before leaving
|
# Clean up before leaving
|
||||||
with create_session() as session:
|
with create_session() as session:
|
||||||
dr = session.query(DagRun).filter(DagRun.dag_id.in_(dag_ids))
|
dr = session.query(DagRun).filter(DagRun.dag_id.in_(dag_ids))
|
||||||
|
|
|
@ -174,3 +174,10 @@ class TestCli(TestCase):
|
||||||
for cmd_args in all_command_as_args:
|
for cmd_args in all_command_as_args:
|
||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
parser.parse_args([*cmd_args, '--help'])
|
parser.parse_args([*cmd_args, '--help'])
|
||||||
|
|
||||||
|
def test_positive_int(self):
|
||||||
|
self.assertEqual(1, cli_parser.positive_int('1'))
|
||||||
|
|
||||||
|
with self.assertRaises(argparse.ArgumentTypeError):
|
||||||
|
cli_parser.positive_int('0')
|
||||||
|
cli_parser.positive_int('-1')
|
||||||
|
|
Загрузка…
Ссылка в новой задаче