Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,75 +54,75 @@ def date_param():
"auth list-envs",
# Assets commands
"assets list",
"assets get --asset-id=1",
"assets get 1",
"assets create-event --asset-id=1",
# Backfill commands
"backfill list",
"backfill list example_bash_operator",
# Config commands
"config get --section core --option executor",
"config get core executor",
"config list",
"config lint",
# Connections commands
"connections create --connection-id=test_con --conn-type=mysql --password=TEST_PASS -o json",
"connections list",
"connections list -o yaml",
"connections list -o table",
"connections get --conn-id=test_con",
"connections get --conn-id=test_con -o json",
"connections get test_con",
"connections get test_con -o json",
"connections update --connection-id=test_con --conn-type=postgres",
"connections import tests/airflowctl_tests/fixtures/test_connections.json",
"connections delete --conn-id=test_con",
"connections delete --conn-id=test_import_conn",
"connections delete test_con",
"connections delete test_import_conn",
# Dags commands
"dags list",
"dags get --dag-id=example_bash_operator",
"dags get-details --dag-id=example_bash_operator",
"dags get-stats --dag-ids=example_bash_operator",
"dags get-version --dag-id=example_bash_operator --version-number=1",
"dags get example_bash_operator",
"dags get-details example_bash_operator",
"dags get-stats example_bash_operator",
"dags get-version example_bash_operator 1",
"dags list-import-errors",
"dags list-version --dag-id=example_bash_operator",
"dags list-version example_bash_operator",
"dags list-warning",
# Order of trigger and pause/unpause is important for test stability because state checked
"dags trigger --dag-id=example_bash_operator --logical-date={date_param} --run-after={date_param}",
"dags trigger example_bash_operator --logical-date={date_param} --run-after={date_param}",
# Test trigger without logical-date (should default to now)
"dags trigger --dag-id=example_bash_operator",
"dags trigger example_bash_operator",
"dags pause example_bash_operator",
"dags unpause example_bash_operator",
# Dag Run commands
'dagrun get --dag-id=example_bash_operator --dag-run-id="manual__{date_param}"',
"dags update --dag-id=example_bash_operator --no-is-paused",
'dagrun get example_bash_operator "manual__{date_param}"',
"dags update example_bash_operator --no-is-paused",
# Dag Run commands
"dagrun list --dag-id example_bash_operator --state success --limit=1",
# XCom commands - need a Dag run with completed tasks
'xcom add --dag-id=example_bash_operator --dag-run-id="manual__{date_param}" --task-id=runme_0 --key={xcom_key} --value=\'{{"test": "value"}}\'',
'xcom get --dag-id=example_bash_operator --dag-run-id="manual__{date_param}" --task-id=runme_0 --key={xcom_key}',
'xcom list --dag-id=example_bash_operator --dag-run-id="manual__{date_param}" --task-id=runme_0',
'xcom edit --dag-id=example_bash_operator --dag-run-id="manual__{date_param}" --task-id=runme_0 --key={xcom_key} --value=\'{{"updated": "value"}}\'',
'xcom delete --dag-id=example_bash_operator --dag-run-id="manual__{date_param}" --task-id=runme_0 --key={xcom_key}',
'xcom add example_bash_operator "manual__{date_param}" runme_0 {xcom_key} \'{{"test": "value"}}\'',
'xcom get example_bash_operator "manual__{date_param}" runme_0 {xcom_key}',
'xcom list example_bash_operator "manual__{date_param}" runme_0',
'xcom edit example_bash_operator "manual__{date_param}" runme_0 {xcom_key} \'{{"updated": "value"}}\'',
'xcom delete example_bash_operator "manual__{date_param}" runme_0 {xcom_key}',
# Jobs commands
"jobs list",
# Pools commands
"pools create --name=test_pool --slots=5",
"pools list",
"pools get --pool-name=test_pool",
"pools get --pool-name=test_pool -o yaml",
"pools get test_pool",
"pools get test_pool -o yaml",
"pools update --pool=test_pool --slots=10",
"pools import tests/airflowctl_tests/fixtures/test_pools.json",
"pools export tests/airflowctl_tests/fixtures/pools_export.json --output=json",
"pools delete --pool=test_pool",
"pools delete --pool=test_import_pool",
"pools delete test_pool",
"pools delete test_import_pool",
# Providers commands
"providers list",
# Variables commands
"variables create --key=test_key --value=test_value",
"variables list",
"variables get --variable-key=test_key",
"variables get --variable-key=test_key -o table",
"variables get test_key",
"variables get test_key -o table",
"variables update --key=test_key --value=updated_value",
"variables import tests/airflowctl_tests/fixtures/test_variables.json",
"variables delete --variable-key=test_key",
"variables delete --variable-key=test_import_var",
"variables delete --variable-key=test_import_var_with_desc",
"variables delete test_key",
"variables delete test_import_var",
"variables delete test_import_var_with_desc",
# Plugins command
"plugins list",
"plugins list-import-errors",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
# Test that config list shows masked sensitive values
"config list",
# Test that getting specific sensitive config values are masked
"config get --section core --option fernet_key",
"config get --section database --option sql_alchemy_conn",
"config get core fernet_key",
"config get database sql_alchemy_conn",
]


