airflow-api-client 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. airflow_api_client/__init__.py +1 -0
  2. airflow_api_client/auth.py +164 -0
  3. airflow_api_client/commands/__init__.py +36 -0
  4. airflow_api_client/commands/create_payload.py +64 -0
  5. airflow_api_client/commands/get/__init__.py +6 -0
  6. airflow_api_client/commands/get/dag.py +23 -0
  7. airflow_api_client/commands/get/dag_run.py +27 -0
  8. airflow_api_client/commands/get/log.py +96 -0
  9. airflow_api_client/commands/get/main.py +72 -0
  10. airflow_api_client/commands/get/task_instance.py +32 -0
  11. airflow_api_client/commands/list/__init__.py +6 -0
  12. airflow_api_client/commands/list/base.py +63 -0
  13. airflow_api_client/commands/list/dag_runs.py +62 -0
  14. airflow_api_client/commands/list/dags.py +53 -0
  15. airflow_api_client/commands/list/main.py +39 -0
  16. airflow_api_client/commands/list/task_instances.py +56 -0
  17. airflow_api_client/commands/pause.py +35 -0
  18. airflow_api_client/commands/profile.py +216 -0
  19. airflow_api_client/commands/raw.py +99 -0
  20. airflow_api_client/commands/run.py +110 -0
  21. airflow_api_client/commands/unpause.py +33 -0
  22. airflow_api_client/commands/xcom.py +54 -0
  23. airflow_api_client/compat.py +66 -0
  24. airflow_api_client/filters.py +84 -0
  25. airflow_api_client/io.py +197 -0
  26. airflow_api_client/log.py +80 -0
  27. airflow_api_client/main.py +357 -0
  28. airflow_api_client/models/__init__.py +674 -0
  29. airflow_api_client/typedefs.py +10 -0
  30. airflow_api_client-0.1.0.dist-info/METADATA +509 -0
  31. airflow_api_client-0.1.0.dist-info/RECORD +35 -0
  32. airflow_api_client-0.1.0.dist-info/WHEEL +4 -0
  33. airflow_api_client-0.1.0.dist-info/entry_points.txt +5 -0
  34. airflow_api_client-0.1.0.dist-info/licenses/LICENSE +201 -0
  35. airflow_api_client-0.1.0.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,164 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Authentication provider for Airflow API client that supports JWT tokens.
