1fanwang opened a new pull request, #73743:
URL: https://github.com/apache/airflow/pull/73743
`airflowctl dags get-tags` crashes as soon as any Dag has a tag, whichever
`--output` is chosen:
```
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
```
## Why
The `dagTags` endpoint returns tag names as plain strings, for example
`{"tags": ["etl", "nightly"], "total_entries": 2}`. Generated commands drop
`total_entries` and print the one list that remains, but
`AirflowConsole.print_as` only renders records and raises for anything else.
Among the API's collection responses, `DAGTagCollectionResponse` is the only
one whose list holds plain values, so `get-tags` is the only command that hits
this. It works only while no Dag has a tag, because an empty list passes the
check. The 0.1.5 release fails the same way.
## What
When the result to print is a list of plain values, airflowctl now prints
one row per value, keyed by the field name. `dags get-tags -o json` prints
`[{"tags": "etl"}, {"tags": "nightly"}]`, and the table and plain formats print
one tag per line under a `tags` header. Lists of records, and values nested
inside a record such as a Dag run's `conf`, keep their current output.
The new unit test runs `dags get-tags` through the real argument parser for
all four output formats and fails on `main` with the error above. The command
is also added to the airflow-ctl integration tests, whose example Dags have
tags.
## Testing Done
airflowctl built from `main` at 28cab90ccb and from this branch, each run
against a real Airflow 3.3.2 API server with a fresh SQLite database and two
tagged Dags. The script in the details block starts the server, signs in with
`airflowctl auth token`, and asks for the tags in each output format.
| Output | `main` | This branch |
|---|---|---|
| `json` | `ValueError`, exit 1 | `[{"tags": "etl"}, {"tags": "nightly"},
{"tags": "reporting"}]`, exit 0 |
| `yaml` | `ValueError`, exit 1 | one `- tags:` item per tag, exit 0 |
| `table` | `ValueError`, exit 1 | one tag per line under `tags`, exit 0 |
| `plain` | `ValueError`, exit 1 | one tag per line under `tags`, exit 0 |
Setup, on macOS with Python 3.12:
```bash
# on main at 28cab90ccb, then again on this branch with -o
/tmp/gt-wheels/fix and /tmp/gt-ctl-fix
uv build --package apache-airflow-ctl --wheel -o /tmp/gt-wheels/main
uv venv --python 3.12 /tmp/gt-ctl-main
uv pip install --python /tmp/gt-ctl-main/bin/python
/tmp/gt-wheels/main/apache_airflow_ctl-0.1.5-py3-none-any.whl
# the server
uv venv --python 3.12 /tmp/gt-af332
uv pip install --python /tmp/gt-af332/bin/python "apache-airflow==3.3.2" \
--constraint
https://raw.githubusercontent.com/apache/airflow/constraints-3.3.2/constraints-3.12.txt
```
Before, with airflowctl from `main`:
```
$ bash ./get-tags-e2e.sh /tmp/gt-ctl-main/bin/airflowctl
/tmp/gt-af332/bin/airflow
$ airflow version # the server
3.3.2
$ airflowctl auth login --api-url http://127.0.0.1:58137 --skip-keyring #
AIRFLOW_CLI_TOKEN is set
Login successful! Welcome to airflowctl!
$ curl -s -H "Authorization: Bearer $AIRFLOW_CLI_TOKEN"
http://127.0.0.1:58137/api/v2/dagTags
{"tags":["etl","nightly","reporting"],"total_entries":3}
$ airflowctl dags get-tags -o json
Traceback (most recent call last):
File "/tmp/gt-ctl-main/bin/airflowctl", line 10, in <module>
sys.exit(main())
^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/__main__.py", line
34, in main
safe_call_command(args.func, args=args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 78, in safe_call_command
function(args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/api/client.py", line
563, in wrapper
return func(*args, api_client=api_client, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 933, in _get_func
AirflowConsole().print_as(
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/console_formatting.py",
line 125, in print_as
raise ValueError("To tabulate non-dictionary data you need to provide
`mapper` function")
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
$ airflowctl dags get-tags -o yaml
Traceback (most recent call last):
File "/tmp/gt-ctl-main/bin/airflowctl", line 10, in <module>
sys.exit(main())
^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/__main__.py", line
34, in main
safe_call_command(args.func, args=args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 78, in safe_call_command
function(args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/api/client.py", line
563, in wrapper
return func(*args, api_client=api_client, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 933, in _get_func
AirflowConsole().print_as(
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/console_formatting.py",
line 125, in print_as
raise ValueError("To tabulate non-dictionary data you need to provide
`mapper` function")
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
$ airflowctl dags get-tags -o table
Traceback (most recent call last):
File "/tmp/gt-ctl-main/bin/airflowctl", line 10, in <module>
sys.exit(main())
^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/__main__.py", line
34, in main
safe_call_command(args.func, args=args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 78, in safe_call_command
function(args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/api/client.py", line
563, in wrapper
return func(*args, api_client=api_client, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 933, in _get_func
AirflowConsole().print_as(
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/console_formatting.py",
line 125, in print_as
raise ValueError("To tabulate non-dictionary data you need to provide
`mapper` function")
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
$ airflowctl dags get-tags -o plain
Traceback (most recent call last):
File "/tmp/gt-ctl-main/bin/airflowctl", line 10, in <module>
sys.exit(main())
^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/__main__.py", line
34, in main
safe_call_command(args.func, args=args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 78, in safe_call_command
function(args)
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/api/client.py", line
563, in wrapper
return func(*args, api_client=api_client, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/cli_config.py",
line 933, in _get_func
AirflowConsole().print_as(
File
"/tmp/gt-ctl-main/lib/python3.12/site-packages/airflowctl/ctl/console_formatting.py",
line 125, in print_as
raise ValueError("To tabulate non-dictionary data you need to provide
`mapper` function")
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
```
After, with airflowctl from this branch:
```
$ bash ./get-tags-e2e.sh /tmp/gt-ctl-fix/bin/airflowctl
/tmp/gt-af332/bin/airflow
$ airflow version # the server
3.3.2
$ airflowctl auth login --api-url http://127.0.0.1:58193 --skip-keyring #
AIRFLOW_CLI_TOKEN is set
Login successful! Welcome to airflowctl!
$ curl -s -H "Authorization: Bearer $AIRFLOW_CLI_TOKEN"
http://127.0.0.1:58193/api/v2/dagTags
{"tags":["etl","nightly","reporting"],"total_entries":3}
$ airflowctl dags get-tags -o json
[{"tags": "etl"}, {"tags": "nightly"}, {"tags": "reporting"}]
exit=0
$ airflowctl dags get-tags -o yaml
- tags: etl
- tags: nightly
- tags: reporting
exit=0
$ airflowctl dags get-tags -o table
tags
=========
etl
nightly
reporting
exit=0
$ airflowctl dags get-tags -o plain
tags
etl
nightly
reporting
exit=0
```
The released 0.1.5 from PyPI, installed with `uv pip install --python
/tmp/gt-ctl-pypi/bin/python apache-airflow-ctl==0.1.5`, fails the same way:
```
$ bash ./get-tags-e2e.sh /tmp/gt-ctl-pypi/bin/airflowctl
/tmp/gt-af332/bin/airflow > release-0.1.5.log 2>&1
$ grep -E '^\$ airflowctl dags|^exit=|^ValueError' release-0.1.5.log
$ airflowctl dags get-tags -o json
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
$ airflowctl dags get-tags -o yaml
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
$ airflowctl dags get-tags -o table
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
$ airflowctl dags get-tags -o plain
ValueError: To tabulate non-dictionary data you need to provide `mapper`
function
exit=1
```
<details>
<summary>Reproducer source: get-tags-e2e.sh</summary>
```bash
#!/usr/bin/env bash
# List Dag tags with airflowctl against a real Airflow API server that has
two tagged Dags.
# Usage: get-tags-e2e.sh <airflowctl executable> [airflow executable for the
server]
set -u
CTL=${1:?usage: get-tags-e2e.sh <airflowctl> [airflow]}
AIRFLOW=${2:-airflow}
PORT=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1",
0)); print(s.getsockname()[1])')
URL="http://127.0.0.1:$PORT"
SERVER_HOME="$(mktemp -d)"
CLIENT_HOME="$(mktemp -d)"
SERVER_ENV=(
AIRFLOW_HOME="$SERVER_HOME"
AIRFLOW__CORE__LOAD_EXAMPLES=False
AIRFLOW__CORE__DAGS_FOLDER="$SERVER_HOME/dags"
AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_USERS=admin:admin
)
say() { printf '\n$ %s\n' "$1"; }
mkdir "$SERVER_HOME/dags"
cat > "$SERVER_HOME/dags/tagged.py" <<'EOF'
from airflow.sdk import dag, task
@dag(schedule=None, tags=["etl", "nightly"])
def orders():
@task
def load():
pass
load()
@dag(schedule=None, tags=["etl", "reporting"])
def revenue():
@task
def report():
pass
report()
orders()
revenue()
EOF
env "${SERVER_ENV[@]}" "$AIRFLOW" db migrate > "$SERVER_HOME/setup.log" 2>&1
|| { cat "$SERVER_HOME/setup.log"; exit 1; }
env "${SERVER_ENV[@]}" "$AIRFLOW" dags reserialize >>
"$SERVER_HOME/setup.log" 2>&1 || {
cat "$SERVER_HOME/setup.log"
exit 1
}
env "${SERVER_ENV[@]}" "$AIRFLOW" api-server --port "$PORT" --workers 1 >
"$SERVER_HOME/api-server.log" 2>&1 &
SERVER_PID=$!
trap 'kill "$SERVER_PID" 2>/dev/null; wait "$SERVER_PID" 2>/dev/null; rm -rf
"$SERVER_HOME" "$CLIENT_HOME"' EXIT
PASSWORDS="$SERVER_HOME/simple_auth_manager_passwords.json.generated"
for _ in $(seq 90); do
curl -sf "$URL/api/v2/monitor/health" > /dev/null && [ -s "$PASSWORDS" ]
&& break
sleep 1
done
PASSWORD=$(python3 -c 'import json, sys;
print(json.load(open(sys.argv[1]))["admin"])' "$PASSWORDS") || {
cat "$SERVER_HOME/api-server.log"
exit 1
}
export AIRFLOW_HOME="$CLIENT_HOME"
say 'airflow version # the server'
env "${SERVER_ENV[@]}" "$AIRFLOW" version 2> /dev/null
AIRFLOW_CLI_TOKEN=$("$CTL" auth token --api-url "$URL" --username admin
--password "$PASSWORD")
export AIRFLOW_CLI_TOKEN
say "airflowctl auth login --api-url $URL --skip-keyring #
AIRFLOW_CLI_TOKEN is set"
"$CTL" auth login --api-url "$URL" --skip-keyring
say "curl -s -H \"Authorization: Bearer \$AIRFLOW_CLI_TOKEN\"
$URL/api/v2/dagTags"
curl -s -H "Authorization: Bearer $AIRFLOW_CLI_TOKEN" "$URL/api/v2/dagTags"
echo
for output in json yaml table plain; do
say "airflowctl dags get-tags -o $output"
"$CTL" dags get-tags -o "$output"
echo "exit=$?"
done
```
</details>
---
##### Was generative AI tooling used to co-author this PR?
- [X] Yes, GitHub Copilot CLI (Claude Opus 5.5)
Generated-by: GitHub Copilot CLI (Claude Opus 5.5) following [the
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
---
* Read the **[Pull Request
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)**
for more information. Note: commit author/co-author name and email in commits
become permanently public when merged.
* For fundamental code changes, an Airflow Improvement Proposal
([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
is needed.
* When adding dependency, check compliance with the [ASF 3rd Party License
Policy](https://www.apache.org/legal/resolved.html#category-x).
* For significant user-facing changes create newsfragment:
`{pr_number}.significant.rst`, in
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
You can add this file in a follow-up commit after the PR is created so you
know the PR number.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]