Skip to content
Draft
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
42 changes: 26 additions & 16 deletions airflow-core/src/airflow/cli/commands/provider_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import re
import sys

from airflow.cli.api_client import NEW_API_CLIENT, Client, provide_api_client
from airflow.cli.simple_table import AirflowConsole
from airflow.cli.utils import deprecated_for_airflowctl
from airflow.providers_manager import ProvidersManager
from airflow.utils.cli import suppress_logs_and_warning
from airflow.utils.providers_configuration_loader import providers_configuration_loaded
Expand All @@ -33,27 +35,35 @@ def _remove_rst_syntax(value: str) -> str:
return re.sub("[`_<>]", "", value.strip(" \n."))


@deprecated_for_airflowctl("airflowctl providers get")
@suppress_logs_and_warning
@providers_configuration_loaded
def provider_get(args):
@provide_api_client
def provider_get(args, api_client: Client = NEW_API_CLIENT):
"""Get a provider info."""
providers = ProvidersManager().providers
if args.provider_name in providers:
provider_version = providers[args.provider_name].version
provider_info = providers[args.provider_name].data
if args.full:
provider_info["description"] = _remove_rst_syntax(provider_info["description"])
AirflowConsole().print_as(
data=[provider_info],
output=args.output,
)
else:
AirflowConsole().print_as(
data=[{"Provider": args.provider_name, "Version": provider_version}], output=args.output
)
else:
# No single-provider API endpoint exists, so filter the providers collection by name.
providers = {provider.package_name: provider for provider in api_client.providers.list().providers}
provider = providers.get(args.provider_name)
if provider is None:
raise SystemExit(f"No such provider installed: {args.provider_name}")

if args.full:
AirflowConsole().print_as(
data=[
{
"package_name": provider.package_name,
"version": provider.version,
"description": _remove_rst_syntax(provider.description),
"documentation_url": provider.documentation_url,
}
],
output=args.output,
)
else:
AirflowConsole().print_as(
data=[{"Provider": provider.package_name, "Version": provider.version}], output=args.output
)


@suppress_logs_and_warning
@providers_configuration_loaded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

import pytest

from airflow.cli.commands import asset_command, dag_command, pool_command
from airflow.cli.commands import asset_command, dag_command, pool_command, provider_command
from airflow.exceptions import RemovedInAirflow4Warning

# (command callable, argv to parse, expected airflowctl replacement named in the warning)
Expand All @@ -52,6 +52,11 @@
["assets", "materialize", "--name=foo"],
"airflowctl assets materialize",
),
(
provider_command.provider_get,
["providers", "get", "apache-airflow-providers-amazon"],
"airflowctl providers get",
),
]


Expand Down
77 changes: 77 additions & 0 deletions airflow-core/tests/unit/cli/commands/test_provider_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

import json

import pytest
from airflowctl.api.datamodels.generated import ProviderResponse

from airflow.cli import cli_parser
from airflow.cli.commands import provider_command


class TestCliProviderGet:
@classmethod
def setup_class(cls):
cls.parser = cli_parser.get_parser()

@staticmethod
def _providers() -> list[ProviderResponse]:
return [
ProviderResponse(
package_name="apache-airflow-providers-amazon",
description="Amazon provider",
version="9.0.0",
documentation_url="https://example.com",
)
]

def test_provider_get(self, mock_cli_api_client, stdout_capture):
mock_cli_api_client.providers.list.return_value.providers = self._providers()
with stdout_capture as stdout:
provider_command.provider_get(
self.parser.parse_args(
["providers", "get", "apache-airflow-providers-amazon", "--output", "json"]
)
)
assert json.loads(stdout.getvalue()) == [
{"Provider": "apache-airflow-providers-amazon", "Version": "9.0.0"}
]

def test_provider_get_full(self, mock_cli_api_client, stdout_capture):
mock_cli_api_client.providers.list.return_value.providers = self._providers()
with stdout_capture as stdout:
provider_command.provider_get(
self.parser.parse_args(
["providers", "get", "apache-airflow-providers-amazon", "--full", "--output", "json"]
)
)
assert json.loads(stdout.getvalue()) == [
{
"package_name": "apache-airflow-providers-amazon",
"version": "9.0.0",
"description": "Amazon provider",
"documentation_url": "https://example.com",
}
]

def test_provider_get_not_found(self, mock_cli_api_client):
mock_cli_api_client.providers.list.return_value.providers = self._providers()
with pytest.raises(SystemExit, match="No such provider installed: does-not-exist"):
provider_command.provider_get(self.parser.parse_args(["providers", "get", "does-not-exist"]))
2 changes: 1 addition & 1 deletion airflow-ctl/docs/images/command_hashes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dags:6b38e6bcd491bc1941e7814b77e63bde
dagrun:c32e0011aa9a845456c778786717208e
jobs:a5b644c5da8889443bb40ee10b599270
pools:19efe105b9515ab1926ebcaf0e028d71
providers:34502fe09dc0b8b0a13e7e46efdffda6
providers:532e07c3c11c6100a753e23a921efabd
variables:f8fc76d3d398b2780f4e97f7cd816646
version:31f4efdf8de0dbaaa4fac71ff7efecc3
plugins:4864fd8f356704bd2b3cd1aec3567e35
Expand Down
66 changes: 35 additions & 31 deletions airflow-ctl/docs/images/output_providers.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions airflow-ctl/src/airflowctl/ctl/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@ def _load_help_texts_yaml() -> dict[str, dict[str, str]]:
action="store_true",
)

# Providers command args
ARG_PROVIDER_NAME = Arg(
flags=("provider_name",), type=str, help="Provider package name, e.g. apache-airflow-providers-amazon"
)
ARG_PROVIDER_FULL = Arg(
flags=("-f", "--full"),
action="store_true",
help="Show full provider information instead of just the version.",
)


class ActionCommand(NamedTuple):
"""Single CLI command."""
Expand Down Expand Up @@ -1015,6 +1025,15 @@ def merge_commands(
),
)

PROVIDER_COMMANDS = (
ActionCommand(
name="get",
help="Get information about a single provider.",
func=lazy_load_command("airflowctl.ctl.commands.provider_command.get"),
args=(ARG_PROVIDER_NAME, ARG_PROVIDER_FULL, ARG_OUTPUT),
),
)

core_commands: list[CLICommand] = [
GroupCommand(
name="auth",
Expand Down Expand Up @@ -1042,6 +1061,11 @@ def merge_commands(
help="Manage Airflow pools",
subcommands=POOL_COMMANDS,
),
GroupCommand(
name="providers",
help="Manage Airflow providers",
subcommands=PROVIDER_COMMANDS,
),
ActionCommand(
name="version",
help="Show version information",
Expand Down
Loading
Loading