This is an automated email from the ASF dual-hosted git repository.
Lee-W 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 e9f89d6aceb Add airflowctl tasks states-for-dag-run command (#69366)
e9f89d6aceb is described below
commit e9f89d6aceb85025a7c3f81188a8cb9d2b5ffc21
Author: PoAn Yang <[email protected]>
AuthorDate: Tue Jul 21 23:19:36 2026 +0900
Add airflowctl tasks states-for-dag-run command (#69366)
Signed-off-by: PoAn Yang <[email protected]>
---
.../src/airflow/cli/commands/task_command.py | 3 +-
.../unit/cli/commands/test_command_deprecations.py | 2 +
.../airflowctl_tests/test_airflowctl_commands.py | 5 +
airflow-ctl/docs/conf.py | 5 +-
airflow-ctl/docs/howto/index.rst | 14 +
airflow-ctl/docs/images/command_hashes.txt | 4 +-
airflow-ctl/docs/images/output_main.svg | 142 ++++++-----
airflow-ctl/docs/images/output_taskinstances.svg | 97 +++++++
airflow-ctl/docs/images/output_tasks.svg | 101 ++++++++
airflow-ctl/docs/spelling_wordlist.txt | 1 +
airflow-ctl/src/airflowctl/api/client.py | 7 +
airflow-ctl/src/airflowctl/api/operations.py | 21 +-
airflow-ctl/src/airflowctl/ctl/cli_config.py | 38 ++-
.../src/airflowctl/ctl/commands/task_command.py | 99 +++++++
airflow-ctl/src/airflowctl/ctl/help_texts.yaml | 3 +
.../tests/airflow_ctl/api/test_operations.py | 37 +++
.../airflow_ctl/ctl/commands/test_task_command.py | 284 +++++++++++++++++++++
.../tests/airflow_ctl/ctl/test_cli_config.py | 2 +
.../in_container/run_capture_airflowctl_help.py | 2 +
19 files changed, 797 insertions(+), 70 deletions(-)
diff --git a/airflow-core/src/airflow/cli/commands/task_command.py
b/airflow-core/src/airflow/cli/commands/task_command.py
index af944526184..e2a1ab28e04 100644
--- a/airflow-core/src/airflow/cli/commands/task_command.py
+++ b/airflow-core/src/airflow/cli/commands/task_command.py
@@ -30,7 +30,7 @@ from typing import TYPE_CHECKING, Protocol, cast
from airflow import settings
from airflow._shared.timezones import timezone
from airflow.cli.simple_table import AirflowConsole
-from airflow.cli.utils import fetch_dag_run_from_run_id_or_logical_date_string
+from airflow.cli.utils import deprecated_for_airflowctl,
fetch_dag_run_from_run_id_or_logical_date_string
from airflow.exceptions import AirflowConfigException, DagRunNotFound,
NotMapped, TaskInstanceNotFound
from airflow.models import TaskInstance
from airflow.models.dag_version import DagVersion
@@ -354,6 +354,7 @@ def _guess_debugger() -> _SupportedDebugger:
raise exc
+@deprecated_for_airflowctl("airflowctl tasks states-for-dag-run")
@cli_utils.action_cli(check_db=False)
@suppress_logs_and_warning
@providers_configuration_loaded
diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
index 20fc2035953..65982d65adb 100644
--- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
+++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py
@@ -37,6 +37,7 @@ from airflow.cli.commands import (
dag_command,
pool_command,
provider_command,
+ task_command,
variable_command,
)
@@ -74,6 +75,7 @@ MIGRATED_CLI_COMMANDS = [
(provider_command.providers_list, "airflowctl providers list"),
(config_command.get_value, "airflowctl config get"),
(config_command.show_config, "airflowctl config list"),
+ (task_command.task_states_for_dag_run, "airflowctl tasks
states-for-dag-run"),
]
diff --git
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index 924cbde90d5..87d9851cbf0 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -96,6 +96,11 @@ TEST_COMMANDS = [
"dags update example_bash_operator --no-is-paused",
# Dag Run commands
"dagrun list --dag-id example_bash_operator --state success --limit=1",
+ # Tasks commands
+ 'tasks states-for-dag-run example_bash_operator "manual__{date_param}"',
+ 'tasks states-for-dag-run example_bash_operator --logical-date
"{date_param}"',
+ # Task Instances commands
+ 'taskinstances list example_bash_operator "manual__{date_param}"',
# XCom commands - need a Dag run with completed tasks
'xcom add example_bash_operator "manual__{date_param}" runme_0 {xcom_key}
\'{{"test": "value"}}\'',
'xcom get example_bash_operator "manual__{date_param}" runme_0 {xcom_key}',
diff --git a/airflow-ctl/docs/conf.py b/airflow-ctl/docs/conf.py
index 055de186d3e..6580a8cbf0c 100644
--- a/airflow-ctl/docs/conf.py
+++ b/airflow-ctl/docs/conf.py
@@ -283,7 +283,10 @@ exampleinclude_sourceroot = os.path.abspath("..")
redirects_file = "redirects.txt"
# -- Options for sphinxcontrib-spelling
----------------------------------------
-spelling_word_list_filename = [SPELLING_WORDLIST_PATH.as_posix()]
+spelling_word_list_filename = [
+ SPELLING_WORDLIST_PATH.as_posix(),
+ (pathlib.Path(__file__).parent / "spelling_wordlist.txt").as_posix(),
+]
spelling_exclude_patterns = ["project.rst", "changelog.rst"]
spelling_ignore_contributor_names = False
diff --git a/airflow-ctl/docs/howto/index.rst b/airflow-ctl/docs/howto/index.rst
index 59f806d4f44..92f92443215 100644
--- a/airflow-ctl/docs/howto/index.rst
+++ b/airflow-ctl/docs/howto/index.rst
@@ -213,6 +213,20 @@ These visual references show the full command syntax,
options, and parameters fo
:width: 60%
:alt: airflowctl Providers Command
+**Task Instances**
+''''''''''''''''''
+.. image:: ../images/output_taskinstances.svg
+ :target:
https://raw.githubusercontent.com/apache/airflow/main/airflow-ctl/docs/images/output_taskinstances.svg
+ :width: 60%
+ :alt: airflowctl Task Instances Command
+
+**Tasks**
+'''''''''
+.. image:: ../images/output_tasks.svg
+ :target:
https://raw.githubusercontent.com/apache/airflow/main/airflow-ctl/docs/images/output_tasks.svg
+ :width: 60%
+ :alt: airflowctl Tasks Command
+
**Variables**
'''''''''''''
.. image:: ../images/output_variables.svg
diff --git a/airflow-ctl/docs/images/command_hashes.txt
b/airflow-ctl/docs/images/command_hashes.txt
index 36eec17c2eb..10e59c5959f 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -1,4 +1,4 @@
-main:27a22c00dcf32e7a1a4f06672dc8e3c8
+main:0460d9c03248bee26207b20b05aa36b9
assets:6419e20452692f577c4c6f570b74be0c
auth:d79e9c7d00c432bdbcbc2a86e2e32053
backfill:74c8737b0a62a86ed3605fa9e6165874
@@ -9,6 +9,8 @@ dagrun:07035226eaaff0a557d3ad9aab4b41b5
jobs:a5b644c5da8889443bb40ee10b599270
pools:19efe105b9515ab1926ebcaf0e028d71
providers:34502fe09dc0b8b0a13e7e46efdffda6
+taskinstances:7e323968c0b585287c2a4ab4339aee1f
+tasks:089b19625c893d189b5fc02b3abd547e
variables:f8fc76d3d398b2780f4e97f7cd816646
version:31f4efdf8de0dbaaa4fac71ff7efecc3
plugins:4864fd8f356704bd2b3cd1aec3567e35
diff --git a/airflow-ctl/docs/images/output_main.svg
b/airflow-ctl/docs/images/output_main.svg
index f586877bce8..9c8cec5269b 100644
--- a/airflow-ctl/docs/images/output_main.svg
+++ b/airflow-ctl/docs/images/output_main.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 708.8"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 782.0"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,146 +19,158 @@
font-weight: 700;
}
- .terminal-2670787131-matrix {
+ .terminal-3519450412-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-2670787131-title {
+ .terminal-3519450412-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-2670787131-r1 { fill: #ff8700 }
-.terminal-2670787131-r2 { fill: #c5c8c6 }
-.terminal-2670787131-r3 { fill: #808080 }
-.terminal-2670787131-r4 { fill: #68a0b3 }
+ .terminal-3519450412-r1 { fill: #ff8700 }
+.terminal-3519450412-r2 { fill: #c5c8c6 }
+.terminal-3519450412-r3 { fill: #808080 }
+.terminal-3519450412-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-2670787131-clip-terminal">
- <rect x="0" y="0" width="914.0" height="657.8" />
+ <clipPath id="terminal-3519450412-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="731.0" />
</clipPath>
- <clipPath id="terminal-2670787131-line-0">
+ <clipPath id="terminal-3519450412-line-0">
<rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-1">
+<clipPath id="terminal-3519450412-line-1">
<rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-2">
+<clipPath id="terminal-3519450412-line-2">
<rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-3">
+<clipPath id="terminal-3519450412-line-3">
<rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-4">
+<clipPath id="terminal-3519450412-line-4">
<rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-5">
+<clipPath id="terminal-3519450412-line-5">
<rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-6">
+<clipPath id="terminal-3519450412-line-6">
<rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-7">
+<clipPath id="terminal-3519450412-line-7">
<rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-8">
+<clipPath id="terminal-3519450412-line-8">
<rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-9">
+<clipPath id="terminal-3519450412-line-9">
<rect x="0" y="221.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-10">
+<clipPath id="terminal-3519450412-line-10">
<rect x="0" y="245.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-11">
+<clipPath id="terminal-3519450412-line-11">
<rect x="0" y="269.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-12">
+<clipPath id="terminal-3519450412-line-12">
<rect x="0" y="294.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-13">
+<clipPath id="terminal-3519450412-line-13">
<rect x="0" y="318.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-14">
+<clipPath id="terminal-3519450412-line-14">
<rect x="0" y="343.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-15">
+<clipPath id="terminal-3519450412-line-15">
<rect x="0" y="367.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-16">
+<clipPath id="terminal-3519450412-line-16">
<rect x="0" y="391.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-17">
+<clipPath id="terminal-3519450412-line-17">
<rect x="0" y="416.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-18">
+<clipPath id="terminal-3519450412-line-18">
<rect x="0" y="440.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-19">
+<clipPath id="terminal-3519450412-line-19">
<rect x="0" y="465.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-20">
+<clipPath id="terminal-3519450412-line-20">
<rect x="0" y="489.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-21">
+<clipPath id="terminal-3519450412-line-21">
<rect x="0" y="513.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-22">
+<clipPath id="terminal-3519450412-line-22">
<rect x="0" y="538.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-23">
+<clipPath id="terminal-3519450412-line-23">
<rect x="0" y="562.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-24">
+<clipPath id="terminal-3519450412-line-24">
<rect x="0" y="587.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2670787131-line-25">
+<clipPath id="terminal-3519450412-line-25">
<rect x="0" y="611.5" width="915" height="24.65"/>
</clipPath>
+<clipPath id="terminal-3519450412-line-26">
+ <rect x="0" y="635.9" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3519450412-line-27">
+ <rect x="0" y="660.3" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3519450412-line-28">
+ <rect x="0" y="684.7" width="915" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="706.8" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="780" rx="8"/>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
- <g transform="translate(9, 41)"
clip-path="url(#terminal-2670787131-clip-terminal)">
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-3519450412-clip-terminal)">
- <g class="terminal-2670787131-matrix">
- <text class="terminal-2670787131-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-2670787131-line-0)">Usage:</text><text
class="terminal-2670787131-r3" x="85.4" y="20" textLength="122"
clip-path="url(#terminal-2670787131-line-0)">airflowctl</text><text
class="terminal-2670787131-r2" x="207.4" y="20" textLength="24.4"
clip-path="url(#terminal-2670787131-line-0)"> [</text><text
class="terminal-2670787131-r4" x="231.8" y="20" textLength="24.4"
clip-path="url(#terminal-26 [...]
-</text><text class="terminal-2670787131-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-2670787131-line-1)">
-</text><text class="terminal-2670787131-r1" x="0" y="68.8" textLength="256.2"
clip-path="url(#terminal-2670787131-line-2)">Positional Arguments:</text><text
class="terminal-2670787131-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-2670787131-line-2)">
-</text><text class="terminal-2670787131-r4" x="24.4" y="93.2"
textLength="195.2"
clip-path="url(#terminal-2670787131-line-3)">GROUP_OR_COMMAND</text><text
class="terminal-2670787131-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-2670787131-line-3)">
-</text><text class="terminal-2670787131-r2" x="915" y="117.6"
textLength="12.2" clip-path="url(#terminal-2670787131-line-4)">
-</text><text class="terminal-2670787131-r4" x="0" y="142" textLength="122"
clip-path="url(#terminal-2670787131-line-5)">    Groups</text><text
class="terminal-2670787131-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-2670787131-line-5)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="166.4"
textLength="73.2"
clip-path="url(#terminal-2670787131-line-6)">assets</text><text
class="terminal-2670787131-r2" x="244" y="166.4" textLength="305"
clip-path="url(#terminal-2670787131-line-6)">Perform Assets operations</text><text
class="terminal-2670787131-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-2670787131-line-6)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="190.8"
textLength="48.8" clip-path="url(#terminal-2670787131-line-7)">auth</text><text
class="terminal-2670787131-r2" x="244" y="190.8" textLength="646.6"
clip-path="url(#terminal-2670787131-line-7)">Manage authentication for CLI. Either pass token from</text><text
class="terminal-2670787131-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-2670787131-line-7)">
-</text><text class="terminal-2670787131-r2" x="244" y="215.2"
textLength="622.2"
clip-path="url(#terminal-2670787131-line-8)">environment variable/parameter or pass username and</text><text
class="terminal-2670787131-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-2670787131-line-8)">
-</text><text class="terminal-2670787131-r2" x="244" y="239.6"
textLength="109.8"
clip-path="url(#terminal-2670787131-line-9)">password.</text><text
class="terminal-2670787131-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-2670787131-line-9)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="264" textLength="97.6"
clip-path="url(#terminal-2670787131-line-10)">backfill</text><text
class="terminal-2670787131-r2" x="244" y="264" textLength="329.4"
clip-path="url(#terminal-2670787131-line-10)">Perform Backfill operations</text><text
class="terminal-2670787131-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-2670787131-line-10)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="288.4"
textLength="73.2"
clip-path="url(#terminal-2670787131-line-11)">config</text><text
class="terminal-2670787131-r2" x="244" y="288.4" textLength="305"
clip-path="url(#terminal-2670787131-line-11)">Perform Config operations</text><text
class="terminal-2670787131-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-2670787131-line-11)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="312.8"
textLength="134.2"
clip-path="url(#terminal-2670787131-line-12)">connections</text><text
class="terminal-2670787131-r2" x="244" y="312.8" textLength="366"
clip-path="url(#terminal-2670787131-line-12)">Perform Connections operations</text><text
class="terminal-2670787131-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-2670787131-line-12)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="337.2"
textLength="73.2"
clip-path="url(#terminal-2670787131-line-13)">dagrun</text><text
class="terminal-2670787131-r2" x="244" y="337.2" textLength="305"
clip-path="url(#terminal-2670787131-line-13)">Perform DagRun operations</text><text
class="terminal-2670787131-r2" x="915" y="337.2" textLength="12.2"
clip-path="url(#terminal-2670787131-line-13)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="361.6"
textLength="48.8"
clip-path="url(#terminal-2670787131-line-14)">dags</text><text
class="terminal-2670787131-r2" x="244" y="361.6" textLength="280.6"
clip-path="url(#terminal-2670787131-line-14)">Perform Dags operations</text><text
class="terminal-2670787131-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-2670787131-line-14)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="386" textLength="48.8"
clip-path="url(#terminal-2670787131-line-15)">jobs</text><text
class="terminal-2670787131-r2" x="244" y="386" textLength="280.6"
clip-path="url(#terminal-2670787131-line-15)">Perform Jobs operations</text><text
class="terminal-2670787131-r2" x="915" y="386" textLength="12.2"
clip-path="url(#terminal-2670787131-line-15)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="410.4"
textLength="85.4"
clip-path="url(#terminal-2670787131-line-16)">plugins</text><text
class="terminal-2670787131-r2" x="244" y="410.4" textLength="317.2"
clip-path="url(#terminal-2670787131-line-16)">Perform Plugins operations</text><text
class="terminal-2670787131-r2" x="915" y="410.4" textLength="12.2"
clip-path="url(#terminal-2670787131-line-16)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="434.8" textLength="61"
clip-path="url(#terminal-2670787131-line-17)">pools</text><text
class="terminal-2670787131-r2" x="244" y="434.8" textLength="292.8"
clip-path="url(#terminal-2670787131-line-17)">Perform Pools operations</text><text
class="terminal-2670787131-r2" x="915" y="434.8" textLength="12.2"
clip-path="url(#terminal-2670787131-line-17)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="459.2"
textLength="109.8"
clip-path="url(#terminal-2670787131-line-18)">providers</text><text
class="terminal-2670787131-r2" x="244" y="459.2" textLength="341.6"
clip-path="url(#terminal-2670787131-line-18)">Perform Providers operations</text><text
class="terminal-2670787131-r2" x="915" y="459.2" textLength="12.2"
clip-path="url(#terminal-2670787131-line-18)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="483.6"
textLength="109.8"
clip-path="url(#terminal-2670787131-line-19)">variables</text><text
class="terminal-2670787131-r2" x="244" y="483.6" textLength="341.6"
clip-path="url(#terminal-2670787131-line-19)">Perform Variables operations</text><text
class="terminal-2670787131-r2" x="915" y="483.6" textLength="12.2"
clip-path="url(#terminal-2670787131-line-19)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="508" textLength="48.8"
clip-path="url(#terminal-2670787131-line-20)">xcom</text><text
class="terminal-2670787131-r2" x="244" y="508" textLength="280.6"
clip-path="url(#terminal-2670787131-line-20)">Perform XCom operations</text><text
class="terminal-2670787131-r2" x="915" y="508" textLength="12.2"
clip-path="url(#terminal-2670787131-line-20)">
-</text><text class="terminal-2670787131-r2" x="915" y="532.4"
textLength="12.2" clip-path="url(#terminal-2670787131-line-21)">
-</text><text class="terminal-2670787131-r4" x="0" y="556.8" textLength="158.6"
clip-path="url(#terminal-2670787131-line-22)">    Commands:</text><text
class="terminal-2670787131-r2" x="915" y="556.8" textLength="12.2"
clip-path="url(#terminal-2670787131-line-22)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="581.2"
textLength="85.4"
clip-path="url(#terminal-2670787131-line-23)">version</text><text
class="terminal-2670787131-r2" x="244" y="581.2" textLength="292.8"
clip-path="url(#terminal-2670787131-line-23)">Show version information</text><text
class="terminal-2670787131-r2" x="915" y="581.2" textLength="12.2"
clip-path="url(#terminal-2670787131-line-23)">
-</text><text class="terminal-2670787131-r2" x="915" y="605.6"
textLength="12.2" clip-path="url(#terminal-2670787131-line-24)">
-</text><text class="terminal-2670787131-r1" x="0" y="630" textLength="97.6"
clip-path="url(#terminal-2670787131-line-25)">Options:</text><text
class="terminal-2670787131-r2" x="915" y="630" textLength="12.2"
clip-path="url(#terminal-2670787131-line-25)">
-</text><text class="terminal-2670787131-r4" x="24.4" y="654.4"
textLength="24.4" clip-path="url(#terminal-2670787131-line-26)">-h</text><text
class="terminal-2670787131-r2" x="48.8" y="654.4" textLength="24.4"
clip-path="url(#terminal-2670787131-line-26)">, </text><text
class="terminal-2670787131-r4" x="73.2" y="654.4" textLength="73.2"
clip-path="url(#terminal-2670787131-line-26)">--help</text><text
class="terminal-2670787131-r2" x="244" y="654.4" textLength="378.2"
clip-path="url( [...]
+ <g class="terminal-3519450412-matrix">
+ <text class="terminal-3519450412-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-3519450412-line-0)">Usage:</text><text
class="terminal-3519450412-r3" x="85.4" y="20" textLength="122"
clip-path="url(#terminal-3519450412-line-0)">airflowctl</text><text
class="terminal-3519450412-r2" x="207.4" y="20" textLength="24.4"
clip-path="url(#terminal-3519450412-line-0)"> [</text><text
class="terminal-3519450412-r4" x="231.8" y="20" textLength="24.4"
clip-path="url(#terminal-35 [...]
+</text><text class="terminal-3519450412-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-3519450412-line-1)">
+</text><text class="terminal-3519450412-r1" x="0" y="68.8" textLength="256.2"
clip-path="url(#terminal-3519450412-line-2)">Positional Arguments:</text><text
class="terminal-3519450412-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-3519450412-line-2)">
+</text><text class="terminal-3519450412-r4" x="24.4" y="93.2"
textLength="195.2"
clip-path="url(#terminal-3519450412-line-3)">GROUP_OR_COMMAND</text><text
class="terminal-3519450412-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-3519450412-line-3)">
+</text><text class="terminal-3519450412-r2" x="915" y="117.6"
textLength="12.2" clip-path="url(#terminal-3519450412-line-4)">
+</text><text class="terminal-3519450412-r4" x="0" y="142" textLength="122"
clip-path="url(#terminal-3519450412-line-5)">    Groups</text><text
class="terminal-3519450412-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-3519450412-line-5)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="166.4"
textLength="73.2"
clip-path="url(#terminal-3519450412-line-6)">assets</text><text
class="terminal-3519450412-r2" x="244" y="166.4" textLength="305"
clip-path="url(#terminal-3519450412-line-6)">Perform Assets operations</text><text
class="terminal-3519450412-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-3519450412-line-6)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="190.8"
textLength="48.8" clip-path="url(#terminal-3519450412-line-7)">auth</text><text
class="terminal-3519450412-r2" x="244" y="190.8" textLength="646.6"
clip-path="url(#terminal-3519450412-line-7)">Manage authentication for CLI. Either pass token from</text><text
class="terminal-3519450412-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-3519450412-line-7)">
+</text><text class="terminal-3519450412-r2" x="244" y="215.2"
textLength="622.2"
clip-path="url(#terminal-3519450412-line-8)">environment variable/parameter or pass username and</text><text
class="terminal-3519450412-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-3519450412-line-8)">
+</text><text class="terminal-3519450412-r2" x="244" y="239.6"
textLength="109.8"
clip-path="url(#terminal-3519450412-line-9)">password.</text><text
class="terminal-3519450412-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-3519450412-line-9)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="264" textLength="97.6"
clip-path="url(#terminal-3519450412-line-10)">backfill</text><text
class="terminal-3519450412-r2" x="244" y="264" textLength="329.4"
clip-path="url(#terminal-3519450412-line-10)">Perform Backfill operations</text><text
class="terminal-3519450412-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-3519450412-line-10)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="288.4"
textLength="73.2"
clip-path="url(#terminal-3519450412-line-11)">config</text><text
class="terminal-3519450412-r2" x="244" y="288.4" textLength="305"
clip-path="url(#terminal-3519450412-line-11)">Perform Config operations</text><text
class="terminal-3519450412-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-3519450412-line-11)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="312.8"
textLength="134.2"
clip-path="url(#terminal-3519450412-line-12)">connections</text><text
class="terminal-3519450412-r2" x="244" y="312.8" textLength="366"
clip-path="url(#terminal-3519450412-line-12)">Perform Connections operations</text><text
class="terminal-3519450412-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-3519450412-line-12)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="337.2"
textLength="73.2"
clip-path="url(#terminal-3519450412-line-13)">dagrun</text><text
class="terminal-3519450412-r2" x="244" y="337.2" textLength="305"
clip-path="url(#terminal-3519450412-line-13)">Perform DagRun operations</text><text
class="terminal-3519450412-r2" x="915" y="337.2" textLength="12.2"
clip-path="url(#terminal-3519450412-line-13)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="361.6"
textLength="48.8"
clip-path="url(#terminal-3519450412-line-14)">dags</text><text
class="terminal-3519450412-r2" x="244" y="361.6" textLength="280.6"
clip-path="url(#terminal-3519450412-line-14)">Perform Dags operations</text><text
class="terminal-3519450412-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-3519450412-line-14)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="386" textLength="48.8"
clip-path="url(#terminal-3519450412-line-15)">jobs</text><text
class="terminal-3519450412-r2" x="244" y="386" textLength="280.6"
clip-path="url(#terminal-3519450412-line-15)">Perform Jobs operations</text><text
class="terminal-3519450412-r2" x="915" y="386" textLength="12.2"
clip-path="url(#terminal-3519450412-line-15)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="410.4"
textLength="85.4"
clip-path="url(#terminal-3519450412-line-16)">plugins</text><text
class="terminal-3519450412-r2" x="244" y="410.4" textLength="317.2"
clip-path="url(#terminal-3519450412-line-16)">Perform Plugins operations</text><text
class="terminal-3519450412-r2" x="915" y="410.4" textLength="12.2"
clip-path="url(#terminal-3519450412-line-16)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="434.8" textLength="61"
clip-path="url(#terminal-3519450412-line-17)">pools</text><text
class="terminal-3519450412-r2" x="244" y="434.8" textLength="292.8"
clip-path="url(#terminal-3519450412-line-17)">Perform Pools operations</text><text
class="terminal-3519450412-r2" x="915" y="434.8" textLength="12.2"
clip-path="url(#terminal-3519450412-line-17)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="459.2"
textLength="109.8"
clip-path="url(#terminal-3519450412-line-18)">providers</text><text
class="terminal-3519450412-r2" x="244" y="459.2" textLength="341.6"
clip-path="url(#terminal-3519450412-line-18)">Perform Providers operations</text><text
class="terminal-3519450412-r2" x="915" y="459.2" textLength="12.2"
clip-path="url(#terminal-3519450412-line-18)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="483.6"
textLength="158.6"
clip-path="url(#terminal-3519450412-line-19)">taskinstances</text><text
class="terminal-3519450412-r2" x="915" y="483.6" textLength="12.2"
clip-path="url(#terminal-3519450412-line-19)">
+</text><text class="terminal-3519450412-r2" x="244" y="508" textLength="390.4"
clip-path="url(#terminal-3519450412-line-20)">Perform TaskInstances operations</text><text
class="terminal-3519450412-r2" x="915" y="508" textLength="12.2"
clip-path="url(#terminal-3519450412-line-20)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="532.4" textLength="61"
clip-path="url(#terminal-3519450412-line-21)">tasks</text><text
class="terminal-3519450412-r2" x="244" y="532.4" textLength="244"
clip-path="url(#terminal-3519450412-line-21)">Manage Airflow tasks</text><text
class="terminal-3519450412-r2" x="915" y="532.4" textLength="12.2"
clip-path="url(#terminal-3519450412-line-21)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="556.8"
textLength="109.8"
clip-path="url(#terminal-3519450412-line-22)">variables</text><text
class="terminal-3519450412-r2" x="244" y="556.8" textLength="341.6"
clip-path="url(#terminal-3519450412-line-22)">Perform Variables operations</text><text
class="terminal-3519450412-r2" x="915" y="556.8" textLength="12.2"
clip-path="url(#terminal-3519450412-line-22)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="581.2"
textLength="48.8"
clip-path="url(#terminal-3519450412-line-23)">xcom</text><text
class="terminal-3519450412-r2" x="244" y="581.2" textLength="280.6"
clip-path="url(#terminal-3519450412-line-23)">Perform XCom operations</text><text
class="terminal-3519450412-r2" x="915" y="581.2" textLength="12.2"
clip-path="url(#terminal-3519450412-line-23)">
+</text><text class="terminal-3519450412-r2" x="915" y="605.6"
textLength="12.2" clip-path="url(#terminal-3519450412-line-24)">
+</text><text class="terminal-3519450412-r4" x="0" y="630" textLength="158.6"
clip-path="url(#terminal-3519450412-line-25)">    Commands:</text><text
class="terminal-3519450412-r2" x="915" y="630" textLength="12.2"
clip-path="url(#terminal-3519450412-line-25)">
+</text><text class="terminal-3519450412-r4" x="73.2" y="654.4"
textLength="85.4"
clip-path="url(#terminal-3519450412-line-26)">version</text><text
class="terminal-3519450412-r2" x="244" y="654.4" textLength="292.8"
clip-path="url(#terminal-3519450412-line-26)">Show version information</text><text
class="terminal-3519450412-r2" x="915" y="654.4" textLength="12.2"
clip-path="url(#terminal-3519450412-line-26)">
+</text><text class="terminal-3519450412-r2" x="915" y="678.8"
textLength="12.2" clip-path="url(#terminal-3519450412-line-27)">
+</text><text class="terminal-3519450412-r1" x="0" y="703.2" textLength="97.6"
clip-path="url(#terminal-3519450412-line-28)">Options:</text><text
class="terminal-3519450412-r2" x="915" y="703.2" textLength="12.2"
clip-path="url(#terminal-3519450412-line-28)">
+</text><text class="terminal-3519450412-r4" x="24.4" y="727.6"
textLength="24.4" clip-path="url(#terminal-3519450412-line-29)">-h</text><text
class="terminal-3519450412-r2" x="48.8" y="727.6" textLength="24.4"
clip-path="url(#terminal-3519450412-line-29)">, </text><text
class="terminal-3519450412-r4" x="73.2" y="727.6" textLength="73.2"
clip-path="url(#terminal-3519450412-line-29)">--help</text><text
class="terminal-3519450412-r2" x="244" y="727.6" textLength="378.2"
clip-path="url( [...]
</text>
</g>
</g>
diff --git a/airflow-ctl/docs/images/output_taskinstances.svg
b/airflow-ctl/docs/images/output_taskinstances.svg
new file mode 100644
index 00000000000..9d9e86568e1
--- /dev/null
+++ b/airflow-ctl/docs/images/output_taskinstances.svg
@@ -0,0 +1,97 @@
+<svg class="rich-terminal" viewBox="0 0 933 294.0"
xmlns="http://www.w3.org/2000/svg">
+ <!-- Generated with Rich https://www.textualize.io -->
+ <style>
+
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Regular"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2")
format("woff2"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff")
format("woff");
+ font-style: normal;
+ font-weight: 400;
+ }
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Bold"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2")
format("woff2"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff")
format("woff");
+ font-style: bold;
+ font-weight: 700;
+ }
+
+ .terminal-1659955685-matrix {
+ font-family: Fira Code, monospace;
+ font-size: 20px;
+ line-height: 24.4px;
+ font-variant-east-asian: full-width;
+ }
+
+ .terminal-1659955685-title {
+ font-size: 18px;
+ font-weight: bold;
+ font-family: arial;
+ }
+
+ .terminal-1659955685-r1 { fill: #ff8700 }
+.terminal-1659955685-r2 { fill: #c5c8c6 }
+.terminal-1659955685-r3 { fill: #808080 }
+.terminal-1659955685-r4 { fill: #68a0b3 }
+ </style>
+
+ <defs>
+ <clipPath id="terminal-1659955685-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="243.0" />
+ </clipPath>
+ <clipPath id="terminal-1659955685-line-0">
+ <rect x="0" y="1.5" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-1">
+ <rect x="0" y="25.9" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-2">
+ <rect x="0" y="50.3" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-3">
+ <rect x="0" y="74.7" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-4">
+ <rect x="0" y="99.1" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-5">
+ <rect x="0" y="123.5" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-6">
+ <rect x="0" y="147.9" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-7">
+ <rect x="0" y="172.3" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-1659955685-line-8">
+ <rect x="0" y="196.7" width="915" height="24.65"/>
+ </clipPath>
+ </defs>
+
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="292" rx="8"/>
+ <g transform="translate(26,22)">
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
+ </g>
+
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-1659955685-clip-terminal)">
+
+ <g class="terminal-1659955685-matrix">
+ <text class="terminal-1659955685-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-1659955685-line-0)">Usage:</text><text
class="terminal-1659955685-r3" x="85.4" y="20" textLength="292.8"
clip-path="url(#terminal-1659955685-line-0)">airflowctl taskinstances</text><text
class="terminal-1659955685-r2" x="378.2" y="20" textLength="24.4"
clip-path="url(#terminal-1659955685-line-0)"> [</text><text
class="terminal-1659955685-r4" x="402.6" y="20" textLength="24.4" clip-p [...]
+</text><text class="terminal-1659955685-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-1659955685-line-1)">
+</text><text class="terminal-1659955685-r2" x="0" y="68.8" textLength="390.4"
clip-path="url(#terminal-1659955685-line-2)">Perform TaskInstances operations</text><text
class="terminal-1659955685-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-1659955685-line-2)">
+</text><text class="terminal-1659955685-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-1659955685-line-3)">
+</text><text class="terminal-1659955685-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-1659955685-line-4)">Positional Arguments:</text><text
class="terminal-1659955685-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-1659955685-line-4)">
+</text><text class="terminal-1659955685-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-1659955685-line-5)">COMMAND</text><text
class="terminal-1659955685-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-1659955685-line-5)">
+</text><text class="terminal-1659955685-r4" x="48.8" y="166.4"
textLength="48.8" clip-path="url(#terminal-1659955685-line-6)">list</text><text
class="terminal-1659955685-r2" x="170.8" y="166.4" textLength="524.6"
clip-path="url(#terminal-1659955685-line-6)">List all task instances for a given Dag run</text><text
class="terminal-1659955685-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-1659955685-line-6)">
+</text><text class="terminal-1659955685-r2" x="915" y="190.8"
textLength="12.2" clip-path="url(#terminal-1659955685-line-7)">
+</text><text class="terminal-1659955685-r1" x="0" y="215.2" textLength="97.6"
clip-path="url(#terminal-1659955685-line-8)">Options:</text><text
class="terminal-1659955685-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-1659955685-line-8)">
+</text><text class="terminal-1659955685-r4" x="24.4" y="239.6"
textLength="24.4" clip-path="url(#terminal-1659955685-line-9)">-h</text><text
class="terminal-1659955685-r2" x="48.8" y="239.6" textLength="24.4"
clip-path="url(#terminal-1659955685-line-9)">, </text><text
class="terminal-1659955685-r4" x="73.2" y="239.6" textLength="73.2"
clip-path="url(#terminal-1659955685-line-9)">--help</text><text
class="terminal-1659955685-r2" x="170.8" y="239.6" textLength="378.2"
clip-path="url(# [...]
+</text>
+ </g>
+ </g>
+</svg>
diff --git a/airflow-ctl/docs/images/output_tasks.svg
b/airflow-ctl/docs/images/output_tasks.svg
new file mode 100644
index 00000000000..d9349c11752
--- /dev/null
+++ b/airflow-ctl/docs/images/output_tasks.svg
@@ -0,0 +1,101 @@
+<svg class="rich-terminal" viewBox="0 0 933 318.4"
xmlns="http://www.w3.org/2000/svg">
+ <!-- Generated with Rich https://www.textualize.io -->
+ <style>
+
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Regular"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2")
format("woff2"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff")
format("woff");
+ font-style: normal;
+ font-weight: 400;
+ }
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Bold"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2")
format("woff2"),
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff")
format("woff");
+ font-style: bold;
+ font-weight: 700;
+ }
+
+ .terminal-3141987423-matrix {
+ font-family: Fira Code, monospace;
+ font-size: 20px;
+ line-height: 24.4px;
+ font-variant-east-asian: full-width;
+ }
+
+ .terminal-3141987423-title {
+ font-size: 18px;
+ font-weight: bold;
+ font-family: arial;
+ }
+
+ .terminal-3141987423-r1 { fill: #ff8700 }
+.terminal-3141987423-r2 { fill: #c5c8c6 }
+.terminal-3141987423-r3 { fill: #808080 }
+.terminal-3141987423-r4 { fill: #68a0b3 }
+ </style>
+
+ <defs>
+ <clipPath id="terminal-3141987423-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="267.4" />
+ </clipPath>
+ <clipPath id="terminal-3141987423-line-0">
+ <rect x="0" y="1.5" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-1">
+ <rect x="0" y="25.9" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-2">
+ <rect x="0" y="50.3" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-3">
+ <rect x="0" y="74.7" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-4">
+ <rect x="0" y="99.1" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-5">
+ <rect x="0" y="123.5" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-6">
+ <rect x="0" y="147.9" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-7">
+ <rect x="0" y="172.3" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-8">
+ <rect x="0" y="196.7" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-3141987423-line-9">
+ <rect x="0" y="221.1" width="915" height="24.65"/>
+ </clipPath>
+ </defs>
+
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="316.4" rx="8"/>
+ <g transform="translate(26,22)">
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
+ </g>
+
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-3141987423-clip-terminal)">
+
+ <g class="terminal-3141987423-matrix">
+ <text class="terminal-3141987423-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-3141987423-line-0)">Usage:</text><text
class="terminal-3141987423-r3" x="85.4" y="20" textLength="195.2"
clip-path="url(#terminal-3141987423-line-0)">airflowctl tasks</text><text
class="terminal-3141987423-r2" x="280.6" y="20" textLength="24.4"
clip-path="url(#terminal-3141987423-line-0)"> [</text><text
class="terminal-3141987423-r4" x="305" y="20" textLength="24.4"
clip-path="url(# [...]
+</text><text class="terminal-3141987423-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-3141987423-line-1)">
+</text><text class="terminal-3141987423-r2" x="0" y="68.8" textLength="244"
clip-path="url(#terminal-3141987423-line-2)">Manage Airflow tasks</text><text
class="terminal-3141987423-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-3141987423-line-2)">
+</text><text class="terminal-3141987423-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-3141987423-line-3)">
+</text><text class="terminal-3141987423-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-3141987423-line-4)">Positional Arguments:</text><text
class="terminal-3141987423-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-3141987423-line-4)">
+</text><text class="terminal-3141987423-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-3141987423-line-5)">COMMAND</text><text
class="terminal-3141987423-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-3141987423-line-5)">
+</text><text class="terminal-3141987423-r4" x="48.8" y="166.4"
textLength="219.6"
clip-path="url(#terminal-3141987423-line-6)">states-for-dag-run</text><text
class="terminal-3141987423-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-3141987423-line-6)">
+</text><text class="terminal-3141987423-r2" x="268.4" y="190.8"
textLength="597.8"
clip-path="url(#terminal-3141987423-line-7)">Get the status of all task instances in a Dag run</text><text
class="terminal-3141987423-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-3141987423-line-7)">
+</text><text class="terminal-3141987423-r2" x="915" y="215.2"
textLength="12.2" clip-path="url(#terminal-3141987423-line-8)">
+</text><text class="terminal-3141987423-r1" x="0" y="239.6" textLength="97.6"
clip-path="url(#terminal-3141987423-line-9)">Options:</text><text
class="terminal-3141987423-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-3141987423-line-9)">
+</text><text class="terminal-3141987423-r4" x="24.4" y="264" textLength="24.4"
clip-path="url(#terminal-3141987423-line-10)">-h</text><text
class="terminal-3141987423-r2" x="48.8" y="264" textLength="24.4"
clip-path="url(#terminal-3141987423-line-10)">, </text><text
class="terminal-3141987423-r4" x="73.2" y="264" textLength="73.2"
clip-path="url(#terminal-3141987423-line-10)">--help</text><text
class="terminal-3141987423-r2" x="268.4" y="264" textLength="378.2"
clip-path="url(#termi [...]
+</text>
+ </g>
+ </g>
+</svg>
diff --git a/airflow-ctl/docs/spelling_wordlist.txt
b/airflow-ctl/docs/spelling_wordlist.txt
new file mode 100644
index 00000000000..04c8494c855
--- /dev/null
+++ b/airflow-ctl/docs/spelling_wordlist.txt
@@ -0,0 +1 @@
+taskinstances
diff --git a/airflow-ctl/src/airflowctl/api/client.py
b/airflow-ctl/src/airflowctl/api/client.py
index de2cec4ea5f..7baf87684dd 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -57,6 +57,7 @@ from airflowctl.api.operations import (
PoolsOperations,
ProvidersOperations,
ServerResponseError,
+ TaskInstancesOperations,
VariablesOperations,
VersionOperations,
XComOperations,
@@ -450,6 +451,12 @@ class Client(httpx.Client):
"""Operations related to providers."""
return ProvidersOperations(self)
+ @lru_cache() # type: ignore[prop-decorator]
+ @property
+ def task_instances(self):
+ """Operations related to task instances."""
+ return TaskInstancesOperations(self)
+
@lru_cache() # type: ignore[prop-decorator]
@property
def variables(self):
diff --git a/airflow-ctl/src/airflowctl/api/operations.py
b/airflow-ctl/src/airflowctl/api/operations.py
index c682608e85f..44bfa30125f 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -68,6 +68,7 @@ from airflowctl.api.datamodels.generated import (
ProviderCollectionResponse,
QueuedEventCollectionResponse,
QueuedEventResponse,
+ TaskInstanceCollectionResponse,
TriggerDAGRunPostBody,
VariableBody,
VariableCollectionResponse,
@@ -634,6 +635,8 @@ class DagRunOperations(BaseOperations):
logical_date_gte: datetime.datetime | None = None,
logical_date_lte: datetime.datetime | None = None,
order_by: str | None = None,
+ *,
+ suppress_error_log: bool = False,
) -> DAGRunCollectionResponse | ServerResponseError:
"""
List dag runs (at most `limit` results).
@@ -647,6 +650,7 @@ class DagRunOperations(BaseOperations):
logical_date_gte: Filter dag runs with a logical date greater than
or equal to this value.
logical_date_lte: Filter dag runs with a logical date less than or
equal to this value.
order_by: Order the results by the specified field.
+ suppress_error_log: Skip client-side error logging, for callers
handling the error themselves.
"""
# Use "~" for all DAGs if dag_id is not specified
if not dag_id:
@@ -663,7 +667,11 @@ class DagRunOperations(BaseOperations):
)
try:
- self.response = self.client.get(f"/dags/{dag_id}/dagRuns",
params=params)
+ self.response = self.client.get(
+ f"/dags/{dag_id}/dagRuns",
+ params=params,
+ extensions={"airflowctl_suppress_error_log":
suppress_error_log},
+ )
return
DAGRunCollectionResponse.model_validate_json(self.response.content)
except ServerResponseError as e:
raise e
@@ -768,6 +776,17 @@ class ProvidersOperations(BaseOperations):
return super().execute_list(path="providers",
data_model=ProviderCollectionResponse)
+class TaskInstancesOperations(BaseOperations):
+ """Task instance operations."""
+
+ def list(self, dag_id: str, dag_run_id: str) ->
TaskInstanceCollectionResponse | ServerResponseError:
+ """List task instances for a Dag run."""
+ return super().execute_list(
+ path=f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances",
+ data_model=TaskInstanceCollectionResponse,
+ )
+
+
class VariablesOperations(BaseOperations):
"""Variable operations."""
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 40ee7a1c67a..47270d9c29c 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -279,7 +279,7 @@ ARG_AUTH_PASSWORD = Arg(
ARG_DAG_ID = Arg(
flags=("dag_id",),
type=str,
- help="The Dag ID of the Dag to pause or unpause",
+ help="The Dag ID",
)
ARG_LOGICAL_DATE_OR_RUN_ID = Arg(
flags=("logical_date_or_run_id",),
@@ -287,6 +287,19 @@ ARG_LOGICAL_DATE_OR_RUN_ID = Arg(
help="The logical date with a timezone offset or run ID of the Dag run",
)
+# Task Commands Args
+ARG_RUN_ID = Arg(
+ flags=("run_id",),
+ type=str,
+ nargs="?",
+ help="The run ID of the Dag run (pass this or --logical-date, not both)",
+)
+ARG_LOGICAL_DATE = Arg(
+ flags=("--logical-date",),
+ type=str,
+ help="The logical date of the Dag run with a timezone offset (pass this or
run_id, not both)",
+)
+
ARG_ACTION_ON_EXISTING_KEY = Arg(
flags=("-a", "--action-on-existing-key"),
type=str,
@@ -1034,6 +1047,24 @@ POOL_COMMANDS = (
),
)
+TASK_COMMANDS = (
+ ActionCommand(
+ name="states-for-dag-run",
+ help="Get the status of all task instances in a Dag run",
+ description=(
+ "Get the status of all task instances in a Dag run. "
+ "Select the run with either run_id or --logical-date (pass exactly
one)."
+ ),
+
func=lazy_load_command("airflowctl.ctl.commands.task_command.states_for_dag_run"),
+ args=(
+ ARG_DAG_ID,
+ ARG_RUN_ID,
+ ARG_LOGICAL_DATE,
+ ARG_OUTPUT,
+ ),
+ ),
+)
+
VARIABLE_COMMANDS = (
ActionCommand(
name="import",
@@ -1070,6 +1101,11 @@ core_commands: list[CLICommand] = [
help="Manage Airflow pools",
subcommands=POOL_COMMANDS,
),
+ GroupCommand(
+ name="tasks",
+ help="Manage Airflow tasks",
+ subcommands=TASK_COMMANDS,
+ ),
ActionCommand(
name="version",
help="Show version information",
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
b/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
new file mode 100644
index 00000000000..5881a1bd556
--- /dev/null
+++ b/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
@@ -0,0 +1,99 @@
+# 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
+import sys
+from typing import TYPE_CHECKING
+
+import rich
+
+from airflowctl.api.client import NEW_API_CLIENT, ClientKind,
ServerResponseError, provide_api_client
+from airflowctl.ctl.console_formatting import AirflowConsole
+
+if TYPE_CHECKING:
+ from airflowctl.api.datamodels.generated import TaskInstanceResponse
+
+
+def _find_run_id_by_logical_date(api_client, dag_id: str, value: str) -> str:
+ """Find the run ID of the Dag run with an exact logical date match."""
+ try:
+ logical_date = datetime.datetime.fromisoformat(value.replace("Z",
"+00:00"))
+ except ValueError:
+ rich.print(f"[red]Invalid --logical-date: {value!r}[/red]")
+ sys.exit(1)
+ if logical_date.tzinfo is None:
+ rich.print("[red]--logical-date must include a timezone offset[/red]")
+ sys.exit(1)
+
+ dag_runs = []
+ try:
+ dag_runs = api_client.dag_runs.list(
+ dag_id=dag_id,
+ logical_date_gte=logical_date,
+ logical_date_lte=logical_date,
+ order_by="-id",
+ limit=1,
+ suppress_error_log=True,
+ ).dag_runs
+ except ServerResponseError as e:
+ if e.response.status_code != 404:
+ raise
+ if not dag_runs:
+ rich.print(f"[red]Dag run for {dag_id} with logical date {value!r} not
found[/red]")
+ sys.exit(1)
+ return dag_runs[0].dag_run_id
+
+
+def _format_task_instance(ti: TaskInstanceResponse, has_mapped_instances:
bool) -> dict[str, str]:
+ data = {
+ "dag_id": ti.dag_id,
+ "logical_date": ti.logical_date.isoformat() if ti.logical_date else "",
+ "task_id": ti.task_id,
+ "state": ti.state.value if ti.state else "",
+ "start_date": ti.start_date.isoformat() if ti.start_date else "",
+ "end_date": ti.end_date.isoformat() if ti.end_date else "",
+ }
+ if has_mapped_instances:
+ data["map_index"] = str(ti.map_index) if ti.map_index >= 0 else ""
+ return data
+
+
+@provide_api_client(kind=ClientKind.CLI)
+def states_for_dag_run(args, api_client=NEW_API_CLIENT) -> None:
+ """Get the status of all task instances in a Dag run."""
+ if (args.run_id is None) == (args.logical_date is None):
+ rich.print("[red]Provide either run_id or --logical-date, but not
both[/red]")
+ sys.exit(1)
+
+ run_id = args.run_id or _find_run_id_by_logical_date(api_client,
args.dag_id, args.logical_date)
+
+ try:
+ task_instances = api_client.task_instances.list(dag_id=args.dag_id,
dag_run_id=run_id).task_instances
+ except ServerResponseError as e:
+ if e.response.status_code == 404:
+ rich.print(f"[red]Dag run {run_id!r} of Dag {args.dag_id!r} not
found[/red]")
+ sys.exit(1)
+ raise
+
+ has_mapped_instances = any(ti.map_index >= 0 for ti in task_instances)
+
+ AirflowConsole().print_as(
+ data=[_format_task_instance(ti, has_mapped_instances) for ti in
task_instances],
+ output=args.output,
+ )
diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
index d0ebdcfeaeb..1c9b23d3f94 100644
--- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
+++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
@@ -85,6 +85,9 @@ pools:
providers:
list: "List all installed Airflow providers"
+taskinstances:
+ list: "List all task instances for a given Dag run"
+
variables:
get: "Retrieve a variable by its key"
list: "List all variables"
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 384e6ecae3d..f988a6f329a 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -91,6 +91,9 @@ from airflowctl.api.datamodels.generated import (
QueuedEventCollectionResponse,
QueuedEventResponse,
ReprocessBehavior,
+ TaskInstanceCollectionResponse,
+ TaskInstanceResponse,
+ TaskInstanceState,
TriggerDAGRunPostBody,
VariableBody,
VariableCollectionResponse,
@@ -1557,6 +1560,40 @@ class TestProvidersOperations:
assert response == self.provider_collection_response
+class TestTaskInstancesOperations:
+ task_instance_response = TaskInstanceResponse(
+ id=uuid.UUID("4d828a62-a417-4936-a7a6-2b3fabacecab"),
+ task_id="task_id",
+ dag_id="dag_id",
+ dag_run_id="dag_run_id",
+ map_index=-1,
+ run_after=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
+ state=TaskInstanceState.SUCCESS,
+ try_number=1,
+ max_tries=0,
+ task_display_name="task_id",
+ dag_display_name="dag_id",
+ pool="default_pool",
+ pool_slots=1,
+ executor_config="{}",
+ )
+ task_instance_collection_response = TaskInstanceCollectionResponse(
+ task_instances=[task_instance_response],
+ total_entries=1,
+ )
+
+ def test_list(self):
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path ==
"/api/v2/dags/dag_id/dagRuns/dag_run_id/taskInstances"
+ return httpx.Response(
+ 200,
json=json.loads(self.task_instance_collection_response.model_dump_json())
+ )
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.task_instances.list(dag_id="dag_id",
dag_run_id="dag_run_id")
+ assert response == self.task_instance_collection_response
+
+
class TestVariablesOperations:
key = "key"
value = "val"
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
new file mode 100644
index 00000000000..cb134726711
--- /dev/null
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
@@ -0,0 +1,284 @@
+# 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
+import uuid
+from unittest import mock
+
+import httpx
+import pytest
+
+from airflowctl.api.datamodels.generated import (
+ TaskInstanceCollectionResponse,
+ TaskInstanceResponse,
+ TaskInstanceState,
+)
+from airflowctl.api.operations import ServerResponseError
+from airflowctl.ctl import cli_parser
+from airflowctl.ctl.commands import task_command
+
+
+def _make_server_error(status_code: int) -> ServerResponseError:
+ request = httpx.Request("GET",
"http://testserver/api/v2/dags/test_dag/dagRuns/test_run")
+ response = httpx.Response(status_code, request=request, json={"detail":
"boom"})
+ return ServerResponseError(message="boom", request=request,
response=response)
+
+
+def _normalize_rich_output(text: str) -> str:
+ return " ".join(text.split())
+
+
+class TestStatesForDagRun:
+ parser = cli_parser.get_parser()
+ dag_id = "test_dag"
+ run_id = "test_run"
+ logical_date = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
+
+ def _make_task_instance(
+ self,
+ task_id: str,
+ *,
+ map_index: int = -1,
+ state: TaskInstanceState | None = TaskInstanceState.SUCCESS,
+ start_date: datetime.datetime | None = None,
+ end_date: datetime.datetime | None = None,
+ ) -> TaskInstanceResponse:
+ return TaskInstanceResponse(
+ id=uuid.uuid4(),
+ task_id=task_id,
+ dag_id=self.dag_id,
+ dag_run_id=self.run_id,
+ map_index=map_index,
+ logical_date=self.logical_date,
+ run_after=self.logical_date,
+ state=state,
+ start_date=start_date,
+ end_date=end_date,
+ try_number=1,
+ max_tries=0,
+ task_display_name=task_id,
+ dag_display_name=self.dag_id,
+ pool="default_pool",
+ pool_slots=1,
+ executor_config="{}",
+ )
+
+ def _make_api_client(self, task_instances: list[TaskInstanceResponse]) ->
mock.MagicMock:
+ api_client = mock.MagicMock()
+ api_client.dag_runs.list.return_value.dag_runs =
[mock.MagicMock(dag_run_id=self.run_id)]
+ api_client.task_instances.list.return_value =
TaskInstanceCollectionResponse(
+ task_instances=task_instances,
+ total_entries=len(task_instances),
+ )
+ return api_client
+
+ @mock.patch("airflowctl.ctl.commands.task_command.AirflowConsole")
+ def test_states_for_dag_run_by_run_id(self, mock_console_cls):
+ start_date = datetime.datetime(2025, 1, 1, 1,
tzinfo=datetime.timezone.utc)
+ end_date = datetime.datetime(2025, 1, 1, 2,
tzinfo=datetime.timezone.utc)
+ api_client = self._make_api_client(
+ [
+ self._make_task_instance("task_a", start_date=start_date,
end_date=end_date),
+ self._make_task_instance("task_b", state=None),
+ ]
+ )
+
+ task_command.states_for_dag_run(
+ self.parser.parse_args(["tasks", "states-for-dag-run",
self.dag_id, self.run_id]),
+ api_client=api_client,
+ )
+
+ api_client.dag_runs.list.assert_not_called()
+
api_client.task_instances.list.assert_called_once_with(dag_id=self.dag_id,
dag_run_id=self.run_id)
+ mock_console_cls.return_value.print_as.assert_called_once_with(
+ data=[
+ {
+ "dag_id": self.dag_id,
+ "logical_date": "2025-01-01T00:00:00+00:00",
+ "task_id": "task_a",
+ "state": "success",
+ "start_date": "2025-01-01T01:00:00+00:00",
+ "end_date": "2025-01-01T02:00:00+00:00",
+ },
+ {
+ "dag_id": self.dag_id,
+ "logical_date": "2025-01-01T00:00:00+00:00",
+ "task_id": "task_b",
+ "state": "",
+ "start_date": "",
+ "end_date": "",
+ },
+ ],
+ output="json",
+ )
+
+ @mock.patch("airflowctl.ctl.commands.task_command.AirflowConsole")
+ def test_states_for_dag_run_by_logical_date(self, mock_console_cls):
+ api_client =
self._make_api_client([self._make_task_instance("task_a")])
+
+ task_command.states_for_dag_run(
+ self.parser.parse_args(
+ ["tasks", "states-for-dag-run", self.dag_id, "--logical-date",
self.logical_date.isoformat()]
+ ),
+ api_client=api_client,
+ )
+
+ api_client.dag_runs.list.assert_called_once_with(
+ dag_id=self.dag_id,
+ logical_date_gte=self.logical_date,
+ logical_date_lte=self.logical_date,
+ order_by="-id",
+ limit=1,
+ suppress_error_log=True,
+ )
+
api_client.task_instances.list.assert_called_once_with(dag_id=self.dag_id,
dag_run_id=self.run_id)
+ mock_console_cls.return_value.print_as.assert_called_once_with(
+ data=[
+ {
+ "dag_id": self.dag_id,
+ "logical_date": "2025-01-01T00:00:00+00:00",
+ "task_id": "task_a",
+ "state": "success",
+ "start_date": "",
+ "end_date": "",
+ },
+ ],
+ output="json",
+ )
+
+ @mock.patch("airflowctl.ctl.commands.task_command.AirflowConsole")
+ def test_states_for_dag_run_includes_map_index_for_mapped_instances(self,
mock_console_cls):
+ api_client = self._make_api_client(
+ [
+ self._make_task_instance("plain_task"),
+ self._make_task_instance("mapped_task", map_index=0),
+ self._make_task_instance("mapped_task", map_index=1),
+ ]
+ )
+
+ task_command.states_for_dag_run(
+ self.parser.parse_args(["tasks", "states-for-dag-run",
self.dag_id, self.run_id]),
+ api_client=api_client,
+ )
+
+ rows = mock_console_cls.return_value.print_as.call_args.kwargs["data"]
+ assert [row["map_index"] for row in rows] == ["", "0", "1"]
+
+ @pytest.mark.parametrize(
+ "extra_args",
+ [
+ [],
+ ["test_run", "--logical-date", "2025-01-01T00:00:00+00:00"],
+ ],
+ ids=["neither", "both"],
+ )
+ def
test_states_for_dag_run_requires_exactly_one_of_run_id_and_logical_date(self,
extra_args, capsys):
+ api_client = mock.MagicMock()
+
+ with pytest.raises(SystemExit, match="1"):
+ task_command.states_for_dag_run(
+ self.parser.parse_args(["tasks", "states-for-dag-run",
self.dag_id, *extra_args]),
+ api_client=api_client,
+ )
+
+ api_client.task_instances.list.assert_not_called()
+ assert _normalize_rich_output(capsys.readouterr().out) == (
+ "Provide either run_id or --logical-date, but not both"
+ )
+
+ @pytest.mark.parametrize(
+ ("logical_date", "expected_message"),
+ [
+ ("not-a-date", "Invalid --logical-date: 'not-a-date'"),
+ ("2025-01-01T00:00:00", "--logical-date must include a timezone
offset"),
+ ],
+ ids=["unparsable", "naive"],
+ )
+ def test_states_for_dag_run_rejects_bad_logical_date(self, logical_date,
expected_message, capsys):
+ api_client = mock.MagicMock()
+
+ with pytest.raises(SystemExit, match="1"):
+ task_command.states_for_dag_run(
+ self.parser.parse_args(
+ ["tasks", "states-for-dag-run", self.dag_id,
"--logical-date", logical_date]
+ ),
+ api_client=api_client,
+ )
+
+ api_client.dag_runs.list.assert_not_called()
+ assert _normalize_rich_output(capsys.readouterr().out) ==
expected_message
+
+ @pytest.mark.parametrize("list_failure", ["no_matching_run",
"dag_not_found_404"])
+ def test_states_for_dag_run_dag_run_not_found_by_logical_date(self,
list_failure, capsys):
+ api_client = mock.MagicMock()
+ if list_failure == "no_matching_run":
+ api_client.dag_runs.list.return_value.dag_runs = []
+ else:
+ api_client.dag_runs.list.side_effect = _make_server_error(404)
+
+ with pytest.raises(SystemExit, match="1"):
+ task_command.states_for_dag_run(
+ self.parser.parse_args(
+ [
+ "tasks",
+ "states-for-dag-run",
+ self.dag_id,
+ "--logical-date",
+ self.logical_date.isoformat(),
+ ]
+ ),
+ api_client=api_client,
+ )
+
+ api_client.task_instances.list.assert_not_called()
+ assert _normalize_rich_output(capsys.readouterr().out) == (
+ "Dag run for test_dag with logical date
'2025-01-01T00:00:00+00:00' not found"
+ )
+
+ def test_states_for_dag_run_dag_run_not_found(self, capsys):
+ api_client = mock.MagicMock()
+ api_client.task_instances.list.side_effect = _make_server_error(404)
+
+ with pytest.raises(SystemExit, match="1"):
+ task_command.states_for_dag_run(
+ self.parser.parse_args(["tasks", "states-for-dag-run",
self.dag_id, self.run_id]),
+ api_client=api_client,
+ )
+
+ assert _normalize_rich_output(capsys.readouterr().out) == (
+ "Dag run 'test_run' of Dag 'test_dag' not found"
+ )
+
+ @pytest.mark.parametrize(
+ "failing_call",
+ ["dag_runs_list", "task_instances_list"],
+ )
+ def test_states_for_dag_run_propagates_non_404_api_error(self,
failing_call):
+ api_client = mock.MagicMock()
+ error = _make_server_error(500)
+ if failing_call == "dag_runs_list":
+ api_client.dag_runs.list.side_effect = error
+ argv = ["tasks", "states-for-dag-run", self.dag_id,
"--logical-date", "2025-01-01T00:00:00+00:00"]
+ else:
+ api_client.task_instances.list.side_effect = error
+ argv = ["tasks", "states-for-dag-run", self.dag_id, self.run_id]
+
+ with pytest.raises(ServerResponseError) as ctx:
+ task_command.states_for_dag_run(self.parser.parse_args(argv),
api_client=api_client)
+
+ assert ctx.value is error
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
index 281bd2b040b..b843b3b7568 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -784,6 +784,7 @@ class TestCliConfigMethods:
[
("assets", "get", "Retrieve an asset by its ID"),
("connections", "get", "Retrieve a connection by its ID"),
+ ("taskinstances", "list", "List all task instances for a given Dag
run"),
],
)
def test_help_texts_used_for_auto_generated_commands(self, group_name,
subcommand_name, expected_help):
@@ -797,3 +798,4 @@ class TestCliConfigMethods:
"Help message should match the help_text.yaml"
)
return
+ pytest.fail(f"Auto-generated command not found: {group_name}
{subcommand_name}")
diff --git a/scripts/in_container/run_capture_airflowctl_help.py
b/scripts/in_container/run_capture_airflowctl_help.py
index 9529dbe0439..52753d6a551 100644
--- a/scripts/in_container/run_capture_airflowctl_help.py
+++ b/scripts/in_container/run_capture_airflowctl_help.py
@@ -45,6 +45,8 @@ COMMANDS = [
"jobs",
"pools",
"providers",
+ "taskinstances",
+ "tasks",
"variables",
"version",
"plugins",