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&#160;tasks</text><text 
class="terminal-761188829-r2" x="280.6" y="20" textLength="24.4" 
clip-path="url(#terminal-761188829-line-0)">&#160;[</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&#160;Tasks&#160;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&#160;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&#160;task&#160;instances&#160;of&#160;a&#160;Dag&#160;by&#160;its&#160;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&#160;the&#160;unmet&#160;dependencies&#160;for&#160;a&#160;task&#160;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&#160;the&#160;state&#160;of&#160;a&#160;task&#160;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&#160;the&#160;status&#160;of&#160;all&#160;task&#160;instances&#160;in&#160;a&#160;Dag&#160;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)">,&#160;</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&#160;tasks</text><text 
class="terminal-4044056635-r2" x="280.6" y="20" textLength="24.4" 
clip-path="url(#terminal-4044056635-line-0)">&#160;[</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&#160;Tasks&#160;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&#160;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&#160;task&#160;instances&#160;of&#160;a&#160;Dag&#160;by&#160;its&#160;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&#160;the&#160;unmet&#160;dependencies&#160;for&#160;a&#160;task&#160;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&#160;a&#160;task&#160;for&#160;a&#160;Dag&#160;by&#160;its&#160;ID&#160;and&#160;task&#160;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&#160;tasks&#160;for&#160;a&#160;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&#160;the&#160;state&#160;of&#160;a&#160;task&#160;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&#160;the&#160;status&#160;of&#160;all&#160;task&#160;instances&#160;in&#160;a&#160;Dag&#160;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)">,&#160;</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"),
         ],
     )

Reply via email to