3
+ AirflowAPIClientAuthProvider is the default authentication provider for Airflow API client. Custom
4
+ providers can be implemented by subclassing AirflowAPIClientAuthProvider and overriding the
5
+ relevant methods. The provider can be configured via command-line arguments or environment
6
+ variables.
7
+
8
+ See --auth-provider argument in the CLI for more details on how to specify a custom authentication
9
+ provider.
10
+ """
11
+
12
+ import base64
13
+ import json
14
+ import os
15
+ import time
16
+ from abc import ABC, abstractmethod
17
+ from argparse import ArgumentParser
18
+ from typing import TYPE_CHECKING
19
+
20
+ import requests
21
+
22
+ from airflow_api_client.log import logger
23
+
24
+ try:
25
+ import keyring # noqa: F401, optional dependency for secure password storage
26
+ except ImportError:
27
+ keyring = None
28
+
29
+ if TYPE_CHECKING:
30
+ # The type hint for `args` argument
31
+ from configparser import RawConfigParser
32
+
33
+ from airflow_client.client import ApiClient
34
+
35
+
36
+ class AuthToken(ABC): # pylint: disable=too-few-public-methods
37
+ """Abstract base class for authentication tokens."""
38
+
39
+ @property
40
+ @abstractmethod
41
+ def expires(self) -> int:
42
+ """Returns the number of seconds until the token expires."""
43
+
44
+
45
+ class AirflowClientJWTToken(AuthToken): # pylint: disable=too-few-public-methods
46
+ """Represents a JWT token used for authenticating with the Airflow API."""
47
+
48
+ def __init__(self, token: str):
49
+ self.token = token
50
+
51
+ parts = token.split(".")
52
+ if len(parts) != 3:
53
+ raise ValueError("Invalid JWT format")
54
+
55
+ payload = parts[1]
56
+
57
+ # Add padding if needed (JWT uses base64url without padding)
58
+ padding = 4 - len(payload) % 4
59
+ if padding != 4:
60
+ payload += "=" * padding
61
+
62
+ # Decode base64
63
+ self.decoded = json.loads(base64.urlsafe_b64decode(payload))
64
+
65
+ @property
66
+ def expires(self) -> int:
67
+ # Returns the number of seconds until the token expires
68
+ return self.decoded.get("exp", 0) - int(time.time())
69
+
70
+
71
+ class AirflowAPIClientAuthProvider:
72
+ """Default authentication provider for Airflow API client that supports username/password auth
73
+ and JWT tokens."""
74
+
75
+ def __init__(self, args):
76
+ self.args = args
77
+ self._token = None
78
+ self.timeout = 10
79
+
80
+ @property
81
+ def password(self) -> str:
82
+ """Returns the password for authentication. Falls back to keyring if available."""
83
+ if self.args.password is not None:
84
+ return self.args.password
85
+
86
+ if keyring:
87
+ password = keyring.get_password(
88
+ f"airflow-api-client/{self.args.endpoint}", self.username
89
+ )
90
+ if password:
91
+ logger.debug("Using password from keyring")
92
+ return password
93
+
94
+ raise ValueError(
95
+ "Password is required. Provide via --password, AIRFLOW_API_CLIENT_PASSWORD,"
96
+ " config file, or install keyring support: pip install airflow-api-client[keyring]"
97
+ )
98
+
99
+ @property
100
+ def username(self) -> str:
101
+ """Returns the username for authentication. Raises an error if username is not provided."""
102
+ if self.args.username is None:
103
+ raise ValueError("Username is required for DefaultAuthProvider")
104
+ return self.args.username
105
+
106
+ @property
107
+ def client_token(self) -> AirflowClientJWTToken:
108
+ """Returns a valid JWT token for authenticating with the Airflow API. If a valid token is
109
+ already cached, it returns the cached token. Otherwise, it fetches a new token using the
110
+ username and password."""
111
+ if self._token and self._token.expires > 60:
112
+ return self._token
113
+
114
+ env_token = os.getenv("AIRFLOW_API_CLIENT_TOKEN")
115
+ if env_token:
116
+ return AirflowClientJWTToken(env_token)
117
+
118
+ url = f"{self.args.endpoint}/auth/token"
119
+ response = requests.post(
120
+ url,
121
+ headers={"Content-Type": "application/json"},
122
+ json={
123
+ "username": self.username,
124
+ "password": self.password,
125
+ },
126
+ timeout=self.timeout,
127
+ )
128
+ logger.debug(f"Token response: {response.status_code} - {response.text}")
129
+ self._token = AirflowClientJWTToken(response.json()["access_token"])
130
+ return self._token
131
+
132
+ def set_client_configuration(self, client: "ApiClient") -> None:
133
+ """Configures the Airflow API client with the appropriate authentication headers. This
134
+ method is called by the CLI when initializing the API client.
135
+ `client` is the instance of airflow_client.client.ApiClient that needs to be configured with
136
+ authentication headers.
137
+ """
138
+ client.access_token = self.client_token.token
139
+
140
+ @classmethod
141
+ def add_arguments(cls, profile: str, parser: "ArgumentParser", config: "RawConfigParser"):
142
+ """Adds authentication-related command-line arguments to the parser. This method is called
143
+ by the CLI when setting up the argument parser for the specified profile. The arguments
144
+ added by this method will be available in the `args` object passed to the constructor of the
145
+ authentication provider.
146
+ """
147
+ parser.add_argument(
148
+ "--username",
149
+ "-u",
150
+ type=str,
151
+ default=os.getenv(
152
+ "AIRFLOW_API_CLIENT_USERNAME", config.get(profile, "username", fallback=None)
153
+ ),
154
+ help="Airflow username (typically your SSO)",
155
+ )
156
+ parser.add_argument(
157
+ "--password",
158
+ "-p",
159
+ type=str,
160
+ default=os.getenv(
161
+ "AIRFLOW_API_CLIENT_PASSWORD", config.get(profile, "password", fallback=None)
162
+ ),
163
+ help="Airflow password (if omitted, attempt to fetch from PWSafe",
164
+ )
@@ -0,0 +1,36 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Base class for CLI commands."""
3
+
4
+ from abc import ABC, abstractmethod
5
+ from argparse import ArgumentParser
6
+
7
+
8
+ class Command(ABC):
9
+ """Base class for CLI commands."""
10
+
11
+ no_client = False
12
+
13
+ def __init__(self):
14
+ # The app is set by the CLI in main() before executing the command.
15
+ # This allows for commands that don't or can't require a client (eg. profile creation) to
16
+ # still be executed.
17
+ self.app = None
18
+
19
+ @property
20
+ @abstractmethod
21
+ def name(self) -> str:
22
+ """Command name (e.g., 'xcom', 'run')."""
23
+
24
+ @property
25
+ @abstractmethod
26
+ def help(self) -> str:
27
+ """Help text for the command."""
28
+
29
+ @classmethod
30
+ @abstractmethod
31
+ def add_arguments(cls, parser: ArgumentParser):
32
+ """Add command-specific arguments to the subparser."""
33
+
34
+ @abstractmethod
35
+ def execute(self, **kwargs):
36
+ """Execute the command."""
@@ -0,0 +1,64 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Command to construct a payload for a DAG run by prompting for values for each parameter defined
3
+ in the DAG's parameters.
4
+ """
5
+
6
+ import json
7
+
8
+ from airflow_api_client.commands import Command
9
+ from airflow_api_client.io import output
10
+ from airflow_api_client.log import logger
11
+ from airflow_api_client.models import DAG
12
+
13
+
14
+ class CreatePayload(Command):
15
+ """Command to construct a payload for a DAG run by prompting for values for each parameter
16
+ defined in the DAG's parameters."""
17
+
18
+ name = "create-payload"
19
+ help = (
20
+ "Construct a payload for a DAG run by prompting for values for each parameter defined"
21
+ " in the DAG's parameters."
22
+ )
23
+
24
+ @classmethod
25
+ def add_arguments(cls, parser) -> None:
26
+ """Adds arguments for the command."""
27
+ parser.add_argument(
28
+ "dag_id",
29
+ metavar="DAG_ID",
30
+ help=(
31
+ "The ID of the DAG for which to create a payload. The DAG's parameters will be used"
32
+ " to prompt for values and construct the payload based on the DAGs `params`."
33
+ ),
34
+ )
35
+ parser.add_argument(
36
+ "--output-file",
37
+ "-o",
38
+ metavar="OUTPUT_FILE",
39
+ help=(
40
+ "Optional file path to save the constructed payload. If not provided, the payload"
41
+ " will be printed to the console."
42
+ ),
43
+ )
44
+
45
+ def execute(self, **kwargs) -> None:
46
+ """Returns the payload expected for a given DAG. Prompts for values for each parameter
47
+ defined in the DAG's parameters and constructs a payload dictionary.
48
+ """
49
+ logger.debug("Executing CreatePayload with arguments", dag_id=self.app.args.dag_id)
50
+ dag = DAG(self.app, self.app.args.dag_id)
51
+ parameters = dag.params or {}
52
+ payload = {}
53
+ logger.debug(
54
+ "Constructing payload for DAG run", dag_id=self.app.args.dag_id, parameters=parameters
55
+ )
56
+ payload = dag.create_payload()
57
+
58
+ if self.app.args.output_file:
59
+ with open(self.app.args.output_file, "w", encoding="utf-8") as f:
60
+ json.dump(payload, f, indent=4, sort_keys=True, default=str)
61
+ logger.info(f"Payload saved to {self.app.args.output_file}")
62
+ else:
63
+ output(json.dumps(payload, indent=4, sort_keys=True, default=str), self.app.args)
64
+ return payload
@@ -0,0 +1,6 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Get command module"""
3
+
4
+ from airflow_api_client.commands.get.main import GetCommand
5
+
6
+ __all__ = ["GetCommand"]
@@ -0,0 +1,23 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Get DAG details"""
3
+
4
+ from airflow_api_client.log import logger
5
+ from airflow_api_client.models import DAG
6
+ from airflow_api_client.typedefs import App
7
+
8
+
9
+ class GetDagCommand:
10
+ """Helper class for getting DAG details"""
11
+
12
+ @classmethod
13
+ def add_subparser(cls, subparsers):
14
+ """Add subparser for dag"""
15
+ dag_parser = subparsers.add_parser("dag", help="Get DAG details")
16
+ dag_parser.add_argument("dag_id", help="DAG ID")
17
+
18
+ @staticmethod
19
+ def get_dag(app: App) -> dict:
20
+ """Get DAG details and return as dict"""
21
+ dag = DAG(app, app.args.dag_id)
22
+ logger.info(f"Retrieved details for DAG '{app.args.dag_id}'")
23
+ return dag.to_dict()
@@ -0,0 +1,27 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Get DAG run details"""
3
+
4
+ from airflow_api_client.log import logger
5
+ from airflow_api_client.models import ResourceSpec
6
+ from airflow_api_client.typedefs import App
7
+
8
+
9
+ class GetDagRunCommand:
10
+ """Helper class for getting DAG run details"""
11
+
12
+ @classmethod
13
+ def add_subparser(cls, subparsers):
14
+ """Add subparser for dag-run"""
15
+ run_parser = subparsers.add_parser("dag-run", help="Get DAG run details")
16
+ run_parser.add_argument(
17
+ "spec",
18
+ metavar="DAG_ID/RUN_ID",
19
+ help="DAG run specification in format: DAG_ID/RUN_ID",
20
+ )
21
+
22
+ @staticmethod
23
+ def get_dag_run(app: App) -> dict:
24
+ """Get DAG run details and return as dict"""
25
+ spec = ResourceSpec.from_string(app, app.args.spec)
26
+ logger.info(f"Retrieved details for DAG run '{spec.dag_run.id}'")
27
+ return spec.dag_run.to_dict()
@@ -0,0 +1,96 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Get task instance log"""
3
+
4
+ from airflow_client.client.exceptions import NotFoundException
5
+
6
+ from airflow_api_client.io import output
7
+ from airflow_api_client.log import logger
8
+ from airflow_api_client.models import ResourceSpec
9
+ from airflow_api_client.typedefs import App
10
+
11
+
12
+ class GetLogCommand:
13
+ """Helper class for getting the log for a task instance"""
14
+
15
+ @classmethod
16
+ def add_subparser(cls, subparsers):
17
+ """Add subparser for task-instance"""
18
+ task_parser = subparsers.add_parser("log", help="Get task instance log")
19
+ task_parser.add_argument(
20
+ "spec",
21
+ metavar="DAG_ID/RUN_ID[/TASK_ID]",
22
+ help="Task instance specification in format: DAG_ID/RUN_ID/TASK_ID",
23
+ )
24
+ task_parser.add_argument(
25
+ "--try-number",
26
+ type=int,
27
+ default=1,
28
+ help="Try number of the task instance log to retrieve (default: 1)",
29
+ )
30
+
31
+ task_parser.add_argument(
32
+ "--time-format",
33
+ default="%Y-%m-%dT%H:%M:%S",
34
+ type=str,
35
+ help=(
36
+ "Time format for log timestamps (only applicable"
37
+ " for human output, default: '%%Y-%%m-%%dT%%H:%%M:%%S')."
38
+ " Supports strftime format codes. Only applies to human output, ignored for"
39
+ " JSON/YAML output."
40
+ ),
41
+ )
42
+
43
+ @staticmethod
44
+ def get_log(app: App) -> dict:
45
+ """Get task instance logs and return as dict"""
46
+ spec = ResourceSpec.from_string(app, app.args.spec)
47
+
48
+ task_instances = [spec.task_instance] if spec.task_instance else []
49
+ if not task_instances:
50
+ task_instances = spec.dag_run.task_instances
51
+
52
+ logger.debug(
53
+ f"Getting logs for task instances: {task_instances} in "
54
+ f" DAG: {spec.dag.id} and DAG Run: {spec.dag_run.id}"
55
+ )
56
+
57
+ if app.args.output_format == "human":
58
+ identifier = f"{spec.dag.id}/{spec.dag_run.id}"
59
+ for task_instance in task_instances:
60
+ if task_instance.try_number < app.args.try_number:
61
+ logger.warning(
62
+ f"Task instance {task_instance.id} has try number "
63
+ f"{task_instance.try_number} which is less than the requested "
64
+ f"try number {app.args.try_number}. Returning latest available log instead."
65
+ )
66
+ output(
67
+ [[f"Log for Task Instance: {identifier}/{task_instance.id}"]],
68
+ app.args,
69
+ headers=[],
70
+ )
71
+ try:
72
+ task_instance.get_log(app.args.try_number).output(
73
+ time_format=app.args.time_format
74
+ )
75
+ except NotFoundException:
76
+ logger.error(
77
+ f"Failed to get log for task instance: {task_instance.id} (not found)"
78
+ " Upstream failed or skipped?"
79
+ )
80
+ continue
81
+ return None
82
+
83
+ # json or yaml output
84
+ logs = {}
85
+ for task_instance in task_instances:
86
+ try:
87
+ log = task_instance.get_log(try_number=app.args.try_number)
88
+ logs[task_instance.id] = list(log)
89
+ except NotFoundException:
90
+ logger.error(
91
+ f"Failed to get log for task instance: {task_instance.id} (not found)"
92
+ " Upstream failed or skipped"
93
+ )
94
+ continue
95
+
96
+ return logs
@@ -0,0 +1,72 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Main get command with subcommand dispatch"""
3
+
4
+ import json
5
+
6
+ from airflow_api_client.commands import Command
7
+ from airflow_api_client.commands.get.dag import GetDagCommand
8
+ from airflow_api_client.commands.get.dag_run import GetDagRunCommand
9
+ from airflow_api_client.commands.get.log import GetLogCommand
10
+ from airflow_api_client.commands.get.task_instance import GetTaskInstanceCommand
11
+ from airflow_api_client.io import output
12
+ from airflow_api_client.log import logger
13
+
14
+ DESCRIPTION = """
15
+ Command to get details about a specific Airflow resource. This command retrieves full details
16
+ about a DAG, DAG run, or task instance by its identifier. Unlike the list command which returns
17
+ multiple resources, get returns a single resource with all available details.
18
+ """
19
+
20
+
21
+ class GetCommand(Command):
22
+ """Command to get details about a specific resource."""
23
+
24
+ name = "get"
25
+ help = DESCRIPTION
26
+
27
+ @classmethod
28
+ def add_arguments(cls, parser) -> None:
29
+ subparsers = parser.add_subparsers(dest="resource_type", required=True)
30
+
31
+ GetDagCommand.add_subparser(subparsers)
32
+ GetDagRunCommand.add_subparser(subparsers)
33
+ GetTaskInstanceCommand.add_subparser(subparsers)
34
+ GetLogCommand.add_subparser(subparsers)
35
+
36
+ def execute(self, **kwargs) -> None:
37
+ """Get details about a specific resource"""
38
+ logger.debug(f"Executing GetCommand for resource type: {self.app.args.resource_type}")
39
+
40
+ result = None
41
+ if self.app.args.resource_type == "dag":
42
+
43
+ result = GetDagCommand.get_dag(self.app)
44
+ elif self.app.args.resource_type == "dag-run":
45
+
46
+ result = GetDagRunCommand.get_dag_run(self.app)
47
+ elif self.app.args.resource_type == "task-instance":
48
+
49
+ result = GetTaskInstanceCommand.get_task_instance(self.app)
50
+ elif self.app.args.resource_type == "log":
51
+ result = GetLogCommand.get_log(self.app)
52
+
53
+ if result:
54
+ # For human output, format as key-value table
55
+ if self.app.args.output_format == "human":
56
+ self._output_human(result)
57
+ else:
58
+ output(result, self.app.args)
59
+
60
+ def _output_human(self, data: dict) -> None:
61
+ """Output data in human-readable key-value format"""
62
+
63
+ rows = []
64
+ for key, value in sorted(data.items()):
65
+ # Format complex values as JSON
66
+ if isinstance(value, (dict, list)):
67
+ value_str = json.dumps(value, indent=2, default=str)
68
+ else:
69
+ value_str = str(value)
70
+ rows.append([key, value_str])
71
+
72
+ output(rows, self.app.args, headers=["Field", "Value"])
@@ -0,0 +1,32 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Get task instance details"""
3
+
4
+ from airflow_api_client.log import logger
5
+ from airflow_api_client.models import ResourceSpec
6
+ from airflow_api_client.typedefs import App
7
+
8
+
9
+ class GetTaskInstanceCommand:
10
+ """Helper class for getting task instance details"""
11
+
12
+ @classmethod
13
+ def add_subparser(cls, subparsers):
14
+ """Add subparser for task-instance"""
15
+ task_parser = subparsers.add_parser("task-instance", help="Get task instance details")
16
+ task_parser.add_argument(
17
+ "spec",
18
+ metavar="DAG_ID/RUN_ID/TASK_ID",
19
+ help="Task instance specification in format: DAG_ID/RUN_ID/TASK_ID",
20
+ )
21
+
22
+ @staticmethod
23
+ def get_task_instance(app: App) -> dict:
24
+ """Get task instance details and return as dict"""
25
+ spec = ResourceSpec.from_string(app, app.args.spec)
26
+ if spec.task_instance is None:
27
+ raise ValueError(
28
+ f"Invalid task instance specification: {app.args.spec}. "
29
+ "Expected format: DAG_ID/RUN_ID/TASK_ID"
30
+ )
31
+ logger.info(f"Retrieved details for task instance '{spec.task_instance.task_id}'")
32
+ return spec.task_instance.to_dict()
@@ -0,0 +1,6 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """List command module"""
3
+
4
+ from airflow_api_client.commands.list.main import ListCommand
5
+
6
+ __all__ = ["ListCommand"]
@@ -0,0 +1,63 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """Base class for list commands"""
3
+
4
+ from typing import Any, List
5
+
6
+ from airflow_api_client.commands import Command
7
+ from airflow_api_client.filters import get_obj_value
8
+ from airflow_api_client.typedefs import App
9
+
10
+
11
+ class ListCommandBase(Command):
12
+ """Base class for list commands with common functionality"""
13
+
14
+ @classmethod
15
+ def add_common_arguments(cls, parser) -> None:
16
+ """Add common arguments for all list commands"""
17
+ parser.add_argument(
18
+ "--filter",
19
+ "-f",
20
+ action="append",
21
+ help="""
22
+ Filter results based on an object's value on the form: 'attr operator value', ie
23
+ 'is_paused == true' or 'owners !^ "airflow"'. Supported operators:
24
+ " ==, !=, >, <, ^ (contains), !^ (not contains). Repeat for multiple filters.
25
+ Note that the value will be serialised using JSON, so use types accordingly. (eg. set `value` to
26
+ `null` in order to match attributes with a value of `None` and enclose strings in
27
+ quotation marks etc. Supports jmespath-notation to traverse objects.
28
+ Note that this functionality is generic and can be used to filter on any
29
+ attribute of any underlying object's JSON representation (see --output-format json) for obtaining
30
+ the full JSON representation of each object to determine which attributes to filter on.
31
+ See the airflow_client Python library docs for more info.
32
+ """,
33
+ )
34
+ parser.add_argument(
35
+ "--extra-attrs",
36
+ "-e",
37
+ type=str,
38
+ action="append",
39
+ help="""
40
+ Extra attribute to include in the output.
41
+ This will be included in the output alongside the default attributes for each resource. Output
42
+ may be modified to better suit tabular representation. Repeat for multiple extra attributes.
43
+ Related to notices in --filter. Richer objects such as lists and dictionaries are converted to
44
+ comma-separated strings for better readability in table output, for more complex or verbatim
45
+ output consider using the --output-format option with json or yaml and parsing the
46
+ output with jq/yq or similar. Supports jmespath-notation to select attributes
47
+ Example: list -e last_parse_duration -e owners -e default_args.has_on_failure_callback dags
48
+ Or see slow DAG Runs: list -f "duration > 10" dag-runs my_dag_id
49
+ """,
50
+ )
51
+
52
+ def _add_extra_attrs(self, app: App, obj: Any, headers: List, attrs: List) -> None:
53
+ """Helper method to add extra attributes to the output based on user input. Modifies headers
54
+ and attrs lists in place.
55
+ """
56
+ for extra_attr in app.args.extra_attrs or []:
57
+ extra_attr_value = get_obj_value(obj, extra_attr)
58
+ # Make lists and dicts more readable in output by converting to comma-separated strings
59
+ if hasattr(extra_attr_value, "__iter__") and not isinstance(extra_attr_value, str):
60
+ extra_attr_value = ", ".join(str(v) for v in extra_attr_value)
61
+
62
+ headers.append(extra_attr.replace("_", " ").replace(".", " ").title())
63
+ attrs.append(extra_attr_value)
@@ -0,0 +1,62 @@
1
+ # Copyright 2026 Rackspace Technology, Inc. SPDX-License-Identifier: Apache-2.0
2
+ """List DAG runs command"""
3
+
4
+ from airflow_api_client.filters import filter_obj
5
+ from airflow_api_client.io import output
6
+ from airflow_api_client.models import DAG
7
+ from airflow_api_client.typedefs import App
8
+
9
+
10
+ class ListDagRunsCommand:
11
+ """Helper class for listing DAG runs"""
12
+
13
+ @classmethod
14
+ def add_subparser(cls, subparsers):
15
+ """Add subparser for dag-runs"""
16
+ runs_parser = subparsers.add_parser("dag-runs", help="List DAG runs")
17
+ runs_parser.add_argument("dag_id", help="DAG ID")
18
+ runs_parser.add_argument("--limit", type=int, default=10, help="Limit results")
19
+
20
+ @staticmethod
21
+ def list_dag_runs(app: App, add_extra_attrs_fn) -> None:
22
+ """List DAG runs for a given DAG"""
23
+
24
+ dag = DAG(app, app.args.dag_id)
25
+
26
+ dag_runs = dag.get_dag_runs(app.args.limit)
27
+
28
+ if app.args.filter:
29
+ dag_runs = [d for d in dag_runs if filter_obj(d, app.args.filter)]
30
+
31
+ if app.args.output_format in ("json", "yaml"):
32
+ output({"dag_runs": [d.to_dict() for d in dag_runs]}, app.args)
33
+ else:
34
+ headers = [
35
+ "DAG ID",
36
+ "Run ID",
37
+ "State",
38
+ "Run Type",
39
+ "Triggered Via",
40
+ "Triggered By",
41
+ "Duration",
42
+ "Start Date",
43
+ "End Date",
44
+ ]
45
+ data = []
46
+ for run in dag_runs:
47
+ attrs = [
48
+ run.dag_id,
49
+ run.id,
50
+ run.state.value,
51
+ run.run_type.value,
52
+ run.triggered_by.value,
53
+ run.triggering_user_name,
54
+ run.duration,
55
+ run.start_date,
56
+ run.end_date,
57
+ ]
58
+
59
+ add_extra_attrs_fn(app, run, headers, attrs)
60
+
61
+ data.append(attrs)
62
+ output(data, app.args, headers=headers)