Expand Down
11 changes: 11 additions & 0 deletions airflow-ctl/RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
specific language governing permissions and limitations
under the License.

airflowctl unreleased
---------------------

Significant Changes
^^^^^^^^^^^^^^^^^^^

- Expose required primitive parameters of auto-generated commands as positional
arguments instead of ``--flag`` options. Optional parameters keep the
``--flag`` form. Follows the dev-list lazy consensus on airflowctl parameter
style (see `<https://lists.apache.org/thread/m1qvcvow3l17ytv40vhslh40wn3rntrm>`_).

airflowctl 0.1.4 (2026-04-18)
-----------------------------

Expand Down
75 changes: 64 additions & 11 deletions airflow-ctl/src/airflowctl/ctl/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,11 +423,19 @@ def get_function_details(node: ast.FunctionDef, parent_node: ast.ClassDef) -> di
args = []
return_annotation: str = ""

for arg in node.args.args:
# In ``ast.arguments``, ``defaults`` aligns with the *tail* of
# ``args``. A parameter is required when its position from the
# left is *before* the first defaulted position. Equivalent to
# ``len(args) - len(defaults)``.
positional_args = [a for a in node.args.args if a.arg != "self"]
defaults_count = len(node.args.defaults)
required_count = len(positional_args) - defaults_count
required_param_names: set[str] = {a.arg for a in positional_args[:required_count]}

for arg in positional_args:
arg_name = arg.arg
arg_type = ast.unparse(arg.annotation) if arg.annotation else "Any"
if arg_name != "self":
args.append({arg_name: arg_type})
args.append({arg_name: arg_type})

if node.returns:
return_annotation = [
Expand All @@ -437,6 +445,7 @@ def get_function_details(node: ast.FunctionDef, parent_node: ast.ClassDef) -> di
return {
"name": func_name,
"parameters": args,
"required_param_names": required_param_names,
"return_type": return_annotation,
"parent": parent_node,
}
Expand Down Expand Up @@ -532,6 +541,26 @@ def _create_arg(
action=arg_action,
)

@staticmethod
def _create_positional_arg(
parameter_key: str,
arg_type: type | Callable,
arg_help: str,
) -> Arg:
"""
Build a positional ``Arg`` for a required primitive parameter.

``argparse`` rejects ``default`` and ``dest`` on positional arguments,
so this helper keeps both unset and uses the raw parameter name (with
underscores) as the flag so the parsed ``Namespace`` attribute lines up
with the operation method's signature.
"""
return Arg(
flags=(parameter_key,),
type=arg_type,
help=arg_help,
)

def _create_arg_for_non_primitive_type(
self,
parameter_type: str,
Expand Down Expand Up @@ -577,20 +606,44 @@ def _create_args_map_from_operation(self):
"""Create Arg from Operation Method checking for parameters and return types."""
for operation in self.operations:
args = []
required_names: set[str] = operation.get("required_param_names") or set()
for parameter in operation.get("parameters"):
for parameter_key, parameter_type in parameter.items():
if self._is_primitive_type(type_name=parameter_type):
base_parameter_type = parameter_type.replace(" | None", "").strip()
is_bool = base_parameter_type == "bool"
args.append(
self._create_arg(
arg_flags=("--" + self._sanitize_arg_parameter_key(parameter_key),),
arg_type=self._python_type_from_string(parameter_type),
arg_action=argparse.BooleanOptionalAction if is_bool else None,
arg_help=f"{parameter_key} for {operation.get('name')} operation in {operation.get('parent').name}",
arg_default=None,
)
# Required, non-bool primitives are exposed as positional
# arguments per the dev-list lazy consensus
# (https://lists.apache.org/thread/m1qvcvow3l17ytv40vhslh40wn3rntrm).
# Bool stays --flag/--no-flag and ``parameter_type``
# ending in ``| None`` is treated as optional.
is_required_positional = (
parameter_key in required_names and not is_bool and "| None" not in parameter_type
)
if is_required_positional:
args.append(
self._create_positional_arg(
parameter_key=parameter_key,
arg_type=self._python_type_from_string(parameter_type),
arg_help=(
f"{parameter_key} for {operation.get('name')} "
f"operation in {operation.get('parent').name}"
),
)
)
else:
args.append(
self._create_arg(
arg_flags=("--" + self._sanitize_arg_parameter_key(parameter_key),),
arg_type=self._python_type_from_string(parameter_type),
arg_action=argparse.BooleanOptionalAction if is_bool else None,
arg_help=(
f"{parameter_key} for {operation.get('name')} "
f"operation in {operation.get('parent').name}"
),
arg_default=None,
)
)
else:
args.extend(
self._create_arg_for_non_primitive_type(
Expand Down
Loading
Loading