Skip to content

Commit 2ba704a

Browse files
committed
refactor: [AIP-94] airflowctl variables: add list command
Signed-off-by: PoAn Yang <payang@apache.org>
1 parent f0684b0 commit 2ba704a

3 files changed

Lines changed: 64 additions & 89 deletions

File tree

airflow-core/src/airflow/cli/commands/variable_command.py

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525

2626
from sqlalchemy import select
2727

28+
from airflow.cli.api_client import NEW_API_CLIENT, Client, provide_api_client
2829
from airflow.cli.simple_table import AirflowConsole
29-
from airflow.cli.utils import SENSITIVE_PLACEHOLDER, print_export_output
30+
from airflow.cli.utils import SENSITIVE_PLACEHOLDER, deprecated_for_airflowctl, print_export_output
3031
from airflow.exceptions import (
3132
AirflowFileParseException,
3233
AirflowUnsupportedFileTypeException,
@@ -43,29 +44,11 @@
4344
from sqlalchemy.orm.session import Session
4445

4546

46-
class VariableDisplayMapper:
47-
"""Mapper class for formatting variable data for CLI display."""
48-
49-
@staticmethod
50-
def keys_only(var) -> dict[str, str]:
51-
"""Return only variable keys. Accepts Variable model or dict with 'key'."""
52-
key = var.key if hasattr(var, "key") else var["key"]
53-
return {"key": key}
54-
55-
@staticmethod
56-
def with_values(var, hide_sensitive: bool = False) -> dict[str, str]:
57-
"""Return variable with value, optionally masked."""
58-
key = var.key if hasattr(var, "key") else var["key"]
59-
raw = var.val if hasattr(var, "val") else var.get("val", var.get("_val"))
60-
val = "" if raw is None else str(raw)
61-
if hide_sensitive:
62-
val = SENSITIVE_PLACEHOLDER
63-
return {"key": key, "val": val}
64-
65-
47+
@deprecated_for_airflowctl("airflowctl variables list")
6648
@suppress_logs_and_warning
6749
@providers_configuration_loaded
68-
def variables_list(args):
50+
@provide_api_client
51+
def variables_list(args, api_client: Client = NEW_API_CLIENT):
6952
"""
7053
Display all the variables.
7154
@@ -79,17 +62,20 @@ def variables_list(args):
7962
if hide_sensitive and not show_values:
8063
raise SystemExit("--hide-sensitive can only be used with --show-values")
8164

82-
def _mapper(var):
83-
return VariableDisplayMapper.with_values(var, hide_sensitive)
65+
variables = api_client.variables.list().variables
8466

85-
with create_session() as session:
86-
if show_values:
87-
variables = session.scalars(select(Variable)).all()
88-
AirflowConsole().print_as(data=variables, output=args.output, mapper=_mapper)
89-
else:
90-
keys = session.scalars(select(Variable.key).distinct()).all()
91-
variables = [{"key": key} for key in keys]
92-
AirflowConsole().print_as(data=variables, output=args.output, mapper=None)
67+
if show_values:
68+
69+
def _mapper(var):
70+
val = "" if var.value is None else str(var.value)
71+
if hide_sensitive:
72+
val = SENSITIVE_PLACEHOLDER
73+
return {"key": var.key, "val": val}
74+
75+
AirflowConsole().print_as(data=variables, output=args.output, mapper=_mapper)
76+
else:
77+
data = [{"key": var.key} for var in variables]
78+
AirflowConsole().print_as(data=data, output=args.output, mapper=None)
9379

9480

9581
@suppress_logs_and_warning

airflow-core/tests/unit/cli/commands/test_command_deprecations.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
import pytest
3232

33-
from airflow.cli.commands import asset_command, dag_command, pool_command
33+
from airflow.cli.commands import asset_command, dag_command, pool_command, variable_command
3434
from airflow.exceptions import RemovedInAirflow4Warning
3535

3636
# (command callable, argv to parse, expected airflowctl replacement named in the warning)
@@ -52,6 +52,7 @@
5252
["assets", "materialize", "--name=foo"],
5353
"airflowctl assets materialize",
5454
),
55+
(variable_command.variables_list, ["variables", "list"], "airflowctl variables list"),
5556
]
5657

5758

airflow-core/tests/unit/cli/commands/test_variable_command.py

Lines changed: 44 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@
1919

2020
import json
2121
import os
22-
from contextlib import redirect_stdout
23-
from io import StringIO
2422

2523
import pytest
2624
import yaml
25+
from airflowctl.api.datamodels.generated import VariableResponse
2726
from sqlalchemy import select
2827

2928
from airflow import models
@@ -229,101 +228,90 @@ def test_variables_set_different_types(self):
229228

230229
os.remove("variables_types.json")
231230

232-
def test_variables_list(self):
231+
def test_variables_list(self, mock_cli_api_client):
233232
"""Test variable_list command"""
234-
# Test command is received
233+
mock_cli_api_client.variables.list.return_value.variables = []
235234
variable_command.variables_list(self.parser.parse_args(["variables", "list"]))
235+
mock_cli_api_client.variables.list.assert_called_once()
236236

237-
def test_variables_list_show_values(self):
237+
def test_variables_list_show_values(self, mock_cli_api_client, stdout_capture):
238238
"""Test variables list with --show-values flag shows actual values."""
239-
# Create test variables
240-
Variable.set("test_key1", "test_value1")
241-
Variable.set("test_key2", "test_value2")
239+
mock_cli_api_client.variables.list.return_value.variables = [
240+
VariableResponse(key="test_key1", value="test_value1", is_encrypted=False),
241+
VariableResponse(key="test_key2", value="test_value2", is_encrypted=False),
242+
]
242243

243244
args = self.parser.parse_args(["variables", "list", "--output", "json", "--show-values"])
244-
with redirect_stdout(StringIO()) as stdout_io:
245+
with stdout_capture as stdout:
245246
variable_command.variables_list(args)
246-
output = stdout_io.getvalue()
247247

248-
# Parse JSON output and verify values are shown
249-
data = json.loads(output)
250-
assert len(data) >= 2
248+
data = json.loads(stdout.getvalue())
251249
key_value_map = {item["key"]: item["val"] for item in data}
252-
assert "test_value1" in key_value_map["test_key1"]
253-
assert "test_value2" in key_value_map["test_key2"]
250+
assert key_value_map["test_key1"] == "test_value1"
251+
assert key_value_map["test_key2"] == "test_value2"
254252

255-
def test_variables_list_hide_sensitive(self):
253+
def test_variables_list_hide_sensitive(self, mock_cli_api_client, stdout_capture):
256254
"""Test variables list with --hide-sensitive masks all values."""
257-
# Create test variables
258-
Variable.set("test_key1", "test_value1")
259-
Variable.set("test_key2", "test_value2")
255+
mock_cli_api_client.variables.list.return_value.variables = [
256+
VariableResponse(key="test_key1", value="test_value1", is_encrypted=False),
257+
VariableResponse(key="test_key2", value="test_value2", is_encrypted=False),
258+
]
260259

261260
args = self.parser.parse_args(
262261
["variables", "list", "--output", "json", "--show-values", "--hide-sensitive"]
263262
)
264-
with redirect_stdout(StringIO()) as stdout_io:
263+
with stdout_capture as stdout:
265264
variable_command.variables_list(args)
266-
output = stdout_io.getvalue()
267265

268-
# Parse JSON output and verify values are masked
269-
data = json.loads(output)
270-
assert len(data) >= 2
266+
data = json.loads(stdout.getvalue())
267+
assert len(data) == 2
271268
for item in data:
272-
if "test_key" in item["key"]:
273-
assert item["val"] == "***"
269+
assert item["val"] == "***"
274270

275-
def test_variables_list_hide_sensitive_without_show_values_fails(self):
271+
def test_variables_list_hide_sensitive_without_show_values_fails(self, mock_cli_api_client):
276272
"""--hide-sensitive without --show-values should fail."""
277273
args = self.parser.parse_args(["variables", "list", "--hide-sensitive"])
278274
with pytest.raises(SystemExit, match="--hide-sensitive can only be used with --show-values"):
279275
variable_command.variables_list(args)
276+
mock_cli_api_client.variables.list.assert_not_called()
280277

281-
def test_variables_list_default_hides_values(self):
278+
def test_variables_list_default_hides_values(self, mock_cli_api_client, stdout_capture):
282279
"""By default, variables list should only show keys, not values."""
283-
Variable.set("test_key1", "test_value1")
284-
Variable.set("test_key2", "test_value2")
280+
mock_cli_api_client.variables.list.return_value.variables = [
281+
VariableResponse(key="test_key1", value="test_value1", is_encrypted=False),
282+
VariableResponse(key="test_key2", value="test_value2", is_encrypted=False),
283+
]
285284

286285
args = self.parser.parse_args(["variables", "list", "--output", "json"])
287-
with redirect_stdout(StringIO()) as stdout_io:
286+
with stdout_capture as stdout:
288287
variable_command.variables_list(args)
289-
output = stdout_io.getvalue()
290288

291-
data = json.loads(output)
292-
assert len(data) >= 2
289+
data = json.loads(stdout.getvalue())
290+
assert len(data) == 2
293291
for item in data:
294-
if "test_key" in item["key"]:
295-
assert "val" not in item
292+
assert "val" not in item
296293

297-
def test_variables_list_edge_cases(self):
294+
def test_variables_list_edge_cases(self, mock_cli_api_client, stdout_capture):
298295
"""Test variables list with None and empty values."""
299-
Variable.set("empty_var", "")
300-
Variable.set("none_var", None)
301-
Variable.set("normal_var", "normal_value")
296+
mock_cli_api_client.variables.list.return_value.variables = [
297+
VariableResponse(key="empty_var", value="", is_encrypted=False),
298+
VariableResponse(key="none_str_var", value="None", is_encrypted=False),
299+
VariableResponse(key="none_var", value=None, is_encrypted=False),
300+
VariableResponse(key="normal_var", value="normal_value", is_encrypted=False),
301+
]
302302

303303
args = self.parser.parse_args(["variables", "list", "--output", "json", "--show-values"])
304-
with redirect_stdout(StringIO()) as stdout_io:
304+
with stdout_capture as stdout:
305305
variable_command.variables_list(args)
306-
output = stdout_io.getvalue()
307306

308-
data = json.loads(output)
307+
data = json.loads(stdout.getvalue())
309308
key_value_map = {item["key"]: item["val"] for item in data}
310309

311310
assert key_value_map["empty_var"] == ""
312-
assert key_value_map["none_var"] == "None"
311+
assert key_value_map["none_str_var"] == "None"
312+
assert key_value_map["none_var"] == ""
313313
assert key_value_map["normal_var"] == "normal_value"
314314

315-
args = self.parser.parse_args(
316-
["variables", "list", "--output", "json", "--show-values", "--hide-sensitive"]
317-
)
318-
with redirect_stdout(StringIO()) as stdout_io:
319-
variable_command.variables_list(args)
320-
output = stdout_io.getvalue()
321-
322-
data = json.loads(output)
323-
for item in data:
324-
if item["key"] in ["empty_var", "none_var", "normal_var"]:
325-
assert item["val"] == "***"
326-
327315
def test_variables_delete(self):
328316
"""Test variable_delete command"""
329317
variable_command.variables_set(self.parser.parse_args(["variables", "set", "foo", "bar"]))

0 commit comments

Comments
 (0)