This is an automated email from the ASF dual-hosted git repository.
potiuk 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 9c038c1d483 Add airflowctl tasks get and list operations (#76145)
9c038c1d483 is described below
commit 9c038c1d483ac6c30d7c310ee6a5338897e80213
Author: Pratik <[email protected]>
AuthorDate: Sun Nov 1 04:08:24 2026 -0800
Add airflowctl tasks get and list operations (#76145)
* Add airflowctl tasks get and list operations
TasksOperations was missing get and list, leaving the airflowctl tasks
command surface incomplete for #66173. Task-instance operations are
covered separately by #61547.
* Regenerate airflowctl command hashes and help images
* Add CLI parsing tests for tasks get and list
Cover argument parsing for the new tasks commands: --version-number
for get, --order-by for list, and missing required arguments, as
suggested in review.
* Encode the path parameters of airflowctl tasks get and list
Every other airflowctl operation passes its path parameters through
_quote_path_param; without it a task ID such as '..' is dropped from the path
by httpx.
Generated-by: Claude Opus 5
---------
Co-authored-by: Jarek Potiuk <[email protected]>
---
.../airflowctl_tests/test_airflowctl_commands.py | 2 +
airflow-ctl/docs/images/command_hashes.txt | 2 +-
airflow-ctl/docs/images/output_tasks.svg | 90 ++++++++++++----------
airflow-ctl/src/airflowctl/api/operations.py | 22 ++++++
airflow-ctl/src/airflowctl/ctl/help_texts.yaml | 2 +
.../tests/airflow_ctl/api/test_operations.py | 78 +++++++++++++++++++
.../tests/airflow_ctl/ctl/test_cli_config.py | 33 ++++++++
7 files changed, 189 insertions(+), 40 deletions(-)
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 48a213902d4..6ac690fc9d5 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -105,6 +105,8 @@ TEST_COMMANDS = [
# Dag Run commands
"dagrun list --dag-id example_bash_operator --state success --limit=1",
# Tasks commands
+ "tasks get example_bash_operator runme_0",
+ "tasks list example_bash_operator",
'tasks failed-deps example_bash_operator runme_0 "manual__{date_param}"',
'tasks failed-deps example_bash_operator runme_0 --logical-date
"{date_param}"',
'tasks states-for-dag-run example_bash_operator "manual__{date_param}"',
diff --git a/airflow-ctl/docs/images/command_hashes.txt
b/airflow-ctl/docs/images/command_hashes.txt
index 830500bf60b..7b30c1068bf 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -10,7 +10,7 @@ jobs:d4af478f28dae48ee18d43b998edc345
pools:19efe105b9515ab1926ebcaf0e028d71
providers:826e68c9de8254293ba810a5c67fa8c7
taskinstances:af95eda24a2bc4c6bd87c9f1324dbc3b
-tasks:eb70701dfe1b9baeda39d5eea07fde5f
+tasks:56fa16e067fea389a11727b21985ec2f
variables:f8fc76d3d398b2780f4e97f7cd816646
version:31f4efdf8de0dbaaa4fac71ff7efecc3
plugins:4864fd8f356704bd2b3cd1aec3567e35
diff --git a/airflow-ctl/docs/images/output_tasks.svg
b/airflow-ctl/docs/images/output_tasks.svg
index 08d22e642ed..b1b5bf1014b 100644
--- a/airflow-ctl/docs/images/output_tasks.svg
+++ b/airflow-ctl/docs/images/output_tasks.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 391.59999999999997"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 464.79999999999995"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,94 +19,106 @@
font-weight: 700;
}
- .terminal-761188829-matrix {
+ .terminal-4044056635-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-761188829-title {
+ .terminal-4044056635-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-761188829-r1 { fill: #ff8700 }
-.terminal-761188829-r2 { fill: #c5c8c6 }
-.terminal-761188829-r3 { fill: #808080 }
-.terminal-761188829-r4 { fill: #68a0b3 }
+ .terminal-4044056635-r1 { fill: #ff8700 }
+.terminal-4044056635-r2 { fill: #c5c8c6 }
+.terminal-4044056635-r3 { fill: #808080 }
+.terminal-4044056635-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-761188829-clip-terminal">
- <rect x="0" y="0" width="914.0" height="340.59999999999997" />
+ <clipPath id="terminal-4044056635-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="413.79999999999995" />
</clipPath>
- <clipPath id="terminal-761188829-line-0">
+ <clipPath id="terminal-4044056635-line-0">
<rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-1">
+<clipPath id="terminal-4044056635-line-1">
<rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-2">
+<clipPath id="terminal-4044056635-line-2">
<rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-3">
+<clipPath id="terminal-4044056635-line-3">
<rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-4">
+<clipPath id="terminal-4044056635-line-4">
<rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-5">
+<clipPath id="terminal-4044056635-line-5">
<rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-6">
+<clipPath id="terminal-4044056635-line-6">
<rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-7">
+<clipPath id="terminal-4044056635-line-7">
<rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-8">
+<clipPath id="terminal-4044056635-line-8">
<rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-9">
+<clipPath id="terminal-4044056635-line-9">
<rect x="0" y="221.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-10">
+<clipPath id="terminal-4044056635-line-10">
<rect x="0" y="245.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-11">
+<clipPath id="terminal-4044056635-line-11">
<rect x="0" y="269.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-761188829-line-12">
+<clipPath id="terminal-4044056635-line-12">
<rect x="0" y="294.3" width="915" height="24.65"/>
</clipPath>
+<clipPath id="terminal-4044056635-line-13">
+ <rect x="0" y="318.7" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-4044056635-line-14">
+ <rect x="0" y="343.1" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-4044056635-line-15">
+ <rect x="0" y="367.5" 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="389.6" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="462.8" 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-761188829-clip-terminal)">
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-4044056635-clip-terminal)">
- <g class="terminal-761188829-matrix">
- <text class="terminal-761188829-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-761188829-line-0)">Usage:</text><text
class="terminal-761188829-r3" x="85.4" y="20" textLength="195.2"
clip-path="url(#terminal-761188829-line-0)">airflowctl tasks</text><text
class="terminal-761188829-r2" x="280.6" y="20" textLength="24.4"
clip-path="url(#terminal-761188829-line-0)"> [</text><text
class="terminal-761188829-r4" x="305" y="20" textLength="24.4"
clip-path="url(#termina [...]
-</text><text class="terminal-761188829-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-761188829-line-1)">
-</text><text class="terminal-761188829-r2" x="0" y="68.8" textLength="292.8"
clip-path="url(#terminal-761188829-line-2)">Perform Tasks operations</text><text
class="terminal-761188829-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-761188829-line-2)">
-</text><text class="terminal-761188829-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-761188829-line-3)">
-</text><text class="terminal-761188829-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-761188829-line-4)">Positional Arguments:</text><text
class="terminal-761188829-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-761188829-line-4)">
-</text><text class="terminal-761188829-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-761188829-line-5)">COMMAND</text><text
class="terminal-761188829-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-761188829-line-5)">
-</text><text class="terminal-761188829-r4" x="48.8" y="166.4" textLength="61"
clip-path="url(#terminal-761188829-line-6)">clear</text><text
class="terminal-761188829-r2" x="268.4" y="166.4" textLength="475.8"
clip-path="url(#terminal-761188829-line-6)">Clear task instances of a Dag by its ID</text><text
class="terminal-761188829-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-761188829-line-6)">
-</text><text class="terminal-761188829-r4" x="48.8" y="190.8"
textLength="134.2"
clip-path="url(#terminal-761188829-line-7)">failed-deps</text><text
class="terminal-761188829-r2" x="268.4" y="190.8" textLength="610"
clip-path="url(#terminal-761188829-line-7)">Returns the unmet dependencies for a task instance</text><text
class="terminal-761188829-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-761188829-line-7)">
-</text><text class="terminal-761188829-r4" x="48.8" y="215.2" textLength="61"
clip-path="url(#terminal-761188829-line-8)">state</text><text
class="terminal-761188829-r2" x="268.4" y="215.2" textLength="390.4"
clip-path="url(#terminal-761188829-line-8)">Get the state of a task instance</text><text
class="terminal-761188829-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-761188829-line-8)">
-</text><text class="terminal-761188829-r4" x="48.8" y="239.6"
textLength="219.6"
clip-path="url(#terminal-761188829-line-9)">states-for-dag-run</text><text
class="terminal-761188829-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-761188829-line-9)">
-</text><text class="terminal-761188829-r2" x="268.4" y="264"
textLength="597.8"
clip-path="url(#terminal-761188829-line-10)">Get the status of all task instances in a Dag run</text><text
class="terminal-761188829-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-761188829-line-10)">
-</text><text class="terminal-761188829-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-761188829-line-11)">
-</text><text class="terminal-761188829-r1" x="0" y="312.8" textLength="97.6"
clip-path="url(#terminal-761188829-line-12)">Options:</text><text
class="terminal-761188829-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-761188829-line-12)">
-</text><text class="terminal-761188829-r4" x="24.4" y="337.2"
textLength="24.4" clip-path="url(#terminal-761188829-line-13)">-h</text><text
class="terminal-761188829-r2" x="48.8" y="337.2" textLength="24.4"
clip-path="url(#terminal-761188829-line-13)">, </text><text
class="terminal-761188829-r4" x="73.2" y="337.2" textLength="73.2"
clip-path="url(#terminal-761188829-line-13)">--help</text><text
class="terminal-761188829-r2" x="268.4" y="337.2" textLength="378.2"
clip-path="url(#term [...]
+ <g class="terminal-4044056635-matrix">
+ <text class="terminal-4044056635-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-4044056635-line-0)">Usage:</text><text
class="terminal-4044056635-r3" x="85.4" y="20" textLength="195.2"
clip-path="url(#terminal-4044056635-line-0)">airflowctl tasks</text><text
class="terminal-4044056635-r2" x="280.6" y="20" textLength="24.4"
clip-path="url(#terminal-4044056635-line-0)"> [</text><text
class="terminal-4044056635-r4" x="305" y="20" textLength="24.4"
clip-path="url(# [...]
+</text><text class="terminal-4044056635-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-4044056635-line-1)">
+</text><text class="terminal-4044056635-r2" x="0" y="68.8" textLength="292.8"
clip-path="url(#terminal-4044056635-line-2)">Perform Tasks operations</text><text
class="terminal-4044056635-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-4044056635-line-2)">
+</text><text class="terminal-4044056635-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-4044056635-line-3)">
+</text><text class="terminal-4044056635-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-4044056635-line-4)">Positional Arguments:</text><text
class="terminal-4044056635-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-4044056635-line-4)">
+</text><text class="terminal-4044056635-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-4044056635-line-5)">COMMAND</text><text
class="terminal-4044056635-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-4044056635-line-5)">
+</text><text class="terminal-4044056635-r4" x="48.8" y="166.4" textLength="61"
clip-path="url(#terminal-4044056635-line-6)">clear</text><text
class="terminal-4044056635-r2" x="268.4" y="166.4" textLength="475.8"
clip-path="url(#terminal-4044056635-line-6)">Clear task instances of a Dag by its ID</text><text
class="terminal-4044056635-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-4044056635-line-6)">
+</text><text class="terminal-4044056635-r4" x="48.8" y="190.8"
textLength="134.2"
clip-path="url(#terminal-4044056635-line-7)">failed-deps</text><text
class="terminal-4044056635-r2" x="268.4" y="190.8" textLength="610"
clip-path="url(#terminal-4044056635-line-7)">Returns the unmet dependencies for a task instance</text><text
class="terminal-4044056635-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-4044056635-line-7)">
+</text><text class="terminal-4044056635-r4" x="48.8" y="215.2"
textLength="36.6" clip-path="url(#terminal-4044056635-line-8)">get</text><text
class="terminal-4044056635-r2" x="268.4" y="215.2" textLength="573.4"
clip-path="url(#terminal-4044056635-line-8)">Retrieve a task for a Dag by its ID and task ID</text><text
class="terminal-4044056635-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-4044056635-line-8)">
+</text><text class="terminal-4044056635-r4" x="48.8" y="239.6"
textLength="48.8" clip-path="url(#terminal-4044056635-line-9)">list</text><text
class="terminal-4044056635-r2" x="268.4" y="239.6" textLength="244"
clip-path="url(#terminal-4044056635-line-9)">List tasks for a Dag</text><text
class="terminal-4044056635-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-4044056635-line-9)">
+</text><text class="terminal-4044056635-r4" x="48.8" y="264" textLength="61"
clip-path="url(#terminal-4044056635-line-10)">state</text><text
class="terminal-4044056635-r2" x="268.4" y="264" textLength="390.4"
clip-path="url(#terminal-4044056635-line-10)">Get the state of a task instance</text><text
class="terminal-4044056635-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-4044056635-line-10)">
+</text><text class="terminal-4044056635-r4" x="48.8" y="288.4"
textLength="219.6"
clip-path="url(#terminal-4044056635-line-11)">states-for-dag-run</text><text
class="terminal-4044056635-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-4044056635-line-11)">
+</text><text class="terminal-4044056635-r2" x="268.4" y="312.8"
textLength="597.8"
clip-path="url(#terminal-4044056635-line-12)">Get the status of all task instances in a Dag run</text><text
class="terminal-4044056635-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-4044056635-line-12)">
+</text><text class="terminal-4044056635-r2" x="915" y="337.2"
textLength="12.2" clip-path="url(#terminal-4044056635-line-13)">
+</text><text class="terminal-4044056635-r1" x="0" y="361.6" textLength="97.6"
clip-path="url(#terminal-4044056635-line-14)">Options:</text><text
class="terminal-4044056635-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-4044056635-line-14)">
+</text><text class="terminal-4044056635-r4" x="24.4" y="386" textLength="24.4"
clip-path="url(#terminal-4044056635-line-15)">-h</text><text
class="terminal-4044056635-r2" x="48.8" y="386" textLength="24.4"
clip-path="url(#terminal-4044056635-line-15)">, </text><text
class="terminal-4044056635-r4" x="73.2" y="386" textLength="73.2"
clip-path="url(#terminal-4044056635-line-15)">--help</text><text
class="terminal-4044056635-r2" x="268.4" y="386" textLength="378.2"
clip-path="url(#termi [...]
+</text><text class="terminal-4044056635-r2" x="915" y="410.4"
textLength="12.2" clip-path="url(#terminal-4044056635-line-16)">
</text>
</g>
</g>
diff --git a/airflow-ctl/src/airflowctl/api/operations.py
b/airflow-ctl/src/airflowctl/api/operations.py
index 556d857f371..ac934fe6f94 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -79,9 +79,11 @@ from airflowctl.api.datamodels.generated import (
ProviderDetailsResponse,
QueuedEventCollectionResponse,
QueuedEventResponse,
+ TaskCollectionResponse,
TaskDependencyCollectionResponse,
TaskInstanceCollectionResponse,
TaskInstanceResponse,
+ TaskResponse,
TaskStateStoreBody,
TaskStateStoreCollectionResponse,
TaskStateStorePatchBody,
@@ -969,6 +971,26 @@ class TaskInstancesOperations(BaseOperations):
class TasksOperations(BaseOperations):
"""Tasks operations."""
+ def get(
+ self, dag_id: str, task_id: str, version_number: int | None = None
+ ) -> TaskResponse | ServerResponseError:
+ """Get a task for a Dag."""
+ self.response = self.client.get(
+
f"dags/{_quote_path_param(dag_id)}/tasks/{_quote_path_param(task_id)}",
+ params=_build_query_params(version_number=version_number),
+ )
+ return validate_response(self.response.content, TaskResponse)
+
+ def list(self, dag_id: str, order_by: str | None = None) ->
TaskCollectionResponse | ServerResponseError:
+ """List tasks for a Dag."""
+ # The server returns every task for the Dag in one response, so this
+ # intentionally does not use execute_list's limit/offset pagination.
+ self.response = self.client.get(
+ f"dags/{_quote_path_param(dag_id)}/tasks",
+ params=_build_query_params(order_by=order_by),
+ )
+ return validate_response(self.response.content, TaskCollectionResponse)
+
def clear(
self, dag_id: str, clear_task_instances: ClearTaskInstancesBody
) -> TaskInstanceCollectionResponse | ServerResponseError:
diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
index 665cef60b3b..26e6074f16d 100644
--- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
+++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
@@ -107,6 +107,8 @@ taskinstances:
clear-state-store: "Delete all state store keys for a task instance"
tasks:
+ get: "Retrieve a task for a Dag by its ID and task ID"
+ list: "List tasks for a Dag"
clear: "Clear task instances of a Dag by its ID"
variables:
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index b7efd5b02a6..400feaa9423 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -99,6 +99,7 @@ from airflowctl.api.datamodels.generated import (
QueuedEventCollectionResponse,
QueuedEventResponse,
ReprocessBehavior,
+ TaskCollectionResponse,
TaskDependencyCollectionResponse,
TaskDependencyResponse,
TaskInletAssetReference,
@@ -106,6 +107,7 @@ from airflowctl.api.datamodels.generated import (
TaskInstanceResponse,
TaskInstanceState,
TaskOutletAssetReference,
+ TaskResponse,
TaskStateStoreCollectionResponse,
TaskStateStoreResponse,
TriggerDAGRunPostBody,
@@ -2648,6 +2650,82 @@ class TestTasksOperations:
task_instances=[task_instance_response],
total_entries=1,
)
+ task_response = TaskResponse(
+ task_id="task_1",
+ task_display_name="task_1",
+ owner="airflow",
+ start_date=None,
+ end_date=None,
+ trigger_rule="all_success",
+ depends_on_past=False,
+ wait_for_downstream=False,
+ retries=0,
+ queue=None,
+ pool=None,
+ pool_slots=None,
+ execution_timeout=None,
+ retry_delay=None,
+ retry_exponential_backoff=1.0,
+ priority_weight=None,
+ weight_rule=None,
+ ui_color=None,
+ ui_fgcolor=None,
+ template_fields=None,
+ downstream_task_ids=None,
+ doc_md=None,
+ operator_name="BashOperator",
+ params=None,
+ class_ref=None,
+ is_mapped=False,
+ extra_links=[],
+ )
+ task_collection_response = TaskCollectionResponse(
+ tasks=[task_response],
+ total_entries=1,
+ )
+
+ @pytest.mark.parametrize("version_number", [None, 2])
+ def test_get(self, version_number):
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path ==
f"/api/v2/dags/{self.dag_id}/tasks/task_1"
+ if version_number is None:
+ assert "version_number" not in request.url.params
+ else:
+ assert request.url.params["version_number"] ==
str(version_number)
+ return httpx.Response(200,
json=json.loads(self.task_response.model_dump_json()))
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.tasks.get(self.dag_id, "task_1",
version_number=version_number)
+ assert response == self.task_response
+
+ @pytest.mark.parametrize("order_by", [None, "-task_id"])
+ def test_list(self, order_by):
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path == f"/api/v2/dags/{self.dag_id}/tasks"
+ if order_by is None:
+ assert "order_by" not in request.url.params
+ else:
+ assert request.url.params["order_by"] == order_by
+ return httpx.Response(200,
json=json.loads(self.task_collection_response.model_dump_json()))
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.tasks.list(self.dag_id, order_by=order_by)
+ assert response == self.task_collection_response
+
+ @pytest.mark.parametrize(
+ ("method", "args", "expected_path", "response"),
+ [
+ ("get", ("a#b", ".."), "/api/v2/dags/a%23b/tasks/%2E%2E",
"task_response"),
+ ("list", ("a#b",), "/api/v2/dags/a%23b/tasks",
"task_collection_response"),
+ ],
+ )
+ def test_encodes_path_params(self, method, args, expected_path, response):
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.raw_path.decode().split("?")[0] == expected_path
+ return httpx.Response(200, json=json.loads(getattr(self,
response).model_dump_json()))
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ assert getattr(client.tasks, method)(*args) == getattr(self, response)
def test_clear(self):
expected_body = self.clear_task_instances.model_dump(mode="json",
exclude_defaults=True)
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 961a41c8c77..27bfcab1467 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -1192,6 +1192,37 @@ class TestCliConfigMethods:
assert args_by_flag["--task-ids"].kwargs["type"] is str
assert "--output" in args_by_flag
+ def test_tasks_get_and_list_args_parse(self):
+ """``tasks get`` and ``tasks list`` parse positionals and optional
flags, and reject missing required args."""
+ command_factory = CommandFactory()
+ tasks_group = next(
+ group_command for group_command in command_factory.group_commands
if group_command.name == "tasks"
+ )
+ sub_commands = {sub_command.name: sub_command for sub_command in
tasks_group.subcommands}
+
+ get_parser = argparse.ArgumentParser()
+ for arg in sub_commands["get"].args:
+ arg.add_to_parser(get_parser)
+ namespace = get_parser.parse_args(["example_bash_operator", "runme_0"])
+ assert namespace.dag_id == "example_bash_operator"
+ assert namespace.task_id == "runme_0"
+ assert namespace.version_number is None
+ namespace = get_parser.parse_args(["example_bash_operator", "runme_0",
"--version-number", "3"])
+ assert namespace.version_number == 3
+ with pytest.raises(SystemExit):
+ get_parser.parse_args(["example_bash_operator"])
+
+ list_parser = argparse.ArgumentParser()
+ for arg in sub_commands["list"].args:
+ arg.add_to_parser(list_parser)
+ namespace = list_parser.parse_args(["example_bash_operator"])
+ assert namespace.dag_id == "example_bash_operator"
+ assert namespace.order_by is None
+ namespace = list_parser.parse_args(["example_bash_operator",
"--order-by=-start_date"])
+ assert namespace.order_by == "-start_date"
+ with pytest.raises(SystemExit):
+ list_parser.parse_args([])
+
@pytest.mark.parametrize(
("field", "raw_value", "expected_value"),
[
@@ -1312,6 +1343,8 @@ class TestCliConfigMethods:
("taskinstances", "get", "Get a task instance for a given Dag
run"),
("taskinstances", "get-dependencies", "Get unmet scheduler
dependencies for a task instance"),
("taskinstances", "state-store", "Manage the state store of a task
instance"),
+ ("tasks", "get", "Retrieve a task for a Dag by its ID and task
ID"),
+ ("tasks", "list", "List tasks for a Dag"),
("tasks", "clear", "Clear task instances of a Dag by its ID"),
],
)