From ef422ff24b2cfe45bdf14c753de271db1c497de2 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Sun, 26 Apr 2026 22:55:43 +0200 Subject: [PATCH 01/10] [airflow-ctl-v0-1-test] Allow remote version check without authentication (#65099) (#65904) * fix(cli): allow remote version check without local config file * fix(cli): introduce NO_AUTH client * test(cli): add unit test for no-auth commands (cherry picked from commit 80cefdeff0358cbae67cd66bf5a0c20cb7e9d550) Co-authored-by: rjgoyln <151457491+rjgoyln@users.noreply.github.com> --- .../test_airflowctl_commands.py | 21 ++++++++- airflow-ctl/src/airflowctl/api/client.py | 44 +++++++++++++------ .../ctl/commands/version_command.py | 2 +- .../tests/airflow_ctl/api/test_client.py | 40 ++++++++++++++++- .../ctl/commands/test_version_command.py | 9 +++- 5 files changed, 98 insertions(+), 18 deletions(-) diff --git a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py index 5215c2946cb10..1f893c37efd04 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py @@ -51,6 +51,7 @@ def date_param(): TEST_COMMANDS = [ # Auth commands f"auth token {CREDENTIAL_SUFFIX}", + "auth list-envs", # Assets commands "assets list", "assets get --asset-id=1", @@ -122,13 +123,15 @@ def date_param(): "variables delete --variable-key=test_key", "variables delete --variable-key=test_import_var", "variables delete --variable-key=test_import_var_with_desc", - # Version command - "version --remote", # Plugins command "plugins list", "plugins list-import-errors", ] +NO_AUTH_TEST_COMMANDS = [ + "version --remote", +] + DATE_PARAM_1 = date_param() DATE_PARAM_2 = date_param() @@ -189,3 +192,17 @@ def test_airflowctl_commands_skip_keyring(command: str, api_token: str, run_comm env_vars["AIRFLOW_CLI_ENVIRONMENT"] = "nokeyring" run_command(command, env_vars, skip_login=True) + + +@pytest.mark.parametrize("command", NO_AUTH_TEST_COMMANDS) +def test_airflowctl_no_auth_commands(command: str, run_command, tmp_path): + """Test airflowctl no-auth commands without login or persisted credentials.""" + run_command( + command=command, + env_vars={ + "AIRFLOW_HOME": str(tmp_path), + "AIRFLOW_CLI_ENVIRONMENT": "no-auth", + "AIRFLOW_CLI_DEBUG_MODE": "false", + }, + skip_login=True, + ) diff --git a/airflow-ctl/src/airflowctl/api/client.py b/airflow-ctl/src/airflowctl/api/client.py index f3ca3f673f119..e4c5ddf2302d6 100644 --- a/airflow-ctl/src/airflowctl/api/client.py +++ b/airflow-ctl/src/airflowctl/api/client.py @@ -98,6 +98,7 @@ class ClientKind(enum.Enum): CLI = "cli" AUTH = "auth" + NO_AUTH = "no_auth" def add_correlation_id(request: httpx.Request): @@ -253,6 +254,8 @@ def load(self) -> Credentials: with open(config_path) as f: credentials = json.load(f) self.api_url = credentials["api_url"] + if self.client_kind == ClientKind.NO_AUTH: + return self if self.api_token is not None: return self if os.getenv("AIRFLOW_CLI_DEBUG_MODE") == "true": @@ -294,16 +297,17 @@ def load(self) -> Credentials: raise AirflowCtlKeyringException("Keyring backend is not available") from e self.api_token = None except FileNotFoundError: - # This is expected during the auth login command - if self.client_kind != ClientKind.AUTH: + # This is expected during the auth login command. + # Also allow token-only usage without local config (for commands like `version --remote`). + if self.client_kind not in (ClientKind.AUTH, ClientKind.NO_AUTH) and self.api_token is None: raise AirflowCtlCredentialNotFoundException("No credentials file found. Please login first.") return self class BearerAuth(httpx.Auth): - def __init__(self, token: str): - self.token: str = token + def __init__(self, token: str | None): + self.token: str | None = token def auth_flow(self, request: httpx.Request): if self.token: @@ -332,11 +336,13 @@ def __init__( self, *, base_url: str, - token: str, - kind: Literal[ClientKind.CLI, ClientKind.AUTH] = ClientKind.CLI, + token: str | None = None, + kind: Literal[ClientKind.CLI, ClientKind.AUTH, ClientKind.NO_AUTH] = ClientKind.CLI, **kwargs: Any, ) -> None: - auth = BearerAuth(token) + auth: httpx.Auth | None = None + if kind != ClientKind.NO_AUTH: + auth = BearerAuth(token) kwargs["base_url"] = self._get_base_url(base_url=base_url, kind=kind) pyver = f"{'.'.join(map(str, sys.version_info[:3]))}" super().__init__( @@ -347,14 +353,18 @@ def __init__( ) def refresh_base_url( - self, base_url: str, kind: Literal[ClientKind.AUTH, ClientKind.CLI] = ClientKind.CLI + self, + base_url: str, + kind: Literal[ClientKind.AUTH, ClientKind.CLI, ClientKind.NO_AUTH] = ClientKind.CLI, ): """Refresh the base URL of the client.""" self.base_url = URL(self._get_base_url(base_url=base_url, kind=kind)) @classmethod def _get_base_url( - cls, base_url: str, kind: Literal[ClientKind.AUTH, ClientKind.CLI] = ClientKind.CLI + cls, + base_url: str, + kind: Literal[ClientKind.AUTH, ClientKind.CLI, ClientKind.NO_AUTH] = ClientKind.CLI, ) -> str: """Get the base URL of the client.""" base_url = base_url.rstrip("/") @@ -466,7 +476,10 @@ def plugins(self): # API Client Decorator for CLI Actions @contextlib.contextmanager -def get_client(kind: Literal[ClientKind.CLI, ClientKind.AUTH] = ClientKind.CLI, api_token: str | None = None): +def get_client( + kind: Literal[ClientKind.CLI, ClientKind.AUTH, ClientKind.NO_AUTH] = ClientKind.CLI, + api_token: str | None = None, +): """ Get CLI API client. @@ -476,11 +489,16 @@ def get_client(kind: Literal[ClientKind.CLI, ClientKind.AUTH] = ClientKind.CLI, api_token = api_token or os.getenv("AIRFLOW_CLI_TOKEN", None) try: # API URL always loaded from the config file, please save with it if you are using other than ClientKind.CLI - credentials = Credentials(client_kind=kind, api_token=api_token).load() + if kind == ClientKind.NO_AUTH: + credentials = Credentials(client_kind=kind).load() + resolved_token = None + else: + credentials = Credentials(client_kind=kind, api_token=api_token).load() + resolved_token = api_token or credentials.api_token api_client = Client( base_url=credentials.api_url or "http://localhost:8080", limits=httpx.Limits(max_keepalive_connections=1, max_connections=1), - token=str(api_token or credentials.api_token), + token=resolved_token, kind=kind, ) yield api_client @@ -492,7 +510,7 @@ def get_client(kind: Literal[ClientKind.CLI, ClientKind.AUTH] = ClientKind.CLI, def provide_api_client( - kind: Literal[ClientKind.CLI, ClientKind.AUTH] = ClientKind.CLI, + kind: Literal[ClientKind.CLI, ClientKind.AUTH, ClientKind.NO_AUTH] = ClientKind.CLI, ) -> Callable[[Callable[PS, RT]], Callable[PS, RT]]: """ Provide a CLI API Client to the decorated function. diff --git a/airflow-ctl/src/airflowctl/ctl/commands/version_command.py b/airflow-ctl/src/airflowctl/ctl/commands/version_command.py index d1f9034e03362..42be64980ef89 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/version_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/version_command.py @@ -26,7 +26,7 @@ def version_info(arg): """Get version information.""" version_dict = {"airflowctl_version": airflowctl_version} if arg.remote: - with get_client(kind=ClientKind.CLI, api_token=getattr(arg, "api_token", None)) as api_client: + with get_client(kind=ClientKind.NO_AUTH) as api_client: version_response = api_client.version.get() version_dict.update(version_response.model_dump()) rich.print(version_dict) diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py b/airflow-ctl/tests/airflow_ctl/api/test_client.py index f2d216fcc82fb..56cab7f7a8edc 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_client.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py @@ -28,7 +28,7 @@ import time_machine from httpx import URL -from airflowctl.api.client import Client, ClientKind, Credentials, _bounded_get_new_password +from airflowctl.api.client import Client, ClientKind, Credentials, _bounded_get_new_password, get_client from airflowctl.api.operations import ServerResponseError from airflowctl.exceptions import ( AirflowCtlCredentialNotFoundException, @@ -105,12 +105,16 @@ def handle_request(request: httpx.Request) -> httpx.Response: [ ("http://localhost:8080", ClientKind.CLI, "http://localhost:8080/api/v2/"), ("http://localhost:8080", ClientKind.AUTH, "http://localhost:8080/auth/"), + ("http://localhost:8080", ClientKind.NO_AUTH, "http://localhost:8080/api/v2/"), ("https://example.com", ClientKind.CLI, "https://example.com/api/v2/"), ("https://example.com", ClientKind.AUTH, "https://example.com/auth/"), + ("https://example.com", ClientKind.NO_AUTH, "https://example.com/api/v2/"), ("http://localhost:8080/", ClientKind.CLI, "http://localhost:8080/api/v2/"), ("http://localhost:8080/", ClientKind.AUTH, "http://localhost:8080/auth/"), + ("http://localhost:8080/", ClientKind.NO_AUTH, "http://localhost:8080/api/v2/"), ("https://example.com/", ClientKind.CLI, "https://example.com/api/v2/"), ("https://example.com/", ClientKind.AUTH, "https://example.com/auth/"), + ("https://example.com/", ClientKind.NO_AUTH, "https://example.com/api/v2/"), ], ) def test_refresh_base_url(self, base_url, client_kind, expected_base_url): @@ -214,6 +218,25 @@ def test_load_no_credentials(self, mock_keyring): assert not os.path.exists(config_dir) + @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_NO_CONFIG_WITH_EXPLICIT_TOKEN"}) + @patch("airflowctl.api.client.keyring") + def test_load_no_config_with_explicit_token(self, mock_keyring): + cli_client = ClientKind.CLI + credentials = Credentials(client_kind=cli_client, api_token="TEST_TOKEN").load() + + assert credentials.api_token == "TEST_TOKEN" + assert credentials.api_url is None + mock_keyring.get_password.assert_not_called() + + @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_NO_CONFIG_NO_AUTH"}) + @patch("airflowctl.api.client.keyring") + def test_load_no_config_no_auth_kind(self, mock_keyring): + credentials = Credentials(client_kind=ClientKind.NO_AUTH).load() + + assert credentials.api_token is None + assert credentials.api_url is None + mock_keyring.get_password.assert_not_called() + @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_KEYRING_VALUE_ERROR"}) @patch("airflowctl.api.client.keyring") def test_load_incorrect_keyring_password(self, mock_keyring): @@ -400,6 +423,21 @@ def test_credentials_accepts_safe_env(): assert creds.api_environment == "prod-us_1" +@patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_GET_CLIENT_WITH_TOKEN_ONLY"}) +def test_get_client_allows_explicit_token_without_config(): + with get_client(kind=ClientKind.CLI, api_token="TEST_TOKEN") as client: + assert str(client.base_url) == "http://localhost:8080/api/v2/" + + +@patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_GET_CLIENT_NO_AUTH_WITHOUT_CONFIG"}) +@patch("airflowctl.api.client.keyring") +def test_get_client_no_auth_without_config(mock_keyring): + with get_client(kind=ClientKind.NO_AUTH) as client: + assert str(client.base_url) == "http://localhost:8080/api/v2/" + + mock_keyring.get_password.assert_not_called() + + @pytest.mark.parametrize("api_environment", ["../evil", "..\\evil", "a/b", "a\\b"]) def test_credentials_rejects_unsafe_env_argument(api_environment): with pytest.raises(AirflowCtlException, match="environment"): diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py index e6b20ed7fa5d4..6168e874d6061 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py @@ -22,7 +22,7 @@ import pytest -from airflowctl.api.client import Client +from airflowctl.api.client import Client, ClientKind from airflowctl.ctl import cli_parser from airflowctl.ctl.commands.version_command import version_info @@ -52,6 +52,13 @@ def test_ctl_version_remote(self, mock_client): assert "version" in stdout.getvalue() assert "git_version" in stdout.getvalue() assert "airflowctl_version" in stdout.getvalue() + mock_get_client.assert_called_once_with(kind=ClientKind.NO_AUTH) + + def test_ctl_version_remote_with_api_token(self, mock_client): + with mock.patch("airflowctl.ctl.commands.version_command.get_client") as mock_get_client: + mock_get_client.return_value.__enter__.return_value = mock_client + version_info(self.parser.parse_args(["version", "--remote", "--api-token", "TOKEN"])) + mock_get_client.assert_called_once_with(kind=ClientKind.NO_AUTH) def test_ctl_version_only_local_version(self, mock_client): """Test the version command without --remote does not touch credentials.""" From c22b0906a3faa08e9c84eb975bb4067984a7e7fb Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Sun, 26 Apr 2026 22:57:36 +0200 Subject: [PATCH 02/10] [airflow-ctl-v0-1-test] use existing safe_load function in airflowctl utils to load help texts (#65841) (#65903) (cherry picked from commit ad213e0) Co-authored-by: Justin Pakzad <114518232+justinpakzad@users.noreply.github.com> --- airflow-ctl/src/airflowctl/ctl/cli_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 466ee671b61b2..c540b939fbf75 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -35,12 +35,12 @@ import httpx import rich -import yaml import airflowctl.api.datamodels.generated as generated_datamodels from airflowctl.api.client import NEW_API_CLIENT, Client, ClientKind, provide_api_client from airflowctl.api.operations import BaseOperations, ServerResponseError from airflowctl.ctl.console_formatting import AirflowConsole +from airflowctl.ctl.utils.yaml import safe_load from airflowctl.exceptions import ( AirflowCtlConnectionException, AirflowCtlCredentialNotFoundException, @@ -199,7 +199,7 @@ def _load_help_texts_yaml() -> dict[str, dict[str, str]]: """Load the help texts yaml for the auto-generated commands.""" help_texts_path = Path(__file__).parent / "help_texts.yaml" with open(help_texts_path) as yaml_file: - help_texts = yaml.safe_load(yaml_file) + help_texts = safe_load(yaml_file) return help_texts From 8c6bb32c42bea44036b134db96ae36921d694e12 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Mon, 27 Apr 2026 11:02:28 +0200 Subject: [PATCH 03/10] [airflow-ctl-v0-1-test] airflowctl: Send backfill create and dry-run payloads as JSON (#65158) (#65937) (cherry picked from commit ec8977cc2c562b3b3a4feb81041bb572a19183ea) Co-authored-by: Henry Chen --- airflow-ctl/src/airflowctl/api/operations.py | 4 ++-- airflow-ctl/tests/airflow_ctl/api/test_operations.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index a87683468cddf..7718c1c56498d 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -354,7 +354,7 @@ def create(self, backfill: BackfillPostBody) -> BackfillResponse | ServerRespons """Create a backfill.""" try: self.response = self.client.post( - "backfills", data=backfill.model_dump(mode="json", exclude_none=True) + "backfills", json=backfill.model_dump(mode="json", exclude_none=True) ) return BackfillResponse.model_validate_json(self.response.content) except ServerResponseError as e: @@ -364,7 +364,7 @@ def create_dry_run(self, backfill: BackfillPostBody) -> BackfillResponse | Serve """Create a dry run backfill.""" try: self.response = self.client.post( - "backfills/dry_run", data=backfill.model_dump(mode="json", exclude_none=True) + "backfills/dry_run", json=backfill.model_dump(mode="json", exclude_none=True) ) return BackfillResponse.model_validate_json(self.response.content) except ServerResponseError as e: diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py b/airflow-ctl/tests/airflow_ctl/api/test_operations.py index efb430ccc532c..d01f70eedfea7 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py @@ -463,8 +463,12 @@ class TestBackfillOperations: ) def test_create(self): + expected_body = self.backfill_body.model_dump(mode="json", exclude_none=True) + def handle_request(request: httpx.Request) -> httpx.Response: assert request.url.path == "/api/v2/backfills" + assert request.headers.get("content-type", "").startswith("application/json") + assert json.loads(request.content.decode()) == expected_body return httpx.Response(200, json=json.loads(self.backfill_response.model_dump_json())) client = make_api_client(transport=httpx.MockTransport(handle_request)) @@ -472,8 +476,12 @@ def handle_request(request: httpx.Request) -> httpx.Response: assert response == self.backfill_response def test_create_dry_run(self): + expected_body = self.backfill_body.model_dump(mode="json", exclude_none=True) + def handle_request(request: httpx.Request) -> httpx.Response: assert request.url.path == "/api/v2/backfills/dry_run" + assert request.headers.get("content-type", "").startswith("application/json") + assert json.loads(request.content.decode()) == expected_body return httpx.Response(200, json=json.loads(self.backfill_response.model_dump_json())) client = make_api_client(transport=httpx.MockTransport(handle_request)) From cb1426c31f07b693ea2882bb2b663f4d3348beeb Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Wed, 29 Apr 2026 00:23:18 +0200 Subject: [PATCH 04/10] Bump uv floor to 0.11.8, override cooldown for uv (#66042) (#66057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pins `[tool.uv] required-version` (and the matching marker-tagged constants in breeze) to 0.11.8 across the root and the three workspace members that carried their own (stale) floor (`airflow-core`, `airflow-ctl`, `dev/mypy` — all previously `>=0.6.3`). The bump picks up the timestamp-elision fix from astral-sh/uv#19022 (closes astral-sh/uv#18708 — relative `exclude-newer` no longer writes a churning timestamp into uv.lock that two branches collide on). Drops uv's per-package cooldown to "12 hours" in both [tool.uv.exclude-newer-package] and [tool.uv.pip.exclude-newer-package]; without the override the project-wide 4-day window blocks a freshly-released uv from being adopted as the floor. The override is flagged "REMOVE BY 2026-05-01" — once 0.11.8 is older than the global 4-day cooldown the override is redundant. Bumps AIRFLOW_UV_VERSION across Dockerfiles, breeze constants, and the image-args doc to match. The `# sync-uv-min-version`-tagged test fixtures were auto-rewritten by the prek hook of the same name. uv.lock confirms the upstream fix is engaged: `exclude-newer` reads as the no-op `0001-01-01T00:00:00Z` placeholder. (cherry picked from commit dbfc27de982226cb325c7c6d4cb2ccfab946b6c5) --- Dockerfile | 2 +- Dockerfile.ci | 2 +- airflow-core/pyproject.toml | 2 +- airflow-ctl/pyproject.toml | 2 +- dev/breeze/doc/ci/02_images.md | 2 +- .../commands/release_management_commands.py | 2 +- .../src/airflow_breeze/global_constants.py | 2 +- pyproject.toml | 26 +++++++++- uv.lock | 51 ++++++++++--------- 9 files changed, 57 insertions(+), 34 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5562e2586a26c..7cc18dbd44158 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ ARG PYTHON_LTO="true" # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` ARG AIRFLOW_PIP_VERSION=26.0.1 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.11.7 +ARG AIRFLOW_UV_VERSION=0.11.8 ARG AIRFLOW_USE_UV="false" ARG AIRFLOW_IMAGE_REPOSITORY="https://github.com/apache/airflow" ARG AIRFLOW_IMAGE_README_URL="https://raw.githubusercontent.com/apache/airflow/main/docs/docker-stack/README.md" diff --git a/Dockerfile.ci b/Dockerfile.ci index aa92494945b12..da6e4b746175f 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1828,7 +1828,7 @@ COPY --from=scripts common.sh install_packaging_tools.sh install_additional_depe # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` ARG AIRFLOW_PIP_VERSION=26.0.1 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.11.7 +ARG AIRFLOW_UV_VERSION=0.11.8 ARG AIRFLOW_PREK_VERSION="0.3.9" # UV_LINK_MODE=copy is needed since we are using cache mounted from the host diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml index bb8b250c159a6..4c0e33d62c665 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -314,7 +314,7 @@ docs = [ [tool.uv] -required-version = ">=0.6.3" +required-version = ">=0.11.8" [tool.uv.sources] apache-airflow-core = {workspace = true} diff --git a/airflow-ctl/pyproject.toml b/airflow-ctl/pyproject.toml index 41d4e86442275..9aeaf44d31b8d 100644 --- a/airflow-ctl/pyproject.toml +++ b/airflow-ctl/pyproject.toml @@ -215,7 +215,7 @@ tmp_path_retention_count = "2" tmp_path_retention_policy = "failed" [tool.uv] -required-version = ">=0.6.3" +required-version = ">=0.11.8" [tool.uv.sources] apache-airflow-devel-common = { workspace = true } diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md index d008b73fd62c1..8ea8acd989773 100644 --- a/dev/breeze/doc/ci/02_images.md +++ b/dev/breeze/doc/ci/02_images.md @@ -443,7 +443,7 @@ can be used for CI images: | `ADDITIONAL_DEV_APT_DEPS` | | Additional apt dev dependencies installed in the first part of the image | | `ADDITIONAL_DEV_APT_ENV` | | Additional env variables defined when installing dev deps | | `AIRFLOW_PIP_VERSION` | `26.0.1` | `pip` version used. | -| `AIRFLOW_UV_VERSION` | `0.11.7` | `uv` version used. | +| `AIRFLOW_UV_VERSION` | `0.11.8` | `uv` version used. | | `AIRFLOW_PREK_VERSION` | `0.3.9` | `prek` version used. | | `AIRFLOW_USE_UV` | `true` | Whether to use UV for installation. | | `PIP_PROGRESS_BAR` | `on` | Progress bar for PIP installation | diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py index e1188d0152e31..001ae2c868c93 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py @@ -260,7 +260,7 @@ class VersionedFile(NamedTuple): AIRFLOW_PIP_VERSION = "26.0.1" -AIRFLOW_UV_VERSION = "0.11.7" +AIRFLOW_UV_VERSION = "0.11.8" AIRFLOW_USE_UV = False GITPYTHON_VERSION = "3.1.46" RICH_VERSION = "15.0.0" diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 060cb2dccf6b6..0fd522c3565a1 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -281,7 +281,7 @@ def get_allowed_llm_models() -> list[str]: ALLOWED_INSTALL_MYSQL_CLIENT_TYPES = ["mariadb"] PIP_VERSION = "26.0.1" -UV_VERSION = "0.11.7" +UV_VERSION = "0.11.8" # packages that providers docs REGULAR_DOC_PACKAGES = [ diff --git a/pyproject.toml b/pyproject.toml index 34bbeab57b220..430506d15afee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -543,7 +543,7 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-amazon[s3fs]", ] "uv" = [ - "uv>=0.11.7", + "uv>=0.11.8", ] [project.urls] @@ -1343,7 +1343,12 @@ leveldb = [ ] [tool.uv] -required-version = ">=0.9.17" +# Bump this only when the project actually relies on a newer uv feature/fix. It is a +# minimum contributors must install, NOT the uv CI pins to — keeping it in lockstep +# with AIRFLOW_UV_VERSION would force everyone to upgrade uv on every release. The +# breeze/prek uv version checks read this value dynamically and tolerate a stale +# floor; `scripts/ci/prek/upgrade_important_versions.py` deliberately skips it. +required-version = ">=0.11.8" no-build-isolation-package = ["sphinx-redoc"] # Synchroonize with scripts/ci/prek/upgrade_important_versions.py exclude-newer = "4 days" @@ -1479,6 +1484,18 @@ apache-airflow-task-sdk-integration-tests = false apache-aurflow-docker-stack = false # End of automatically generated exclude-newer-package entries +# Manual overrides (kept outside the auto-generated block above so the +# update_airflow_pyproject_toml.py script doesn't clobber them). +# uv 0.11.8 lifted the timestamp from `uv.lock` for relative `exclude-newer` +# (https://github.com/astral-sh/uv/issues/18708, fixed in +# https://github.com/astral-sh/uv/pull/19022) — we want that fix, but the +# global 4-day cooldown above would otherwise block a freshly-published uv +# from being adopted as the floor. Drop uv's cooldown to 12 hours so the +# pinned `required-version` floor isn't gated by the project-wide window. +# REMOVE BY 2026-05-01 — once 0.11.8 is older than the global 4-day cooldown +# this override is redundant and should be deleted along with the line below. +uv = "12 hours" + [tool.uv.pip] # Synchroonize with scripts/ci/prek/upgrade_important_versions.py exclude-newer = "4 days" @@ -1614,6 +1631,11 @@ apache-airflow-task-sdk-integration-tests = false apache-aurflow-docker-stack = false # End of automatically generated exclude-newer-package-pip entries +# Manual overrides — see the matching block under +# `[tool.uv.exclude-newer-package]` above for rationale. +# REMOVE BY 2026-05-01 along with the matching entry above. +uv = "12 hours" + [tool.uv.sources] # These names must match the names as defined in the pyproject.toml of the workspace items, diff --git a/uv.lock b/uv.lock index 8775eea4918fd..a7a98c70d06e1 100644 --- a/uv.lock +++ b/uv.lock @@ -22,7 +22,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-17T18:29:18.615995929Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P4D" [options.exclude-newer-package] @@ -107,6 +107,7 @@ apache-airflow-providers-snowflake = false apache-airflow-providers-presto = false apache-airflow-providers-airbyte = false apache-airflow-providers-apache-hive = false +uv = { timestamp = "0001-01-01T00:00:00Z", span = "PT12H" } apache-airflow-kubernetes-tests = false apache-airflow-providers-grpc = false apache-airflow-providers-apache-druid = false @@ -1619,7 +1620,7 @@ requires-dist = [ { name = "cloudpickle", marker = "extra == 'cloudpickle'", specifier = ">=2.2.1" }, { name = "python-ldap", marker = "extra == 'ldap'", specifier = ">=3.4.4" }, { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.30.0" }, - { name = "uv", marker = "extra == 'uv'", specifier = ">=0.11.7" }, + { name = "uv", marker = "extra == 'uv'", specifier = ">=0.11.8" }, ] provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "alibaba", "amazon", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "github", "google", "grpc", "hashicorp", "http", "imap", "influxdb", "informatica", "jdbc", "jenkins", "keycloak", "microsoft-azure", "microsoft-mssql", "microsoft-psrp", "microsoft-winrm", "mongo", "mysql", "neo4j", "odbc", "openai", "openfaas", "openlineage", "opensearch", "opsgenie", "oracle", "pagerduty", "papermill", "pgvector", "pinecone", "postgres", "presto", "qdrant", "redis", "salesforce", "samba", "segment", "sendgrid", "sftp", "singularity", "slack", "smtp", "snowflake", "sqlite", "ssh", "standard", "tableau", "telegram", "teradata", "trino", "vertica", "weaviate", "yandex", "ydb", "zendesk", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] @@ -1990,7 +1991,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "argcomplete", specifier = ">=1.10" }, - { name = "httpx", specifier = ">=0.27.0" }, + { name = "httpx", specifier = ">=0.27.0,<1.0" }, { name = "keyring", specifier = ">=25.7.0" }, { name = "keyrings-alt", marker = "extra == 'dev'", specifier = ">=5.0.2" }, { name = "lazy-object-proxy", specifier = ">=1.2.0" }, @@ -21897,28 +21898,28 @@ wheels = [ [[package]] name = "uv" -version = "0.11.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/7d/17750123a8c8e324627534fe1ae2e7a46689db8492f1a834ab4fd229a7d8/uv-0.11.7.tar.gz", hash = "sha256:46d971489b00bdb27e0aa715e4a5cd4ef2c28ea5b6ef78f2b67bf861eb44b405", size = 4083385, upload-time = "2026-04-15T21:42:55.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/5b/2bb2ab6fe6c78c2be10852482ef0cae5f3171460a6e5e24c32c9a0843163/uv-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:f422d39530516b1dfb28bb6e90c32bb7dacd50f6a383cd6e40c1a859419fbc8c", size = 23757265, upload-time = "2026-04-15T21:43:14.494Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f5/36ff27b01e60a88712628c8a5a6003b8e418883c24e084e506095844a797/uv-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8b2fe1ec6775dad10183e3fdce430a5b37b7857d49763c884f3a67eaa8ca6f8a", size = 23184529, upload-time = "2026-04-15T21:42:30.225Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fa/f379be661316698f877e78f4c51e5044be0b6f390803387237ad92c4057f/uv-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:162fa961a9a081dcea6e889c79f738a5ae56507047e4672964972e33c301bea9", size = 21780167, upload-time = "2026-04-15T21:42:44.942Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/fbed29775b0612f4f5679d3226268f1a347161abc1727b4080fb41d9f46f/uv-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5985a15a92bd9a170fc1947abb1fbc3e9828c5a430ad85b5bed8356c20b67a71", size = 23609640, upload-time = "2026-04-15T21:42:22.57Z" }, - { url = "https://files.pythonhosted.org/packages/ad/de/989a69634a869a22322770120557c2d8cbba5b77ec7cfad326b4ec0f0547/uv-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:fab0bb43fbbc0ee5b5fee212078d2300c371b725faff7cf72eeaafa0bff0606b", size = 23322484, upload-time = "2026-04-15T21:43:26.52Z" }, - { url = "https://files.pythonhosted.org/packages/24/08/c1af05ea602eb4eb75d86badb6b0594cc104c3ca83ccf06d9ed4dd2186ad/uv-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23d457d6731ebdb83f1bffebe4894edab2ef43c1ec5488433c74300db4958924", size = 23326385, upload-time = "2026-04-15T21:42:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/68/99/e246962da06383e992ecab55000c62a50fb36efef855ea7264fad4816bf4/uv-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d6a17507b8139b8803f445a03fd097f732ce8356b1b7b13cdb4dd8ef7f4b2e0", size = 24985751, upload-time = "2026-04-15T21:42:37.777Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/b0b68083859579ce811996c1480765ec6a2442b44c451eaef53e6218fbae/uv-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd48823ca4b505124389f49ae50626ba9f57212b9047738efc95126ed5f3844d", size = 25724160, upload-time = "2026-04-15T21:43:18.762Z" }, - { url = "https://files.pythonhosted.org/packages/4e/19/5970e89d9e458fd3c4966bbc586a685a1c0ab0a8bf334503f63fa20b925b/uv-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb91f52ee67e10d5290f2c2897e2171357f1a10966de38d83eefa93d96843b0c", size = 25028512, upload-time = "2026-04-15T21:43:02.721Z" }, - { url = "https://files.pythonhosted.org/packages/83/eb/4e1557daf6693cb446ed28185664ad6682fd98c6dbac9e433cbc35df450a/uv-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e4d5e31bea86e1b6e0f5a0f95e14e80018e6f6c0129256d2915a4b3d793644d", size = 24933975, upload-time = "2026-04-15T21:42:18.828Z" }, - { url = "https://files.pythonhosted.org/packages/68/55/3b517ec8297f110d6981f525cccf26f86e30883fbb9c282769cffbcdcfca/uv-0.11.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ceae53b202ea92bc954759bc7c7570cdcd5c3512fce15701198c19fd2dfb8605", size = 23706403, upload-time = "2026-04-15T21:43:10.664Z" }, - { url = "https://files.pythonhosted.org/packages/dc/30/7d93a0312d60e147722967036dc8ea37baab4802784bddc22464cb707deb/uv-0.11.7-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:f97e9f4e4d44fb5c4dfaa05e858ef3414a96416a2e4af270ecd88a3e5fb049a9", size = 24495797, upload-time = "2026-04-15T21:42:26.538Z" }, - { url = "https://files.pythonhosted.org/packages/8c/89/d49480bdab7725d36982793857e461d471bde8e1b7f438ffccee677a7bf8/uv-0.11.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:750ee5b96959b807cf442b73dd8b55111862d63f258f896787ea5f06b68aaca9", size = 24580471, upload-time = "2026-04-15T21:42:52.871Z" }, - { url = "https://files.pythonhosted.org/packages/b6/9f/c57dc03b48be17b564e304eb9ff982890c12dfb888b1ce370788733329ab/uv-0.11.7-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f394331f0507e80ee732cb3df737589de53bed999dd02a6d24682f08c2f8ac4f", size = 24113637, upload-time = "2026-04-15T21:42:34.094Z" }, - { url = "https://files.pythonhosted.org/packages/13/ba/b87e358b629a68258527e3490e73b7b148770f4d2257842dea3b7981d4e8/uv-0.11.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0df59ab0c6a4b14a763e8445e1c303af9abeb53cdfa4428daf9ff9642c0a3cce", size = 25119850, upload-time = "2026-04-15T21:43:22.529Z" }, - { url = "https://files.pythonhosted.org/packages/4b/74/16d229e1d8574bcbafa6dc643ac20b70c3e581f42ac31a6f4fd53035ffe3/uv-0.11.7-py3-none-win32.whl", hash = "sha256:553e67cc766d013ce24353fecd4ea5533d2aedcfd35f9fac430e07b1d1f23ed4", size = 22918454, upload-time = "2026-04-15T21:42:58.702Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1d/b73e473da616ac758b8918fb218febcc46ddf64cba9e03894dfa226b28bd/uv-0.11.7-py3-none-win_amd64.whl", hash = "sha256:5674dfb5944513f4b3735b05c2deba6b1b01151f46729d533d413a9a905f8c5d", size = 25447744, upload-time = "2026-04-15T21:42:48.813Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bb/e6bfdea92ed270f3445a5a3c17599d041b3f2dbc5026c09e02830a03bbaf/uv-0.11.7-py3-none-win_arm64.whl", hash = "sha256:6158b7e39464f1aa1e040daa0186cae4749a78b5cd80ac769f32ca711b8976b1", size = 23941816, upload-time = "2026-04-15T21:43:06.732Z" }, +version = "0.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/cd/4393fecb083897e956f016d4e66d0b8a496a08fe2e03cbda32a1e91da7ee/uv-0.11.8.tar.gz", hash = "sha256:bb2cf302b8503629aab6f0090a05551e6f8cfc2d687ca059cad7ec9e11214335", size = 4098020, upload-time = "2026-04-27T13:15:31.625Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/84/dcb676a3e36a3a2b44dc2e4dfea471b8cd709025e27cce3e588b176fd899/uv-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:a53e704a780a9e78a50f5a880e99a690f84e6fb9e82610903ce26f47c271d74c", size = 23664296, upload-time = "2026-04-27T13:15:15.644Z" }, + { url = "https://files.pythonhosted.org/packages/86/05/557aa070fda7b8460bbbe1e867e8e5b80602c5b30ed77d1d94fc5acae518/uv-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d414fc3795b6f56fb6b1fa359537930924fdfe857750a144d2aedf3077be3f1d", size = 23087321, upload-time = "2026-04-27T13:15:36.193Z" }, + { url = "https://files.pythonhosted.org/packages/d5/62/82953018801a250e16b091ef4b5e95e939b2f01224363d6fc80f600b7eff/uv-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f0d402e182ab581e934c159cc9edf25ec6e08d32f29aa797980e949afefc87cd", size = 21747142, upload-time = "2026-04-27T13:15:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/af/4c/477f2abe16f9a3d3c73077f15615878a303eef3760115ec946be58ecb9b2/uv-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:877c9af3b3955a35ef739e5b2ba79c56dae5c4d50420a7ed908c0901e1c8c807", size = 23425861, upload-time = "2026-04-27T13:15:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/2a/63/19f46193e49f0c9bf33346a4d726313871864db16e7cdd1c0a63bc112000/uv-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:8278144df8d80a83f770c264a5e79ea50791316d2a0dda869e53b3c1174142a8", size = 23215551, upload-time = "2026-04-27T13:15:38.706Z" }, + { url = "https://files.pythonhosted.org/packages/72/3e/5595b265df848a33cd060b10e8f763a46d67521ac9f6c314e8a4ad5329d7/uv-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3494ad32465f4e02259cfb104d24efe5bb8f7a782351f0354de9385415fb310", size = 23224170, upload-time = "2026-04-27T13:15:18.083Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b3/6ca95e690b52542caa1dae10ede57732f90c629946ab5f027ff746f87deb/uv-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4421e27e81f85bce3bdb75986c38b5f9bfab9cdccaf3d977cf124b3f0f0b989", size = 24730048, upload-time = "2026-04-27T13:15:13.254Z" }, + { url = "https://files.pythonhosted.org/packages/ea/49/71b7322067c85a3736a22a300072b0566991fe3f95b81bed793508ff5315/uv-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91943e77fc962752d4f64ad5739219858395981078051c740b28b52963b366aa", size = 25585906, upload-time = "2026-04-27T13:15:41.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/16/4e84cd5131327fe86d4784ebfc8a983149f4e6b811476ef271fc548b29e6/uv-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:41fbba287efcc9bc9505a60549b3a223220da720eacd03be8c23d9daaafa44f4", size = 24795740, upload-time = "2026-04-27T13:15:49.842Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/df175979018743cc5ba6e2fb9dcec916868271e8d88cf0b9df8fd805a0df/uv-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d97bb2920d6cddc07faa475013461294cc09b77ec8139278416c6e54b938d037", size = 24824980, upload-time = "2026-04-27T13:15:53.506Z" }, + { url = "https://files.pythonhosted.org/packages/1c/95/93c7f595f7136fb32807442860c55d0faed2cd3d7da4b7105ed3c2535d5f/uv-0.11.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:fb6a755305eb1e081dfe6a8bc007dbae2d26fe75e551656ca7c9cd08fba21d26", size = 23526790, upload-time = "2026-04-27T13:15:04.955Z" }, + { url = "https://files.pythonhosted.org/packages/04/02/77430b89e172c20cc549b07a5b1dfda0c882c161b6d82781d3150a7063ac/uv-0.11.8-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:841ecbb38532698f73b14b49dc5f0c5e756194c7fcf6e5c6b7ed3859200fe91b", size = 24280498, upload-time = "2026-04-27T13:15:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e3/23e4a2bb91e3880e017e6116886e2d0bde14ba6aa95ddc458160ee630e7c/uv-0.11.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b3ff2b20c1897105ebe7ed7f9b1b331c7171da029bc1e35970ce31dc086141c1", size = 24375233, upload-time = "2026-04-27T13:15:25.753Z" }, + { url = "https://files.pythonhosted.org/packages/d9/67/fb7dc17cea816a667d1be2632525aa1687566bfafd17bdac561a7a6c9484/uv-0.11.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ad381228b0170ef9646902c7e908d4a10a7ecc3da8139450506cf70c7e7f3e80", size = 23904818, upload-time = "2026-04-27T13:15:23.21Z" }, + { url = "https://files.pythonhosted.org/packages/4b/91/b920e35f54f8c6b51f2c639e8170bb80a47a739a1442fea33a479bc93a3d/uv-0.11.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0172b5215544844cd3db0fa3c73a2eb74999b3f00cd2527dde578725076d7b65", size = 25015448, upload-time = "2026-04-27T13:15:46.666Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/3771956dc1c94b8484789bb8070d91872080d0af99332b8bdec7218c2bfd/uv-0.11.8-py3-none-win32.whl", hash = "sha256:e71c1dd23cbb480f3952c3a95b4fd00f96bd618e2a94583fc9388c500af3070d", size = 22823583, upload-time = "2026-04-27T13:15:33.674Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9b/a91a9c60dcae0e1e3da06377d38f32118a523697d461fe41bc9f117ecf59/uv-0.11.8-py3-none-win_amd64.whl", hash = "sha256:306c624c68d95dd7ea3647675323d72c1abc25f91c3e92ae4cd6f0f11b508726", size = 25407438, upload-time = "2026-04-27T13:15:28.957Z" }, + { url = "https://files.pythonhosted.org/packages/61/5d/defa29fe617e6f07d4e514089e9d36fd9f44ede869e597e39ff7d69f6917/uv-0.11.8-py3-none-win_arm64.whl", hash = "sha256:a9853456696d579f206135c9dda7227a6ed8311b8a9a0b9b2008c4ae81950efe", size = 23914243, upload-time = "2026-04-27T13:15:07.717Z" }, ] [[package]] From 2e10497288a8ddc41b2a05e9568ca21ece3f8d93 Mon Sep 17 00:00:00 2001 From: Yeonguk Choo Date: Sat, 2 May 2026 07:59:37 +0900 Subject: [PATCH 05/10] [airflow-ctl-v0-1-test] Align Dag capitalization from "DAG" to "Dag" for airflow-ctl/ (#66112) (#66217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of #66112. Conflicts resolved by keeping v0-1-test base for unrelated divergences (only_new field, validate_model, is_backfillable property, DAGRunCollectionResponse multi-line docstring) and applying only the DAG -> Dag description-text changes — feature-related lines absent in v0-1-test were not introduced. Co-authored-by: hojeong park --- .../api_fastapi/core_api/datamodels/assets.py | 2 +- .../core_api/datamodels/dag_run.py | 14 +- .../core_api/datamodels/dag_sources.py | 2 +- .../core_api/datamodels/dag_stats.py | 4 +- .../core_api/datamodels/dag_tags.py | 4 +- .../core_api/datamodels/dag_versions.py | 2 +- .../core_api/datamodels/dag_warning.py | 4 +- .../api_fastapi/core_api/datamodels/dags.py | 8 +- .../core_api/openapi/_private_ui.yaml | 2 +- .../openapi/v2-rest-api-generated.yaml | 38 +++--- .../ui/openapi-gen/requests/schemas.gen.ts | 38 +++--- .../ui/openapi-gen/requests/types.gen.ts | 38 +++--- airflow-ctl/docs/howto/index.rst | 2 +- airflow-ctl/docs/images/command_hashes.txt | 8 +- airflow-ctl/docs/images/output_assets.svg | 128 +++++++++--------- airflow-ctl/docs/images/output_backfill.svg | 84 ++++++------ airflow-ctl/docs/images/output_dagrun.svg | 60 ++++---- airflow-ctl/docs/images/output_dags.svg | 116 ++++++++-------- airflow-ctl/src/airflowctl/api/client.py | 4 +- .../airflowctl/api/datamodels/generated.py | 38 +++--- airflow-ctl/src/airflowctl/api/operations.py | 2 +- airflow-ctl/src/airflowctl/ctl/cli_config.py | 2 +- .../airflowctl/ctl/commands/config_command.py | 4 +- .../airflowctl/ctl/commands/dag_command.py | 6 +- .../src/airflowctl/ctl/help_texts.yaml | 42 +++--- 25 files changed, 326 insertions(+), 326 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py index 041ec12c1f24b..f463f3ba5e3e5 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py @@ -34,7 +34,7 @@ class DagScheduleAssetReference(StrictBaseModel): - """DAG schedule reference serializer for assets.""" + """Dag schedule reference serializer for assets.""" dag_id: str created_at: datetime diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py index bc56d092b164c..d1beaf5086a5c 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py @@ -36,7 +36,7 @@ class DAGRunPatchStates(str, Enum): - """Enum for DAG Run states when updating a DAG Run.""" + """Enum for Dag Run states when updating a Dag Run.""" QUEUED = DagRunState.QUEUED SUCCESS = DagRunState.SUCCESS @@ -44,14 +44,14 @@ class DAGRunPatchStates(str, Enum): class DAGRunPatchBody(StrictBaseModel): - """DAG Run Serializer for PATCH requests.""" + """Dag Run Serializer for PATCH requests.""" state: DAGRunPatchStates | None = None note: str | None = Field(None, max_length=1000) class DAGRunClearBody(StrictBaseModel): - """DAG Run serializer for clear endpoint body.""" + """Dag Run serializer for clear endpoint body.""" dry_run: bool = True only_failed: bool = False @@ -62,7 +62,7 @@ class DAGRunClearBody(StrictBaseModel): class DAGRunResponse(BaseModel): - """DAG Run serializer for responses.""" + """Dag Run serializer for responses.""" dag_run_id: str = Field(validation_alias="run_id") dag_id: str @@ -88,14 +88,14 @@ class DAGRunResponse(BaseModel): class DAGRunCollectionResponse(BaseModel): - """DAG Run Collection serializer for responses.""" + """Dag Run Collection serializer for responses.""" dag_runs: Iterable[DAGRunResponse] total_entries: int class TriggerDAGRunPostBody(StrictBaseModel): - """Trigger DAG Run Serializer for POST body.""" + """Trigger Dag Run Serializer for POST body.""" dag_run_id: str | None = None data_interval_start: AwareDatetime | None = None @@ -145,7 +145,7 @@ def validate_context(self, dag: SerializedDAG) -> dict: class DAGRunsBatchBody(StrictBaseModel): - """List DAG Runs body for batch endpoint.""" + """List Dag Runs body for batch endpoint.""" order_by: str | None = None page_offset: NonNegativeInt = 0 diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_sources.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_sources.py index c41d55f9aa2fb..fdc081a0ab2b7 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_sources.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_sources.py @@ -22,7 +22,7 @@ class DAGSourceResponse(BaseModel): - """DAG Source serializer for responses.""" + """Dag Source serializer for responses.""" content: str | None dag_id: str diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_stats.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_stats.py index 921886b37c4b6..d07646f41a0d9 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_stats.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_stats.py @@ -31,7 +31,7 @@ class DagStatsStateResponse(BaseModel): class DagStatsResponse(BaseModel): - """DAG Stats serializer for responses.""" + """Dag Stats serializer for responses.""" dag_id: str dag_display_name: str = Field(validation_alias=AliasPath("dag_model", "dag_display_name")) @@ -39,7 +39,7 @@ class DagStatsResponse(BaseModel): class DagStatsCollectionResponse(BaseModel): - """DAG Stats Collection serializer for responses.""" + """Dag Stats Collection serializer for responses.""" dags: list[DagStatsResponse] total_entries: int diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_tags.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_tags.py index 901787de1fb0a..1ca8ae611288e 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_tags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_tags.py @@ -23,7 +23,7 @@ class DagTagResponse(BaseModel): - """DAG Tag serializer for responses.""" + """Dag Tag serializer for responses.""" name: str dag_id: str @@ -31,7 +31,7 @@ class DagTagResponse(BaseModel): class DAGTagCollectionResponse(BaseModel): - """DAG Tags Collection serializer for responses.""" + """Dag Tags Collection serializer for responses.""" tags: list[str] total_entries: int diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py index 0ec9f2fad8d87..a9a7a585fd446 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py @@ -40,7 +40,7 @@ class DagVersionResponse(BaseModel): class DAGVersionCollectionResponse(BaseModel): - """DAG Version Collection serializer for responses.""" + """Dag Version Collection serializer for responses.""" dag_versions: Iterable[DagVersionResponse] total_entries: int diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_warning.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_warning.py index 255d8af14af17..470a0b14fc64f 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_warning.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_warning.py @@ -27,7 +27,7 @@ class DAGWarningResponse(BaseModel): - """DAG Warning serializer for responses.""" + """Dag Warning serializer for responses.""" dag_id: str warning_type: DagWarningType @@ -37,7 +37,7 @@ class DAGWarningResponse(BaseModel): class DAGWarningCollectionResponse(BaseModel): - """DAG warning collection serializer for responses.""" + """Dag warning collection serializer for responses.""" dag_warnings: Iterable[DAGWarningResponse] total_entries: int diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py index 333349ad5e1a1..ea73f1fca135e 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py @@ -66,7 +66,7 @@ def _get_file_token_serializer() -> URLSafeSerializer: class DAGResponse(BaseModel): - """DAG serializer for responses.""" + """Dag serializer for responses.""" model_config = ConfigDict( alias_generator=AliasGenerator( @@ -110,7 +110,7 @@ def serialize_tags(self, tags: list[DagTagResponse]) -> list[DagTagResponse]: @field_validator("owners", mode="before") @classmethod def get_owners(cls, v: Any) -> list[str] | None: - """Convert owners attribute to DAG representation.""" + """Convert owners attribute to Dag representation.""" if not (v is None or isinstance(v, str)): return v @@ -150,14 +150,14 @@ class DAGPatchBody(StrictBaseModel): class DAGCollectionResponse(BaseModel): - """DAG Collection serializer for responses.""" + """Dag Collection serializer for responses.""" dags: Iterable[DAGResponse] total_entries: int class DAGDetailsResponse(DAGResponse): - """Specific serializer for DAG Details responses.""" + """Specific serializer for Dag Details responses.""" model_config = ConfigDict( from_attributes=True, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index 706cf64b492aa..1777d56e50ed0 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -2052,7 +2052,7 @@ components: - dag_id - dag_display_name title: DagTagResponse - description: DAG Tag serializer for responses. + description: Dag Tag serializer for responses. DagVersionResponse: properties: id: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index 582357bbfea89..256cf21a001d0 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -10153,7 +10153,7 @@ components: - dags - total_entries title: DAGCollectionResponse - description: DAG Collection serializer for responses. + description: Dag Collection serializer for responses. DAGDetailsResponse: properties: dag_id: @@ -10430,7 +10430,7 @@ components: - concurrency - latest_dag_version title: DAGDetailsResponse - description: Specific serializer for DAG Details responses. + description: Specific serializer for Dag Details responses. DAGPatchBody: properties: is_paused: @@ -10603,7 +10603,7 @@ components: - owners - file_token title: DAGResponse - description: DAG serializer for responses. + description: Dag serializer for responses. DAGRunClearBody: properties: dry_run: @@ -10623,7 +10623,7 @@ components: additionalProperties: false type: object title: DAGRunClearBody - description: DAG Run serializer for clear endpoint body. + description: Dag Run serializer for clear endpoint body. DAGRunCollectionResponse: properties: dag_runs: @@ -10639,7 +10639,7 @@ components: - dag_runs - total_entries title: DAGRunCollectionResponse - description: DAG Run Collection serializer for responses. + description: Dag Run Collection serializer for responses. DAGRunPatchBody: properties: state: @@ -10655,7 +10655,7 @@ components: additionalProperties: false type: object title: DAGRunPatchBody - description: DAG Run Serializer for PATCH requests. + description: Dag Run Serializer for PATCH requests. DAGRunPatchStates: type: string enum: @@ -10663,7 +10663,7 @@ components: - success - failed title: DAGRunPatchStates - description: Enum for DAG Run states when updating a DAG Run. + description: Enum for Dag Run states when updating a Dag Run. DAGRunResponse: properties: dag_run_id: @@ -10789,7 +10789,7 @@ components: - dag_display_name - partition_key title: DAGRunResponse - description: DAG Run serializer for responses. + description: Dag Run serializer for responses. DAGRunsBatchBody: properties: order_by: @@ -10947,7 +10947,7 @@ components: additionalProperties: false type: object title: DAGRunsBatchBody - description: List DAG Runs body for batch endpoint. + description: List Dag Runs body for batch endpoint. DAGSourceResponse: properties: content: @@ -10973,7 +10973,7 @@ components: - version_number - dag_display_name title: DAGSourceResponse - description: DAG Source serializer for responses. + description: Dag Source serializer for responses. DAGTagCollectionResponse: properties: tags: @@ -10989,7 +10989,7 @@ components: - tags - total_entries title: DAGTagCollectionResponse - description: DAG Tags Collection serializer for responses. + description: Dag Tags Collection serializer for responses. DAGVersionCollectionResponse: properties: dag_versions: @@ -11005,7 +11005,7 @@ components: - dag_versions - total_entries title: DAGVersionCollectionResponse - description: DAG Version Collection serializer for responses. + description: Dag Version Collection serializer for responses. DAGWarningCollectionResponse: properties: dag_warnings: @@ -11021,7 +11021,7 @@ components: - dag_warnings - total_entries title: DAGWarningCollectionResponse - description: DAG warning collection serializer for responses. + description: Dag warning collection serializer for responses. DAGWarningResponse: properties: dag_id: @@ -11047,7 +11047,7 @@ components: - timestamp - dag_display_name title: DAGWarningResponse - description: DAG Warning serializer for responses. + description: Dag Warning serializer for responses. DagProcessorInfoResponse: properties: status: @@ -11183,7 +11183,7 @@ components: - created_at - updated_at title: DagScheduleAssetReference - description: DAG schedule reference serializer for assets. + description: Dag schedule reference serializer for assets. DagStatsCollectionResponse: properties: dags: @@ -11199,7 +11199,7 @@ components: - dags - total_entries title: DagStatsCollectionResponse - description: DAG Stats Collection serializer for responses. + description: Dag Stats Collection serializer for responses. DagStatsResponse: properties: dag_id: @@ -11219,7 +11219,7 @@ components: - dag_display_name - stats title: DagStatsResponse - description: DAG Stats serializer for responses. + description: Dag Stats serializer for responses. DagStatsStateResponse: properties: state: @@ -11250,7 +11250,7 @@ components: - dag_id - dag_display_name title: DagTagResponse - description: DAG Tag serializer for responses. + description: Dag Tag serializer for responses. DagVersionResponse: properties: id: @@ -13402,7 +13402,7 @@ components: required: - logical_date title: TriggerDAGRunPostBody - description: Trigger DAG Run Serializer for POST body. + description: Trigger Dag Run Serializer for POST body. TriggerResponse: properties: id: diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 7ba43d2c93a93..583b7728b628d 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1762,7 +1762,7 @@ export const $DAGCollectionResponse = { type: 'object', required: ['dags', 'total_entries'], title: 'DAGCollectionResponse', - description: 'DAG Collection serializer for responses.' + description: 'Dag Collection serializer for responses.' } as const; export const $DAGDetailsResponse = { @@ -2189,7 +2189,7 @@ Deprecated: Use max_active_tasks instead.`, type: 'object', required: ['dag_id', 'dag_display_name', 'is_paused', 'is_stale', 'last_parsed_time', 'last_parse_duration', 'last_expired', 'bundle_name', 'bundle_version', 'relative_fileloc', 'fileloc', 'description', 'timetable_summary', 'timetable_description', 'timetable_partitioned', 'tags', 'max_active_tasks', 'max_active_runs', 'max_consecutive_failed_dag_runs', 'has_task_concurrency_limits', 'has_import_errors', 'next_dagrun_logical_date', 'next_dagrun_data_interval_start', 'next_dagrun_data_interval_end', 'next_dagrun_run_after', 'allowed_run_types', 'owners', 'catchup', 'dag_run_timeout', 'asset_expression', 'doc_md', 'start_date', 'end_date', 'is_paused_upon_creation', 'params', 'render_template_as_native_obj', 'template_search_path', 'timezone', 'last_parsed', 'default_args', 'file_token', 'concurrency', 'latest_dag_version'], title: 'DAGDetailsResponse', - description: 'Specific serializer for DAG Details responses.' + description: 'Specific serializer for Dag Details responses.' } as const; export const $DAGPatchBody = { @@ -2446,7 +2446,7 @@ export const $DAGResponse = { type: 'object', required: ['dag_id', 'dag_display_name', 'is_paused', 'is_stale', 'last_parsed_time', 'last_parse_duration', 'last_expired', 'bundle_name', 'bundle_version', 'relative_fileloc', 'fileloc', 'description', 'timetable_summary', 'timetable_description', 'timetable_partitioned', 'tags', 'max_active_tasks', 'max_active_runs', 'max_consecutive_failed_dag_runs', 'has_task_concurrency_limits', 'has_import_errors', 'next_dagrun_logical_date', 'next_dagrun_data_interval_start', 'next_dagrun_data_interval_end', 'next_dagrun_run_after', 'allowed_run_types', 'owners', 'file_token'], title: 'DAGResponse', - description: 'DAG serializer for responses.' + description: 'Dag serializer for responses.' } as const; export const $DAGRunClearBody = { @@ -2471,7 +2471,7 @@ export const $DAGRunClearBody = { additionalProperties: false, type: 'object', title: 'DAGRunClearBody', - description: 'DAG Run serializer for clear endpoint body.' + description: 'Dag Run serializer for clear endpoint body.' } as const; export const $DAGRunCollectionResponse = { @@ -2491,7 +2491,7 @@ export const $DAGRunCollectionResponse = { type: 'object', required: ['dag_runs', 'total_entries'], title: 'DAGRunCollectionResponse', - description: 'DAG Run Collection serializer for responses.' + description: 'Dag Run Collection serializer for responses.' } as const; export const $DAGRunPatchBody = { @@ -2522,14 +2522,14 @@ export const $DAGRunPatchBody = { additionalProperties: false, type: 'object', title: 'DAGRunPatchBody', - description: 'DAG Run Serializer for PATCH requests.' + description: 'Dag Run Serializer for PATCH requests.' } as const; export const $DAGRunPatchStates = { type: 'string', enum: ['queued', 'success', 'failed'], title: 'DAGRunPatchStates', - description: 'Enum for DAG Run states when updating a DAG Run.' + description: 'Enum for Dag Run states when updating a Dag Run.' } as const; export const $DAGRunResponse = { @@ -2729,7 +2729,7 @@ export const $DAGRunResponse = { type: 'object', required: ['dag_run_id', 'dag_id', 'logical_date', 'queued_at', 'start_date', 'end_date', 'duration', 'data_interval_start', 'data_interval_end', 'run_after', 'last_scheduling_decision', 'run_type', 'state', 'triggered_by', 'triggering_user_name', 'conf', 'note', 'dag_versions', 'bundle_version', 'dag_display_name', 'partition_key'], title: 'DAGRunResponse', - description: 'DAG Run serializer for responses.' + description: 'Dag Run serializer for responses.' } as const; export const $DAGRunsBatchBody = { @@ -3043,7 +3043,7 @@ export const $DAGRunsBatchBody = { additionalProperties: false, type: 'object', title: 'DAGRunsBatchBody', - description: 'List DAG Runs body for batch endpoint.' + description: 'List Dag Runs body for batch endpoint.' } as const; export const $DAGSourceResponse = { @@ -3082,7 +3082,7 @@ export const $DAGSourceResponse = { type: 'object', required: ['content', 'dag_id', 'version_number', 'dag_display_name'], title: 'DAGSourceResponse', - description: 'DAG Source serializer for responses.' + description: 'Dag Source serializer for responses.' } as const; export const $DAGTagCollectionResponse = { @@ -3102,7 +3102,7 @@ export const $DAGTagCollectionResponse = { type: 'object', required: ['tags', 'total_entries'], title: 'DAGTagCollectionResponse', - description: 'DAG Tags Collection serializer for responses.' + description: 'Dag Tags Collection serializer for responses.' } as const; export const $DAGVersionCollectionResponse = { @@ -3122,7 +3122,7 @@ export const $DAGVersionCollectionResponse = { type: 'object', required: ['dag_versions', 'total_entries'], title: 'DAGVersionCollectionResponse', - description: 'DAG Version Collection serializer for responses.' + description: 'Dag Version Collection serializer for responses.' } as const; export const $DAGWarningCollectionResponse = { @@ -3142,7 +3142,7 @@ export const $DAGWarningCollectionResponse = { type: 'object', required: ['dag_warnings', 'total_entries'], title: 'DAGWarningCollectionResponse', - description: 'DAG warning collection serializer for responses.' + description: 'Dag warning collection serializer for responses.' } as const; export const $DAGWarningResponse = { @@ -3171,7 +3171,7 @@ export const $DAGWarningResponse = { type: 'object', required: ['dag_id', 'warning_type', 'message', 'timestamp', 'dag_display_name'], title: 'DAGWarningResponse', - description: 'DAG Warning serializer for responses.' + description: 'Dag Warning serializer for responses.' } as const; export const $DagProcessorInfoResponse = { @@ -3337,7 +3337,7 @@ export const $DagScheduleAssetReference = { type: 'object', required: ['dag_id', 'created_at', 'updated_at'], title: 'DagScheduleAssetReference', - description: 'DAG schedule reference serializer for assets.' + description: 'Dag schedule reference serializer for assets.' } as const; export const $DagStatsCollectionResponse = { @@ -3357,7 +3357,7 @@ export const $DagStatsCollectionResponse = { type: 'object', required: ['dags', 'total_entries'], title: 'DagStatsCollectionResponse', - description: 'DAG Stats Collection serializer for responses.' + description: 'Dag Stats Collection serializer for responses.' } as const; export const $DagStatsResponse = { @@ -3381,7 +3381,7 @@ export const $DagStatsResponse = { type: 'object', required: ['dag_id', 'dag_display_name', 'stats'], title: 'DagStatsResponse', - description: 'DAG Stats serializer for responses.' + description: 'Dag Stats serializer for responses.' } as const; export const $DagStatsStateResponse = { @@ -3418,7 +3418,7 @@ export const $DagTagResponse = { type: 'object', required: ['name', 'dag_id', 'dag_display_name'], title: 'DagTagResponse', - description: 'DAG Tag serializer for responses.' + description: 'Dag Tag serializer for responses.' } as const; export const $DagVersionResponse = { @@ -6698,7 +6698,7 @@ export const $TriggerDAGRunPostBody = { type: 'object', required: ['logical_date'], title: 'TriggerDAGRunPostBody', - description: 'Trigger DAG Run Serializer for POST body.' + description: 'Trigger Dag Run Serializer for POST body.' } as const; export const $TriggerResponse = { diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index 5b536a702a0f6..1ca6bf0203433 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -515,7 +515,7 @@ export type CreateAssetEventsBody = { }; /** - * DAG Collection serializer for responses. + * Dag Collection serializer for responses. */ export type DAGCollectionResponse = { dags: Array; @@ -523,7 +523,7 @@ export type DAGCollectionResponse = { }; /** - * Specific serializer for DAG Details responses. + * Specific serializer for Dag Details responses. */ export type DAGDetailsResponse = { dag_id: string; @@ -602,7 +602,7 @@ export type DAGPatchBody = { }; /** - * DAG serializer for responses. + * Dag serializer for responses. */ export type DAGResponse = { dag_id: string; @@ -639,7 +639,7 @@ export type DAGResponse = { }; /** - * DAG Run serializer for clear endpoint body. + * Dag Run serializer for clear endpoint body. */ export type DAGRunClearBody = { dry_run?: boolean; @@ -651,7 +651,7 @@ export type DAGRunClearBody = { }; /** - * DAG Run Collection serializer for responses. + * Dag Run Collection serializer for responses. */ export type DAGRunCollectionResponse = { dag_runs: Array; @@ -659,7 +659,7 @@ export type DAGRunCollectionResponse = { }; /** - * DAG Run Serializer for PATCH requests. + * Dag Run Serializer for PATCH requests. */ export type DAGRunPatchBody = { state?: DAGRunPatchStates | null; @@ -667,12 +667,12 @@ export type DAGRunPatchBody = { }; /** - * Enum for DAG Run states when updating a DAG Run. + * Enum for Dag Run states when updating a Dag Run. */ export type DAGRunPatchStates = 'queued' | 'success' | 'failed'; /** - * DAG Run serializer for responses. + * Dag Run serializer for responses. */ export type DAGRunResponse = { dag_run_id: string; @@ -701,7 +701,7 @@ export type DAGRunResponse = { }; /** - * List DAG Runs body for batch endpoint. + * List Dag Runs body for batch endpoint. */ export type DAGRunsBatchBody = { order_by?: string | null; @@ -733,7 +733,7 @@ export type DAGRunsBatchBody = { }; /** - * DAG Source serializer for responses. + * Dag Source serializer for responses. */ export type DAGSourceResponse = { content: string | null; @@ -743,7 +743,7 @@ export type DAGSourceResponse = { }; /** - * DAG Tags Collection serializer for responses. + * Dag Tags Collection serializer for responses. */ export type DAGTagCollectionResponse = { tags: Array<(string)>; @@ -751,7 +751,7 @@ export type DAGTagCollectionResponse = { }; /** - * DAG Version Collection serializer for responses. + * Dag Version Collection serializer for responses. */ export type DAGVersionCollectionResponse = { dag_versions: Array; @@ -759,7 +759,7 @@ export type DAGVersionCollectionResponse = { }; /** - * DAG warning collection serializer for responses. + * Dag warning collection serializer for responses. */ export type DAGWarningCollectionResponse = { dag_warnings: Array; @@ -767,7 +767,7 @@ export type DAGWarningCollectionResponse = { }; /** - * DAG Warning serializer for responses. + * Dag Warning serializer for responses. */ export type DAGWarningResponse = { dag_id: string; @@ -820,7 +820,7 @@ export type DagRunTriggeredByType = 'cli' | 'operator' | 'rest_api' | 'ui' | 'te export type DagRunType = 'backfill' | 'scheduled' | 'manual' | 'asset_triggered' | 'asset_materialization'; /** - * DAG schedule reference serializer for assets. + * Dag schedule reference serializer for assets. */ export type DagScheduleAssetReference = { dag_id: string; @@ -829,7 +829,7 @@ export type DagScheduleAssetReference = { }; /** - * DAG Stats Collection serializer for responses. + * Dag Stats Collection serializer for responses. */ export type DagStatsCollectionResponse = { dags: Array; @@ -837,7 +837,7 @@ export type DagStatsCollectionResponse = { }; /** - * DAG Stats serializer for responses. + * Dag Stats serializer for responses. */ export type DagStatsResponse = { dag_id: string; @@ -854,7 +854,7 @@ export type DagStatsStateResponse = { }; /** - * DAG Tag serializer for responses. + * Dag Tag serializer for responses. */ export type DagTagResponse = { name: string; @@ -1575,7 +1575,7 @@ export type TimeDelta = { }; /** - * Trigger DAG Run Serializer for POST body. + * Trigger Dag Run Serializer for POST body. */ export type TriggerDAGRunPostBody = { dag_run_id?: string | null; diff --git a/airflow-ctl/docs/howto/index.rst b/airflow-ctl/docs/howto/index.rst index 155dab5fc04d4..59f806d4f44b3 100644 --- a/airflow-ctl/docs/howto/index.rst +++ b/airflow-ctl/docs/howto/index.rst @@ -39,7 +39,7 @@ Datetime Usage '''''''''''''' For datetime parameters, date should be timezone aware and in ISO format. For example: ``2025-10-10T10:00:00+00:00`` -Let's take example of triggering a DAG run with a logical date, run after and a note. +Let's take example of triggering a Dag run with a logical date, run after and a note. .. code-block:: bash diff --git a/airflow-ctl/docs/images/command_hashes.txt b/airflow-ctl/docs/images/command_hashes.txt index c824ba2f9abe0..8e590f3b820cd 100644 --- a/airflow-ctl/docs/images/command_hashes.txt +++ b/airflow-ctl/docs/images/command_hashes.txt @@ -1,11 +1,11 @@ main:27a22c00dcf32e7a1a4f06672dc8e3c8 -assets:6e2d3f0f73df1bd794a6b7d8fefffdc3 +assets:70619a2d92bda80930cde2aefcd8e1cd auth:d79e9c7d00c432bdbcbc2a86e2e32053 -backfill:41e008e4bc78d44e69bd9769098ba3b0 +backfill:74c8737b0a62a86ed3605fa9e6165874 config:a3d936cb15fe3b547bf6c82cf93d923f connections:942f9f88cb908c28bf5c19159fc5065b -dags:d9d0b3460097db0b9fbf8ae42bf500c3 -dagrun:0e46473ad2f3dfa1ee9ee27678dde57e +dags:e2a18f90b1bd150be981cef6fef91858 +dagrun:c32e0011aa9a845456c778786717208e jobs:a5b644c5da8889443bb40ee10b599270 pools:19efe105b9515ab1926ebcaf0e028d71 providers:34502fe09dc0b8b0a13e7e46efdffda6 diff --git a/airflow-ctl/docs/images/output_assets.svg b/airflow-ctl/docs/images/output_assets.svg index 07e78ea7c1e0a..b76118dc52aeb 100644 --- a/airflow-ctl/docs/images/output_assets.svg +++ b/airflow-ctl/docs/images/output_assets.svg @@ -19,108 +19,108 @@ font-weight: 700; } - .terminal-4223370654-matrix { + .terminal-3582101150-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4223370654-title { + .terminal-3582101150-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4223370654-r1 { fill: #ff8700 } -.terminal-4223370654-r2 { fill: #c5c8c6 } -.terminal-4223370654-r3 { fill: #808080 } -.terminal-4223370654-r4 { fill: #68a0b3 } + .terminal-3582101150-r1 { fill: #ff8700 } +.terminal-3582101150-r2 { fill: #c5c8c6 } +.terminal-3582101150-r3 { fill: #808080 } +.terminal-3582101150-r4 { fill: #68a0b3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -132,37 +132,37 @@ - + - - Usage:airflowctl assets [-hCOMMAND... - -Perform Assets operations - -Positional Arguments: -COMMAND -create-eventCreate an event for a given asset -delete-dag-queued-events -Delete all queued asset events for a given DAG -delete-queued-event -Delete a specific queued asset event for a given  -DAG and asset -delete-queued-events -Delete all queued events for a given asset -getRetrieve an asset by its ID -get-by-aliasRetrieve an asset by its alias -get-dag-queued-event -Retrieve a specific queued asset event for a given  -DAG and asset -get-dag-queued-events -List queued asset events for a given DAG -get-queued-eventsList queued events for a given asset -listList all assets -list-by-aliasList all asset aliases -materializeTrigger materialization of an asset by its ID - -Options: --h--helpshow this help message and exit + + Usage:airflowctl assets [-hCOMMAND... + +Perform Assets operations + +Positional Arguments: +COMMAND +create-eventCreate an event for a given asset +delete-dag-queued-events +Delete all queued asset events for a given Dag +delete-queued-event +Delete a specific queued asset event for a given  +Dag and asset +delete-queued-events +Delete all queued events for a given asset +getRetrieve an asset by its ID +get-by-aliasRetrieve an asset by its alias +get-dag-queued-event +Retrieve a specific queued asset event for a given  +Dag and asset +get-dag-queued-events +List queued asset events for a given Dag +get-queued-eventsList queued events for a given asset +listList all assets +list-by-aliasList all asset aliases +materializeTrigger materialization of an asset by its ID + +Options: +-h--helpshow this help message and exit diff --git a/airflow-ctl/docs/images/output_backfill.svg b/airflow-ctl/docs/images/output_backfill.svg index 4119e5058af9c..0f3d493a89a43 100644 --- a/airflow-ctl/docs/images/output_backfill.svg +++ b/airflow-ctl/docs/images/output_backfill.svg @@ -19,75 +19,75 @@ font-weight: 700; } - .terminal-602120787-matrix { + .terminal-3011617491-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-602120787-title { + .terminal-3011617491-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-602120787-r1 { fill: #ff8700 } -.terminal-602120787-r2 { fill: #c5c8c6 } -.terminal-602120787-r3 { fill: #808080 } -.terminal-602120787-r4 { fill: #68a0b3 } + .terminal-3011617491-r1 { fill: #ff8700 } +.terminal-3011617491-r2 { fill: #c5c8c6 } +.terminal-3011617491-r3 { fill: #808080 } +.terminal-3011617491-r4 { fill: #68a0b3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -99,26 +99,26 @@ - + - - Usage:airflowctl backfill [-hCOMMAND... - -Perform Backfill operations - -Positional Arguments: -COMMAND -cancelCancel a backfill job -createCreate a backfill job for a given DAG ID and date range -create-dry-run -Preview a backfill job without executing it -getRetrieve details of a backfill job by its ID -listList all backfill jobs for a given DAG -pausePause an active backfill job -unpauseResume a paused backfill job - -Options: --h--helpshow this help message and exit + + Usage:airflowctl backfill [-hCOMMAND... + +Perform Backfill operations + +Positional Arguments: +COMMAND +cancelCancel a backfill job +createCreate a backfill job for a given Dag ID and date range +create-dry-run +Preview a backfill job without executing it +getRetrieve details of a backfill job by its ID +listList all backfill jobs for a given Dag +pausePause an active backfill job +unpauseResume a paused backfill job + +Options: +-h--helpshow this help message and exit diff --git a/airflow-ctl/docs/images/output_dagrun.svg b/airflow-ctl/docs/images/output_dagrun.svg index a03fb094b3e25..48e96c894208e 100644 --- a/airflow-ctl/docs/images/output_dagrun.svg +++ b/airflow-ctl/docs/images/output_dagrun.svg @@ -19,57 +19,57 @@ font-weight: 700; } - .terminal-3797102828-matrix { + .terminal-3834720684-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3797102828-title { + .terminal-3834720684-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3797102828-r1 { fill: #ff8700 } -.terminal-3797102828-r2 { fill: #c5c8c6 } -.terminal-3797102828-r3 { fill: #808080 } -.terminal-3797102828-r4 { fill: #68a0b3 } + .terminal-3834720684-r1 { fill: #ff8700 } +.terminal-3834720684-r2 { fill: #c5c8c6 } +.terminal-3834720684-r3 { fill: #808080 } +.terminal-3834720684-r4 { fill: #68a0b3 } - + - + - + - + - + - + - + - + - + - + - + @@ -81,20 +81,20 @@ - + - - Usage:airflowctl dagrun [-hCOMMAND... - -Perform DagRun operations - -Positional Arguments: -COMMAND -getRetrieve a DAG run by DAG ID and run ID -listList DAG runs, optionally filtered by state and date range - -Options: --h--helpshow this help message and exit + + Usage:airflowctl dagrun [-hCOMMAND... + +Perform DagRun operations + +Positional Arguments: +COMMAND +getRetrieve a Dag run by Dag ID and run ID +listList Dag runs, optionally filtered by state and date range + +Options: +-h--helpshow this help message and exit diff --git a/airflow-ctl/docs/images/output_dags.svg b/airflow-ctl/docs/images/output_dags.svg index 0bbe6f479d743..f38d12d60e343 100644 --- a/airflow-ctl/docs/images/output_dags.svg +++ b/airflow-ctl/docs/images/output_dags.svg @@ -19,99 +19,99 @@ font-weight: 700; } - .terminal-1967506512-matrix { + .terminal-3628713872-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1967506512-title { + .terminal-3628713872-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1967506512-r1 { fill: #ff8700 } -.terminal-1967506512-r2 { fill: #c5c8c6 } -.terminal-1967506512-r3 { fill: #808080 } -.terminal-1967506512-r4 { fill: #68a0b3 } + .terminal-3628713872-r1 { fill: #ff8700 } +.terminal-3628713872-r2 { fill: #c5c8c6 } +.terminal-3628713872-r3 { fill: #808080 } +.terminal-3628713872-r4 { fill: #68a0b3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -123,34 +123,34 @@ - + - - Usage:airflowctl dags [-hCOMMAND... - -Perform Dags operations - -Positional Arguments: -COMMAND -deleteDelete a DAG by its ID -getRetrieve a DAG by its ID -get-detailsRetrieve detailed information for a DAG -get-import-errorRetrieve a DAG import error by its ID -get-statsRetrieve run statistics for one or more DAGs -get-tagsList all tags used across DAGs -get-versionRetrieve a specific version of a DAG -listList all DAGs -list-import-errors -List all DAG import errors -list-versionList all versions of a DAG -list-warningList all DAG warnings -pausePause a Dag -triggerTrigger a new DAG run -unpauseUnpause a Dag -updateUpdate properties of a DAG - -Options: --h--helpshow this help message and exit + + Usage:airflowctl dags [-hCOMMAND... + +Perform Dags operations + +Positional Arguments: +COMMAND +deleteDelete a Dag by its ID +getRetrieve a Dag by its ID +get-detailsRetrieve detailed information for a Dag +get-import-errorRetrieve a Dag import error by its ID +get-statsRetrieve run statistics for one or more Dags +get-tagsList all tags used across Dags +get-versionRetrieve a specific version of a Dag +listList all Dags +list-import-errors +List all Dag import errors +list-versionList all versions of a Dag +list-warningList all Dag warnings +pausePause a Dag +triggerTrigger a new Dag run +unpauseUnpause a Dag +updateUpdate properties of a Dag + +Options: +-h--helpshow this help message and exit diff --git a/airflow-ctl/src/airflowctl/api/client.py b/airflow-ctl/src/airflowctl/api/client.py index e4c5ddf2302d6..b01200fac1c7f 100644 --- a/airflow-ctl/src/airflowctl/api/client.py +++ b/airflow-ctl/src/airflowctl/api/client.py @@ -422,13 +422,13 @@ def connections(self): @lru_cache() # type: ignore[prop-decorator] @property def dags(self): - """Operations related to DAGs.""" + """Operations related to Dags.""" return DagsOperations(self) @lru_cache() # type: ignore[prop-decorator] @property def dag_runs(self): - """Operations related to DAG runs.""" + """Operations related to Dag runs.""" return DagRunOperations(self) @lru_cache() # type: ignore[prop-decorator] diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index b33543df934d5..510a59c3b09be 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -279,7 +279,7 @@ class DAGPatchBody(BaseModel): class DAGRunClearBody(BaseModel): """ - DAG Run serializer for clear endpoint body. + Dag Run serializer for clear endpoint body. """ model_config = ConfigDict( @@ -298,7 +298,7 @@ class DAGRunClearBody(BaseModel): class DAGRunPatchStates(str, Enum): """ - Enum for DAG Run states when updating a DAG Run. + Enum for Dag Run states when updating a Dag Run. """ QUEUED = "queued" @@ -308,7 +308,7 @@ class DAGRunPatchStates(str, Enum): class DAGSourceResponse(BaseModel): """ - DAG Source serializer for responses. + Dag Source serializer for responses. """ content: Annotated[str | None, Field(title="Content")] = None @@ -319,7 +319,7 @@ class DAGSourceResponse(BaseModel): class DAGTagCollectionResponse(BaseModel): """ - DAG Tags Collection serializer for responses. + Dag Tags Collection serializer for responses. """ tags: Annotated[list[str], Field(title="Tags")] @@ -400,7 +400,7 @@ class DagRunType(str, Enum): class DagScheduleAssetReference(BaseModel): """ - DAG schedule reference serializer for assets. + Dag schedule reference serializer for assets. """ model_config = ConfigDict( @@ -422,7 +422,7 @@ class DagStatsStateResponse(BaseModel): class DagTagResponse(BaseModel): """ - DAG Tag serializer for responses. + Dag Tag serializer for responses. """ name: Annotated[str, Field(title="Name")] @@ -901,7 +901,7 @@ class TimeDelta(BaseModel): class TriggerDAGRunPostBody(BaseModel): """ - Trigger DAG Run Serializer for POST body. + Trigger Dag Run Serializer for POST body. """ model_config = ConfigDict( @@ -1358,7 +1358,7 @@ class ConnectionCollectionResponse(BaseModel): class DAGDetailsResponse(BaseModel): """ - Specific serializer for DAG Details responses. + Specific serializer for Dag Details responses. """ dag_id: Annotated[str, Field(title="Dag Id")] @@ -1423,7 +1423,7 @@ class DAGDetailsResponse(BaseModel): class DAGResponse(BaseModel): """ - DAG serializer for responses. + Dag serializer for responses. """ dag_id: Annotated[str, Field(title="Dag Id")] @@ -1462,7 +1462,7 @@ class DAGResponse(BaseModel): class DAGRunPatchBody(BaseModel): """ - DAG Run Serializer for PATCH requests. + Dag Run Serializer for PATCH requests. """ model_config = ConfigDict( @@ -1474,7 +1474,7 @@ class DAGRunPatchBody(BaseModel): class DAGRunResponse(BaseModel): """ - DAG Run serializer for responses. + Dag Run serializer for responses. """ dag_run_id: Annotated[str, Field(title="Dag Run Id")] @@ -1502,7 +1502,7 @@ class DAGRunResponse(BaseModel): class DAGRunsBatchBody(BaseModel): """ - List DAG Runs body for batch endpoint. + List Dag Runs body for batch endpoint. """ model_config = ConfigDict( @@ -1538,7 +1538,7 @@ class DAGRunsBatchBody(BaseModel): class DAGVersionCollectionResponse(BaseModel): """ - DAG Version Collection serializer for responses. + Dag Version Collection serializer for responses. """ dag_versions: Annotated[list[DagVersionResponse], Field(title="Dag Versions")] @@ -1547,7 +1547,7 @@ class DAGVersionCollectionResponse(BaseModel): class DAGWarningResponse(BaseModel): """ - DAG Warning serializer for responses. + Dag Warning serializer for responses. """ dag_id: Annotated[str, Field(title="Dag Id")] @@ -1559,7 +1559,7 @@ class DAGWarningResponse(BaseModel): class DagStatsResponse(BaseModel): """ - DAG Stats serializer for responses. + Dag Stats serializer for responses. """ dag_id: Annotated[str, Field(title="Dag Id")] @@ -1934,7 +1934,7 @@ class BulkDeleteActionBulkTaskInstanceBody(BaseModel): class DAGCollectionResponse(BaseModel): """ - DAG Collection serializer for responses. + Dag Collection serializer for responses. """ dags: Annotated[list[DAGResponse], Field(title="Dags")] @@ -1943,7 +1943,7 @@ class DAGCollectionResponse(BaseModel): class DAGRunCollectionResponse(BaseModel): """ - DAG Run Collection serializer for responses. + Dag Run Collection serializer for responses. """ dag_runs: Annotated[list[DAGRunResponse], Field(title="Dag Runs")] @@ -1952,7 +1952,7 @@ class DAGRunCollectionResponse(BaseModel): class DAGWarningCollectionResponse(BaseModel): """ - DAG warning collection serializer for responses. + Dag warning collection serializer for responses. """ dag_warnings: Annotated[list[DAGWarningResponse], Field(title="Dag Warnings")] @@ -1961,7 +1961,7 @@ class DAGWarningCollectionResponse(BaseModel): class DagStatsCollectionResponse(BaseModel): """ - DAG Stats Collection serializer for responses. + Dag Stats Collection serializer for responses. """ dags: Annotated[list[DagStatsResponse], Field(title="Dags")] diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index 7718c1c56498d..f52ba055c1c72 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -509,7 +509,7 @@ class DagsOperations(BaseOperations): """Dags operations.""" def get(self, dag_id: str) -> DAGResponse | ServerResponseError: - """Get a DAG.""" + """Get a Dag.""" try: self.response = self.client.get(f"dags/{dag_id}") return DAGResponse.model_validate_json(self.response.content) diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index c540b939fbf75..aa96304f501fb 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -265,7 +265,7 @@ def _load_help_texts_yaml() -> dict[str, dict[str, str]]: ARG_DAG_ID = Arg( flags=("dag_id",), type=str, - help="The DAG ID of the DAG to pause or unpause", + help="The Dag ID of the Dag to pause or unpause", ) ARG_ACTION_ON_EXISTING_KEY = Arg( diff --git a/airflow-ctl/src/airflowctl/ctl/commands/config_command.py b/airflow-ctl/src/airflowctl/ctl/commands/config_command.py index b4cb9725ca8be..20147484d943d 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/config_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/config_command.py @@ -540,9 +540,9 @@ def _get_option_value(self, config_resp: Config) -> str | None: was_removed=False, new_default="False", suggestion="In Airflow 3.0 the default value for `catchup_by_default` is set to `False`. " - "This means that DAGs without explicit definition of the `catchup` parameter will not " + "This means that Dags without explicit definition of the `catchup` parameter will not " "catchup by default. " - "If your DAGs rely on catchup behavior, not explicitly defined in the DAG definition, " + "If your Dags rely on catchup behavior, not explicitly defined in the Dag definition, " "set this configuration parameter to `True` in the `scheduler` section of your `airflow.cfg` " "to enable the behavior from Airflow 2.x.", breaking=True, diff --git a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py index 9b43be47eb27b..33b3d7c95fefa 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py @@ -33,7 +33,7 @@ def update_dag_state( api_client, output: str, ): - """Update DAG state (pause/unpause).""" + """Update Dag state (pause/unpause).""" try: response = api_client.dags.update( dag_id=dag_id, dag_body=DAGPatchBody(is_paused=operation == "pause") @@ -54,7 +54,7 @@ def update_dag_state( @provide_api_client(kind=ClientKind.CLI) def pause(args, api_client=NEW_API_CLIENT) -> None: - """Pause a DAG.""" + """Pause a Dag.""" return update_dag_state( dag_id=args.dag_id, operation="pause", @@ -65,7 +65,7 @@ def pause(args, api_client=NEW_API_CLIENT) -> None: @provide_api_client(kind=ClientKind.CLI) def unpause(args, api_client=NEW_API_CLIENT) -> None: - """Unpause a DAG.""" + """Unpause a Dag.""" return update_dag_state( dag_id=args.dag_id, operation="unpause", diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml index 3dac52be0bc99..eb566a96b1fb8 100644 --- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml +++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml @@ -23,17 +23,17 @@ assets: create-event: "Create an event for a given asset" materialize: "Trigger materialization of an asset by its ID" get-queued-events: "List queued events for a given asset" - get-dag-queued-events: "List queued asset events for a given DAG" - get-dag-queued-event: "Retrieve a specific queued asset event for a given DAG and asset" + get-dag-queued-events: "List queued asset events for a given Dag" + get-dag-queued-event: "Retrieve a specific queued asset event for a given Dag and asset" delete-queued-events: "Delete all queued events for a given asset" - delete-dag-queued-events: "Delete all queued asset events for a given DAG" - delete-queued-event: "Delete a specific queued asset event for a given DAG and asset" + delete-dag-queued-events: "Delete all queued asset events for a given Dag" + delete-queued-event: "Delete a specific queued asset event for a given Dag and asset" backfill: - create: "Create a backfill job for a given DAG ID and date range" + create: "Create a backfill job for a given Dag ID and date range" create-dry-run: "Preview a backfill job without executing it" get: "Retrieve details of a backfill job by its ID" - list: "List all backfill jobs for a given DAG" + list: "List all backfill jobs for a given Dag" pause: "Pause an active backfill job" unpause: "Resume a paused backfill job" cancel: "Cancel a backfill job" @@ -52,23 +52,23 @@ connections: test: "Test connectivity for a given connection" dags: - get: "Retrieve a DAG by its ID" - get-details: "Retrieve detailed information for a DAG" - get-tags: "List all tags used across DAGs" - list: "List all DAGs" - update: "Update properties of a DAG" - delete: "Delete a DAG by its ID" - get-import-error: "Retrieve a DAG import error by its ID" - list-import-errors: "List all DAG import errors" - get-stats: "Retrieve run statistics for one or more DAGs" - get-version: "Retrieve a specific version of a DAG" - list-version: "List all versions of a DAG" - list-warning: "List all DAG warnings" - trigger: "Trigger a new DAG run" + get: "Retrieve a Dag by its ID" + get-details: "Retrieve detailed information for a Dag" + get-tags: "List all tags used across Dags" + list: "List all Dags" + update: "Update properties of a Dag" + delete: "Delete a Dag by its ID" + get-import-error: "Retrieve a Dag import error by its ID" + list-import-errors: "List all Dag import errors" + get-stats: "Retrieve run statistics for one or more Dags" + get-version: "Retrieve a specific version of a Dag" + list-version: "List all versions of a Dag" + list-warning: "List all Dag warnings" + trigger: "Trigger a new Dag run" dagrun: - get: "Retrieve a DAG run by DAG ID and run ID" - list: "List DAG runs, optionally filtered by state and date range" + get: "Retrieve a Dag run by Dag ID and run ID" + list: "List Dag runs, optionally filtered by state and date range" jobs: list: "List scheduler, triggerer, and other Airflow jobs" From a781f731fb83e782f0bb4b21836eb77127c13c6f Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Sat, 23 May 2026 20:44:10 +0200 Subject: [PATCH 06/10] [airflow-ctl/v0-1-test] Increment version of airflowctl for RC (#67295) (#67384) * Increment version of airflowctl for RC * Change airflow-core usage for ctl * Change airflow-core usage for ctl and amend installation in docker * Prepare airflowctl for tests in CI * Amend install airflow and provider to cover airflowctl (cherry picked from commit 336a1199a1c9200d0bbb49f07477bdd24fc222d1) --- .github/workflows/test-providers.yml | 4 +++ Dockerfile | 7 +++-- airflow-core/pyproject.toml | 27 ++++++++++++------- airflow-ctl/src/airflowctl/__init__.py | 2 +- .../install_from_docker_context_files.sh | 7 +++-- .../install_airflow_and_providers.py | 15 ++++++++--- 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test-providers.yml b/.github/workflows/test-providers.yml index db1f31f2453aa..671497af234dd 100644 --- a/.github/workflows/test-providers.yml +++ b/.github/workflows/test-providers.yml @@ -128,6 +128,10 @@ jobs: run: > breeze release-management prepare-task-sdk-distributions --distribution-format ${{ matrix.package-format }} + - name: "Prepare airflow-ctl package: ${{ matrix.package-format }}" + run: > + breeze release-management prepare-airflow-ctl-distributions + --distribution-format ${{ matrix.package-format }} - name: "Verify ${{ matrix.package-format }} packages with twine" run: | uv tool uninstall twine || true diff --git a/Dockerfile b/Dockerfile index 7cc18dbd44158..4651577180af3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1067,8 +1067,11 @@ function install_airflow_and_providers_from_docker_context_files(){ install_airflow_core_distribution=("apache-airflow-core==${AIRFLOW_VERSION}") fi - # Find Provider/TaskSDK/CTL distributions in docker-context files - readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,airflowctl}*.{whl,tar.gz} 2>/dev/null || true) + # Find Provider/TaskSDK/CTL distributions in docker-context files. + # NOTE: the ctl wheel is named ``apache_airflow_ctl-*.whl`` (distribution + # ``apache-airflow-ctl``), not ``apache_airflow_airflowctl-*.whl`` — the + # glob must say ``ctl``, not ``airflowctl``. + readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,ctl}*.{whl,tar.gz} 2>/dev/null || true) echo echo "${COLOR_BLUE}Found provider distributions in docker-context-files folder: ${airflow_distributions[*]}${COLOR_RESET}" echo diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml index 4c0e33d62c665..783ac958fa57b 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -18,14 +18,14 @@ [build-system] requires = [ "gitdb==4.0.12", - "GitPython==3.1.46", + "GitPython==3.1.50", "hatchling==1.29.0", - "packaging==26.1", - "pathspec==1.0.4", + "packaging==26.2", + "pathspec==1.1.1", "pluggy==1.6.0", "smmap==5.0.3", "tomli==2.4.1; python_version < '3.11'", - "trove-classifiers==2026.1.14.14", + "trove-classifiers==2026.5.7.17", ] build-backend = "hatchling.build" @@ -64,10 +64,11 @@ classifiers = [ ] # Version is defined in src/airflow/__init__.py and it is automatically synchronized by prek hook -version = "3.2.1" +version = "3.3.0" dependencies = [ "a2wsgi>=1.10.8", + "cachetools>=6.0.0", # aiosqlite 0.22.0 has a problem with hanging pytest sessions and we excluded it # See https://github.com/omnilib/aiosqlite/issues/369 # It seems that while our test issues are fixed in 0.22.1, sqlalchemy 2 itself @@ -96,9 +97,7 @@ dependencies = [ "dill>=0.2.2", "fastapi[standard-no-fastapi-cloud-cli]>=0.129.0", "uvicorn>=0.37.0", - # Starlette 1.0.0 breaks the API server. Needs more investigation - # https://github.com/apache/airflow/issues/64116 - "starlette>=0.45.0,<1", + "starlette>=0.45.0", "httpx>=0.25.0", 'importlib_metadata>=6.5;python_version<"3.12"', 'importlib_metadata>=7.0;python_version>="3.12"', @@ -148,10 +147,10 @@ dependencies = [ "tenacity>=8.3.0", "termcolor>=3.0.0", "typing-extensions>=4.14.1", - # https://github.com/apache/airflow/issues/56369 , rework universal-pathlib usage "universal-pathlib>=0.3.8", "uuid6>=2024.7.10", - "apache-airflow-task-sdk==1.2.1", + "apache-airflow-task-sdk<1.4.0,>=1.3.0", + "apache-airflow-ctl<0.1.6,>=0.1.5", # pre-installed providers "apache-airflow-providers-common-compat>=1.7.4", "apache-airflow-providers-common-io>=1.6.3", @@ -249,6 +248,7 @@ exclude = [ "../shared/secrets_backend/src/airflow_shared/secrets_backend" = "src/airflow/_shared/secrets_backend" "../shared/secrets_masker/src/airflow_shared/secrets_masker" = "src/airflow/_shared/secrets_masker" "../shared/serialization/src/airflow_shared/serialization" = "src/airflow/_shared/serialization" +"../shared/state/src/airflow_shared/state" = "src/airflow/_shared/state" "../shared/timezones/src/airflow_shared/timezones" = "src/airflow/_shared/timezones" "../shared/listeners/src/airflow_shared/listeners" = "src/airflow/_shared/listeners" "../shared/plugins_manager/src/airflow_shared/plugins_manager" = "src/airflow/_shared/plugins_manager" @@ -285,6 +285,7 @@ dev = [ "apache-airflow-core[all]", "apache-airflow-ctl", "apache-airflow-devel-common", + "apache-airflow-task-sdk", # TODO(potiuk): eventually we do not want any providers nor apache-airflow extras to be needed for # airflow-core tests "apache-airflow[pandas,polars]", @@ -311,6 +312,10 @@ dev = [ docs = [ "apache-airflow-devel-common[docs]" ] +mypy = [ + "apache-airflow-devel-common[mypy]", +] + [tool.uv] @@ -318,6 +323,7 @@ required-version = ">=0.11.8" [tool.uv.sources] apache-airflow-core = {workspace = true} +apache-airflow-ctl = {workspace = true} apache-airflow-devel-common = { workspace = true } [tool.airflow] @@ -331,6 +337,7 @@ shared_distributions = [ "apache-airflow-shared-secrets-backend", "apache-airflow-shared-secrets-masker", "apache-airflow-shared-serialization", + "apache-airflow-shared-state", "apache-airflow-shared-timezones", "apache-airflow-shared-plugins-manager", "apache-airflow-shared-providers-discovery", diff --git a/airflow-ctl/src/airflowctl/__init__.py b/airflow-ctl/src/airflowctl/__init__.py index d2dd19e81e26f..a085e738d8394 100644 --- a/airflow-ctl/src/airflowctl/__init__.py +++ b/airflow-ctl/src/airflowctl/__init__.py @@ -19,4 +19,4 @@ __path__ = __import__("pkgutil").extend_path(__path__, __name__) -__version__ = "0.1.4" +__version__ = "0.1.5" diff --git a/scripts/docker/install_from_docker_context_files.sh b/scripts/docker/install_from_docker_context_files.sh index 5ce45267f8fbd..089b0f2d74bd4 100644 --- a/scripts/docker/install_from_docker_context_files.sh +++ b/scripts/docker/install_from_docker_context_files.sh @@ -85,8 +85,11 @@ function install_airflow_and_providers_from_docker_context_files(){ install_airflow_core_distribution=("apache-airflow-core==${AIRFLOW_VERSION}") fi - # Find Provider/TaskSDK/CTL distributions in docker-context files - readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,airflowctl}*.{whl,tar.gz} 2>/dev/null || true) + # Find Provider/TaskSDK/CTL distributions in docker-context files. + # NOTE: the ctl wheel is named ``apache_airflow_ctl-*.whl`` (distribution + # ``apache-airflow-ctl``), not ``apache_airflow_airflowctl-*.whl`` — the + # glob must say ``ctl``, not ``airflowctl``. + readarray -t airflow_distributions< <(python /scripts/docker/get_distribution_specs.py /docker-context-files/apache?airflow?{providers,task?sdk,ctl}*.{whl,tar.gz} 2>/dev/null || true) echo echo "${COLOR_BLUE}Found provider distributions in docker-context-files folder: ${airflow_distributions[*]}${COLOR_RESET}" echo diff --git a/scripts/in_container/install_airflow_and_providers.py b/scripts/in_container/install_airflow_and_providers.py index 80d7fa601cb64..21c33b28047b5 100755 --- a/scripts/in_container/install_airflow_and_providers.py +++ b/scripts/in_container/install_airflow_and_providers.py @@ -1183,9 +1183,11 @@ def _install_airflow_ctl_with_constraints(installation_spec: InstallationSpec, g "pip", "install", ] - # if airflow is also being installed we should add airflow to the base_install_providers_cmd - # to avoid accidentally upgrading airflow to a version that is different from installed in the - # previous step + # Install the ctl distribution itself, plus pin airflow alongside (if it's being + # installed) so the resolver does not accidentally upgrade airflow to a version + # different from the one installed in the previous step. + if installation_spec.airflow_ctl_distribution: + base_install_airflow_ctl_cmd.append(installation_spec.airflow_ctl_distribution) if installation_spec.airflow_distribution: base_install_airflow_ctl_cmd.append(installation_spec.airflow_distribution) install_airflow_ctl_cmd = base_install_airflow_ctl_cmd.copy() @@ -1243,6 +1245,13 @@ def _install_only_airflow_airflow_core_task_sdk_with_constraints( f"{installation_spec.airflow_task_sdk_distribution} with constraints" ) console.print() + if installation_spec.airflow_ctl_distribution: + base_install_airflow_cmd.append(installation_spec.airflow_ctl_distribution) + console.print( + f"\n[bright_blue]Installing airflow ctl distribution alongside core: " + f"{installation_spec.airflow_ctl_distribution}" + ) + console.print() install_airflow_cmd = base_install_airflow_cmd.copy() if installation_spec.airflow_constraints_location: console.print(f"[bright_blue]Use constraints: {installation_spec.airflow_constraints_location}") From e65c3ecab711583f3db3fd7ac4e52653b3f33783 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Sun, 24 May 2026 21:50:34 +0200 Subject: [PATCH 07/10] [airflow-ctl/v0-1-test] airflowctl: make required CLI params positional, keep optional as --flag (#66768) (#67387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airflowctl: make required CLI params positional, keep optional as --flag Auto-generated commands such as ``airflowctl dags get-details`` now accept required primitive parameters positionally: airflowctl dags get-details my_dag_id instead of the previous ``--dag-id my_dag_id`` form. Optional parameters and booleans keep the ``--flag`` form. This follows the dev-list lazy consensus on airflowctl parameter style. A parameter is considered required when the operation method declares it without a default and without ``| None`` in its annotation. Datamodel- expanded body fields are unaffected — they are not "parameters of the operation method" in this sense and continue to use ``--flag``. * tests: tmp_path fixture for command-factory; positional form for integration tests Two follow-ups to the positional-required-args change: - ``TestCommandFactory._save_temp_operations_py`` previously wrote a shared ``test_command.py`` in cwd; under pytest-xdist that file is raced by workers, so ``next(arg for arg in jobs_list_args if ...)`` in one test could see content written by another and raise ``StopIteration``. Helper now takes the per-test ``tmp_path`` and returns the full path. The classmethod ``teardown_method`` that removed the shared file is no longer needed (pytest auto-cleans ``tmp_path``). - The Airflow CTL PROD-image integration tests still invoked converted parameters with the old ``--flag value`` form (e.g. ``--variable-key=X``, ``--section X --option Y``, ``--dag-id=example_bash_operator``). Updated each occurrence to the positional form that the regenerated CLI now expects. Optional parameters (``--logical-date``, ``--run-after``, ``--is-paused``, ``--state``, ``--limit``, etc.) stay as ``--flag``. --------- (cherry picked from commit ffa426b64d7d99a66482b817887ee74d399824c4) Signed-off-by: 1fanwang <1fannnw@gmail.com> Co-authored-by: Stefan Wang <1fannnw@gmail.com> --- .../test_airflowctl_commands.py | 87 +++++----- .../test_config_sensitive_masking.py | 4 +- airflow-ctl/src/airflowctl/ctl/cli_config.py | 75 +++++++-- .../tests/airflow_ctl/ctl/test_cli_config.py | 148 ++++++++++++++---- 4 files changed, 227 insertions(+), 87 deletions(-) diff --git a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py index 1f893c37efd04..b8b49ad3ae778 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py @@ -54,12 +54,12 @@ 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 @@ -67,62 +67,62 @@ def date_param(): "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", - # DAGs commands + "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", - # DAG Run commands + # Dag Run commands + '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 commands - need a Dag run with completed tasks + '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", @@ -174,9 +174,7 @@ def test_hardcoded_xcom_key_would_collide(): ) def test_airflowctl_commands(command: str, run_command): """Test airflowctl commands using docker-compose environment.""" - env_vars = {"AIRFLOW_CLI_DEBUG_MODE": "true"} - - run_command(command, env_vars, skip_login=True) + run_command(command=command, env_vars={"AIRFLOW_CLI_DEBUG_MODE": "true"}, skip_login=True) @pytest.mark.parametrize( @@ -186,12 +184,15 @@ def test_airflowctl_commands(command: str, run_command): ) def test_airflowctl_commands_skip_keyring(command: str, api_token: str, run_command): """Test airflowctl commands using docker-compose environment without using keyring.""" - env_vars = {} - env_vars["AIRFLOW_CLI_TOKEN"] = api_token - env_vars["AIRFLOW_CLI_DEBUG_MODE"] = "false" - env_vars["AIRFLOW_CLI_ENVIRONMENT"] = "nokeyring" - - run_command(command, env_vars, skip_login=True) + run_command( + command=command, + env_vars={ + "AIRFLOW_CLI_TOKEN": api_token, + "AIRFLOW_CLI_DEBUG_MODE": "false", + "AIRFLOW_CLI_ENVIRONMENT": "nokeyring", + }, + skip_login=True, + ) @pytest.mark.parametrize("command", NO_AUTH_TEST_COMMANDS) diff --git a/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py index 776629da3a26e..a9e98d9365c9d 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py @@ -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", ] diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index aa96304f501fb..1d3c30121b619 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -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 = [ @@ -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, } @@ -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, @@ -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( diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py index e0278cd7c5348..945b5abb83378 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py @@ -19,6 +19,7 @@ import argparse from argparse import BooleanOptionalAction +from pathlib import Path from textwrap import dedent import httpx @@ -158,12 +159,10 @@ def test_args_list(): def test_args_get(): return [ ( - "--backfill-id", + "backfill_id", { "help": "backfill_id for get operation in BackfillsOperations", - "default": None, "type": str, - "dest": None, }, ), ( @@ -182,12 +181,10 @@ def test_args_get(): def test_args_delete(): return [ ( - "--backfill-id", + "backfill_id", { "help": "backfill_id for delete operation in BackfillsOperations", - "default": None, "type": str, - "dest": None, }, ), ( @@ -204,26 +201,26 @@ def test_args_delete(): class TestCommandFactory: @classmethod - def _save_temp_operations_py(cls, temp_file: str, file_content) -> None: + def _save_temp_operations_py(cls, tmp_path: Path, file_content: str) -> Path: """ Save a temporary operations.py file with a simple Command Class to test the command factory. - """ - with open(temp_file, "w") as f: - f.write(dedent(file_content)) - def teardown_method(self): - """ - Remove the temporary file after the test. + Writing inside a per-test ``tmp_path`` keeps the file isolated under + parallel execution (pytest-xdist), avoiding cross-worker overwrites of + a shared ``test_command.py`` in the cwd. Returns the full path. """ - try: - import os - - os.remove("test_command.py") - except FileNotFoundError: - pass + temp_file = tmp_path / "test_command.py" + temp_file.write_text(dedent(file_content)) + return temp_file def test_command_factory( - self, no_op_method, test_args_create, test_args_list, test_args_get, test_args_delete + self, + tmp_path, + no_op_method, + test_args_create, + test_args_list, + test_args_get, + test_args_delete, ): """ Test the command factory. @@ -231,9 +228,8 @@ def test_command_factory( # Create temporary file with pytest and write simple Command Class(check airflow-ctl/src/airflowctl/api/operations.py) to file # to test the command factory # Create a temporary file - temp_file = "test_command.py" - self._save_temp_operations_py( - temp_file=temp_file, + temp_file = self._save_temp_operations_py( + tmp_path=tmp_path, file_content=""" class NotAnOperation: def test_method(self): @@ -260,7 +256,7 @@ def delete(self, backfill_id: str) -> ServerResponseError | None: """, ) - command_factory = CommandFactory(file_path=temp_file) + command_factory = CommandFactory(file_path=str(temp_file)) generated_group_commands = command_factory.group_commands for generated_group_command in generated_group_commands: @@ -287,20 +283,19 @@ def delete(self, backfill_id: str) -> ServerResponseError | None: for arg, test_arg in zip(sub_command.args, test_args_get): assert arg.flags[0] == test_arg[0] assert arg.kwargs["help"] == test_arg[1]["help"] - assert arg.kwargs["default"] == test_arg[1]["default"] + assert arg.kwargs.get("default") == test_arg[1].get("default") assert arg.kwargs["type"] == test_arg[1]["type"] elif sub_command.name == "delete": for arg, test_arg in zip(sub_command.args, test_args_delete): assert arg.flags[0] == test_arg[0] assert arg.kwargs["help"] == test_arg[1]["help"] - assert arg.kwargs["default"] == test_arg[1]["default"] + assert arg.kwargs.get("default") == test_arg[1].get("default") assert arg.kwargs["type"] == test_arg[1]["type"] - def test_command_factory_optional_bool_uses_boolean_optional_action(self): + def test_command_factory_optional_bool_uses_boolean_optional_action(self, tmp_path): """Optional bool parameters should support --flag and --no-flag forms.""" - temp_file = "test_command.py" - self._save_temp_operations_py( - temp_file=temp_file, + temp_file = self._save_temp_operations_py( + tmp_path=tmp_path, file_content=""" class JobsOperations(BaseOperations): def list(self, is_alive: bool | None = None) -> JobCollectionResponse | ServerResponseError: @@ -309,7 +304,7 @@ def list(self, is_alive: bool | None = None) -> JobCollectionResponse | ServerRe """, ) - command_factory = CommandFactory(file_path=temp_file) + command_factory = CommandFactory(file_path=str(temp_file)) generated_group_commands = command_factory.group_commands jobs_list_args = [] @@ -326,6 +321,97 @@ def list(self, is_alive: bool | None = None) -> JobCollectionResponse | ServerRe assert is_alive_arg.kwargs["default"] is None assert is_alive_arg.kwargs["type"] is bool + def test_command_factory_required_primitive_param_is_positional(self, tmp_path): + """Required primitive parameters (no default, not Optional) become positional arguments. + + Following the dev-list lazy consensus on ``airflowctl`` parameter style + (``_), + required parameters of auto-generated commands should be positional and + optional parameters keep the ``--flag`` form. + """ + temp_file = self._save_temp_operations_py( + tmp_path=tmp_path, + file_content=""" + class WidgetsOperations(BaseOperations): + def get(self, widget_id: str) -> WidgetResponse | ServerResponseError: + self.response = self.client.get(f"widgets/{widget_id}") + return WidgetResponse.model_validate_json(self.response.content) + def delete(self, widget_id: str) -> ServerResponseError | None: + self.response = self.client.delete(f"widgets/{widget_id}") + return None + def update_version( + self, widget_id: str, version: int, note: str | None = None + ) -> WidgetResponse | ServerResponseError: + self.response = self.client.patch( + f"widgets/{widget_id}/version/{version}", + json={"note": note}, + ) + return WidgetResponse.model_validate_json(self.response.content) + """, + ) + + command_factory = CommandFactory(file_path=str(temp_file)) + sub_commands = {} + for generated_group_command in command_factory.group_commands: + if generated_group_command.name != "widgets": + continue + for sub_command in generated_group_command.subcommands: + sub_commands[sub_command.name] = sub_command + + # get: single required str -> positional + get_args = list(sub_commands["get"].args) + widget_id_arg = next(arg for arg in get_args if arg.flags[0] in ("widget_id", "--widget-id")) + assert widget_id_arg.flags == ("widget_id",) + assert widget_id_arg.kwargs["type"] is str + assert "default" not in widget_id_arg.kwargs + assert "action" not in widget_id_arg.kwargs + + # delete: same shape + delete_args = list(sub_commands["delete"].args) + delete_widget_id_arg = next( + arg for arg in delete_args if arg.flags[0] in ("widget_id", "--widget-id") + ) + assert delete_widget_id_arg.flags == ("widget_id",) + + # update-version: two required (str + int) positional, ``note`` keeps --flag form + update_args = list(sub_commands["update-version"].args) + update_widget_id_arg = next( + arg for arg in update_args if arg.flags[0] in ("widget_id", "--widget-id") + ) + version_arg = next(arg for arg in update_args if arg.flags[0] in ("version", "--version")) + note_arg = next(arg for arg in update_args if arg.flags[0] in ("note", "--note")) + assert update_widget_id_arg.flags == ("widget_id",) + assert version_arg.flags == ("version",) + assert version_arg.kwargs["type"] is int + assert note_arg.flags == ("--note",) + assert note_arg.kwargs["default"] is None + + def test_command_factory_primitive_param_with_default_keeps_flag(self, tmp_path): + """Primitive parameters with a default value keep the ``--flag`` form.""" + temp_file = self._save_temp_operations_py( + tmp_path=tmp_path, + file_content=""" + class WidgetsOperations(BaseOperations): + def list(self, limit: int = 100) -> WidgetCollectionResponse | ServerResponseError: + self.response = self.client.get("widgets", params={"limit": limit}) + return WidgetCollectionResponse.model_validate_json(self.response.content) + """, + ) + + command_factory = CommandFactory(file_path=str(temp_file)) + list_args = [] + for generated_group_command in command_factory.group_commands: + if generated_group_command.name != "widgets": + continue + for sub_command in generated_group_command.subcommands: + if sub_command.name == "list": + list_args = list(sub_command.args) + break + + limit_arg = next(arg for arg in list_args if arg.flags[0] in ("limit", "--limit")) + assert limit_arg.flags == ("--limit",) + assert limit_arg.kwargs["type"] is int + class TestCliConfigMethods: @pytest.mark.parametrize( From 192441e53b5b0c5f431bc406145e4346dd9d331f Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Sun, 24 May 2026 21:52:39 +0200 Subject: [PATCH 08/10] [airflow-ctl/v0-1-test] Add dags next execution command #66172 (#66188) (#67386) * Add airflowctl dags next-execution command #66172 * Add generated OpenAPI spec and UI types * Revert "Add generated OpenAPI spec and UI types" This reverts commit 6748ed8c45a8cb73ca8c31e711557012db0a30fc. * Update help text Dag definition --------- (cherry picked from commit 16ad4794f5a6c70480de7b137561f7f1bcdc2735) Co-authored-by: Haseeb Malik <118837269+haseebmalik18@users.noreply.github.com> --- .../test_airflowctl_commands.py | 1 + airflow-ctl/docs/images/command_hashes.txt | 2 +- airflow-ctl/docs/images/output_dags.svg | 126 +++++++++--------- airflow-ctl/src/airflowctl/ctl/cli_config.py | 9 ++ .../airflowctl/ctl/commands/dag_command.py | 31 +++++ .../ctl/commands/test_dag_command.py | 72 ++++++++++ 6 files changed, 179 insertions(+), 62 deletions(-) diff --git a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py index b8b49ad3ae778..e1cfc665804d9 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py @@ -86,6 +86,7 @@ def 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 example_bash_operator", + "dags next-execution example_bash_operator", "dags pause example_bash_operator", "dags unpause example_bash_operator", # Dag Run commands diff --git a/airflow-ctl/docs/images/command_hashes.txt b/airflow-ctl/docs/images/command_hashes.txt index 8e590f3b820cd..53c93e7546d1e 100644 --- a/airflow-ctl/docs/images/command_hashes.txt +++ b/airflow-ctl/docs/images/command_hashes.txt @@ -4,7 +4,7 @@ auth:d79e9c7d00c432bdbcbc2a86e2e32053 backfill:74c8737b0a62a86ed3605fa9e6165874 config:a3d936cb15fe3b547bf6c82cf93d923f connections:942f9f88cb908c28bf5c19159fc5065b -dags:e2a18f90b1bd150be981cef6fef91858 +dags:6b38e6bcd491bc1941e7814b77e63bde dagrun:c32e0011aa9a845456c778786717208e jobs:a5b644c5da8889443bb40ee10b599270 pools:19efe105b9515ab1926ebcaf0e028d71 diff --git a/airflow-ctl/docs/images/output_dags.svg b/airflow-ctl/docs/images/output_dags.svg index f38d12d60e343..7f95b26bdbfe0 100644 --- a/airflow-ctl/docs/images/output_dags.svg +++ b/airflow-ctl/docs/images/output_dags.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - + - - Usage:airflowctl dags [-hCOMMAND... - -Perform Dags operations - -Positional Arguments: -COMMAND -deleteDelete a Dag by its ID -getRetrieve a Dag by its ID -get-detailsRetrieve detailed information for a Dag -get-import-errorRetrieve a Dag import error by its ID -get-statsRetrieve run statistics for one or more Dags -get-tagsList all tags used across Dags -get-versionRetrieve a specific version of a Dag -listList all Dags -list-import-errors -List all Dag import errors -list-versionList all versions of a Dag -list-warningList all Dag warnings -pausePause a Dag -triggerTrigger a new Dag run -unpauseUnpause a Dag -updateUpdate properties of a Dag - -Options: --h--helpshow this help message and exit + + Usage:airflowctl dags [-hCOMMAND... + +Perform Dags operations + +Positional Arguments: +COMMAND +deleteDelete a Dag by its ID +getRetrieve a Dag by its ID +get-detailsRetrieve detailed information for a Dag +get-import-errorRetrieve a Dag import error by its ID +get-statsRetrieve run statistics for one or more Dags +get-tagsList all tags used across Dags +get-versionRetrieve a specific version of a Dag +listList all Dags +list-import-errors +List all Dag import errors +list-versionList all versions of a Dag +list-warningList all Dag warnings +next-executionShow the next scheduled execution time for a Dag +pausePause a Dag +triggerTrigger a new Dag run +unpauseUnpause a Dag +updateUpdate properties of a Dag + +Options: +-h--helpshow this help message and exit diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 1d3c30121b619..11ff4542e01ef 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -959,6 +959,15 @@ def merge_commands( ) DAG_COMMANDS = ( + ActionCommand( + name="next-execution", + help="Show the next scheduled execution time for a Dag", + func=lazy_load_command("airflowctl.ctl.commands.dag_command.next_execution"), + args=( + ARG_DAG_ID, + ARG_OUTPUT, + ), + ), ActionCommand( name="pause", help="Pause a Dag", diff --git a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py index 33b3d7c95fefa..301821f9c2c3c 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py @@ -72,3 +72,34 @@ def unpause(args, api_client=NEW_API_CLIENT) -> None: api_client=api_client, output=args.output, ) + + +_NEXT_EXECUTION_FIELDS = ( + "next_dagrun_logical_date", + "next_dagrun_data_interval_start", + "next_dagrun_data_interval_end", + "next_dagrun_run_after", +) + + +@provide_api_client(kind=ClientKind.CLI) +def next_execution(args, api_client=NEW_API_CLIENT) -> dict | None: + """Show next scheduled execution time for a DAG.""" + try: + response = api_client.dags.get(dag_id=args.dag_id) + except ServerResponseError as e: + rich.print(f"[red]Error retrieving DAG {args.dag_id}: {e}[/red]") + sys.exit(1) + + next_exec_data = {field: getattr(response, field) for field in _NEXT_EXECUTION_FIELDS} + + if all(value is None for value in next_exec_data.values()): + rich.print(f"[yellow]No upcoming run scheduled for DAG {args.dag_id}.[/yellow]") + return None + + result = next_exec_data + AirflowConsole().print_as( + data=[result], + output=args.output, + ) + return result diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py index 600a553109b88..d00eaa5791589 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py @@ -86,6 +86,36 @@ class TestDagCommands: is_stale=False, ) + dag_response_no_schedule = DAGResponse( + dag_id=dag_id, + dag_display_name=dag_display_name, + is_paused=True, + last_parsed_time=datetime.datetime(2024, 12, 31, 23, 59, 59), + last_expired=datetime.datetime(2025, 1, 1, 0, 0, 0), + fileloc="fileloc", + relative_fileloc="relative_fileloc", + description="description", + timetable_summary=None, + timetable_description=None, + timetable_partitioned=False, + timetable_periodic=False, + tags=[], + max_active_tasks=1, + max_active_runs=1, + max_consecutive_failed_dag_runs=1, + has_task_concurrency_limits=False, + has_import_errors=False, + next_dagrun_logical_date=None, + next_dagrun_data_interval_start=None, + next_dagrun_data_interval_end=None, + next_dagrun_run_after=None, + owners=["apache-airflow"], + is_backfillable=False, + file_token="file_token", + bundle_name="bundle_name", + is_stale=False, + ) + def test_pause_dag(self, api_client_maker, monkeypatch): api_client = api_client_maker( path=f"/api/v2/dags/{self.dag_id}", @@ -139,3 +169,45 @@ def test_unpause_fail(self, api_client_maker, monkeypatch): self.parser.parse_args(["dags", "unpause", self.dag_id]), api_client=api_client, ) + + def test_next_execution(self, api_client_maker): + api_client = api_client_maker( + path=f"/api/v2/dags/{self.dag_id}", + response_json=self.dag_response_paused.model_dump(mode="json"), + expected_http_status_code=200, + kind=ClientKind.CLI, + ) + result = dag_command.next_execution( + self.parser.parse_args(["dags", "next-execution", self.dag_id]), + api_client=api_client, + ) + assert result["next_dagrun_logical_date"] == datetime.datetime(2025, 1, 1, 0, 0, 0) + assert result["next_dagrun_data_interval_start"] == datetime.datetime(2025, 1, 1, 0, 0, 0) + assert result["next_dagrun_data_interval_end"] == datetime.datetime(2025, 1, 1, 0, 0, 0) + assert result["next_dagrun_run_after"] == datetime.datetime(2025, 1, 1, 0, 0, 0) + + def test_next_execution_no_schedule(self, api_client_maker): + api_client = api_client_maker( + path=f"/api/v2/dags/{self.dag_id}", + response_json=self.dag_response_no_schedule.model_dump(mode="json"), + expected_http_status_code=200, + kind=ClientKind.CLI, + ) + result = dag_command.next_execution( + self.parser.parse_args(["dags", "next-execution", self.dag_id]), + api_client=api_client, + ) + assert result is None + + def test_next_execution_fail(self, api_client_maker): + api_client = api_client_maker( + path=f"/api/v2/dags/{self.dag_id}", + response_json={"detail": "DAG not found"}, + expected_http_status_code=404, + kind=ClientKind.CLI, + ) + with pytest.raises(SystemExit): + dag_command.next_execution( + self.parser.parse_args(["dags", "next-execution", self.dag_id]), + api_client=api_client, + ) From 871710c054dd7eccd327bc3c0e614367ce502f20 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Tue, 26 May 2026 18:16:23 +0200 Subject: [PATCH 09/10] Sync main to backport branch for airflowctl (#67559) --- airflow-ctl/.pre-commit-config.yaml | 4 +- .../installation/installing-from-sources.rst | 16 +- airflow-ctl/pyproject.toml | 10 +- .../airflowctl/api/datamodels/generated.py | 272 ++++++++++++++++-- airflow-ctl/src/airflowctl/api/operations.py | 13 +- .../ctl/commands/connection_command.py | 1 + .../tests/airflow_ctl/api/test_operations.py | 116 ++++++++ .../ctl/commands/test_connections_command.py | 35 +++ .../ctl/commands/test_dag_command.py | 4 + 9 files changed, 432 insertions(+), 39 deletions(-) diff --git a/airflow-ctl/.pre-commit-config.yaml b/airflow-ctl/.pre-commit-config.yaml index a5773e94aabca..4561533c574d0 100644 --- a/airflow-ctl/.pre-commit-config.yaml +++ b/airflow-ctl/.pre-commit-config.yaml @@ -16,7 +16,7 @@ # under the License. --- default_stages: [pre-commit, pre-push] -minimum_prek_version: '0.2.0' +minimum_prek_version: '0.3.4' default_language_version: python: python3 node: 22.19.0 @@ -27,7 +27,7 @@ repos: - id: mypy-airflow-ctl name: Run mypy for airflow-ctl language: python - entry: ../scripts/ci/prek/mypy_local_folder.py airflow-ctl + entry: ../scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py airflow-ctl pass_filenames: false files: ^.*\.py$ require_serial: true diff --git a/airflow-ctl/docs/installation/installing-from-sources.rst b/airflow-ctl/docs/installation/installing-from-sources.rst index 8f13d381db529..ebf027123509e 100644 --- a/airflow-ctl/docs/installation/installing-from-sources.rst +++ b/airflow-ctl/docs/installation/installing-from-sources.rst @@ -52,7 +52,7 @@ a ``INSTALL`` file containing details on how you can build and install airflowct Release integrity ''''''''''''''''' -`PGP signatures KEYS `__ +`PGP signatures KEYS `__ It is essential that you verify the integrity of the downloaded files using the PGP or SHA signatures. The PGP signatures can be verified using GPG or PGP. Please download the KEYS as well as the asc @@ -144,14 +144,14 @@ and SHA sum files with the script below: #!/bin/bash airflowctl_version="{{ airflowctl_version }}" ctl_download_dir="$(mktemp -d)" - pip download --no-deps "apache-airflow-ctl==${airflowctl_version}" --dest "${airflow_download_dir}" - curl "https://downloads.apache.org/airflowctl/${airflowctl_version}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.asc" \ - -L -o "${airflowctl_download_dir}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.asc" - curl "https://downloads.apache.org/airflow/${airflowctl_version}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.sha512" \ - -L -o "${airflowctl_download_dir}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.sha512" + pip download --no-deps "apache-airflow-ctl==${airflowctl_version}" --dest "${ctl_download_dir}" + curl "https://downloads.apache.org/airflow/airflow-ctl/${airflowctl_version}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.asc" \ + -L -o "${ctl_download_dir}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.asc" + curl "https://downloads.apache.org/airflow/airflow-ctl/${airflowctl_version}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.sha512" \ + -L -o "${ctl_download_dir}/apache_airflow_ctl-${airflowctl_version}-py3-none-any.whl.sha512" echo - echo "Please verify files downloaded to ${airflowctl_download_dir}" - ls -la "${airflowctl_download_dir}" + echo "Please verify files downloaded to ${ctl_download_dir}" + ls -la "${ctl_download_dir}" echo Once you verify the files following the instructions from previous chapter you can remove the temporary diff --git a/airflow-ctl/pyproject.toml b/airflow-ctl/pyproject.toml index 9aeaf44d31b8d..acca75209720c 100644 --- a/airflow-ctl/pyproject.toml +++ b/airflow-ctl/pyproject.toml @@ -76,11 +76,11 @@ apache-airflow-ctl = "airflowctl.__main__:main" [build-system] requires = [ "hatchling==1.29.0", - "packaging==26.1", - "pathspec==1.0.4", + "packaging==26.2", + "pathspec==1.1.1", "pluggy==1.6.0", "tomli==2.4.1; python_version < '3.11'", - "trove-classifiers==2026.1.14.14", + "trove-classifiers==2026.5.20.19", ] build-backend = "hatchling.build" @@ -154,6 +154,10 @@ codegen = [ ] # uv run --verbose --group codegen --project apache-airflow-ctl --directory airflow-ctl/ datamodel-codegen --url="http://0.0.0.0:28080/auth/openapi.json" --output=src/airflowctl/api/datamodels/auth_generated.py +mypy = [ + "apache-airflow-devel-common[mypy]", +] + [tool.datamodel-codegen] capitalise-enum-members=true # `State.RUNNING` not `State.running` disable-timestamp=true diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index 510a59c3b09be..f05fa65cf56f8 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -49,6 +49,27 @@ class AssetAliasResponse(BaseModel): group: Annotated[str, Field(title="Group")] +class AssetStateBody(BaseModel): + """ + Request body for setting an asset state value. + """ + + model_config = ConfigDict( + extra="forbid", + ) + value: Annotated[str, Field(max_length=65535, title="Value")] + + +class AssetStateResponse(BaseModel): + """ + A single asset state key/value pair with metadata. + """ + + key: Annotated[str, Field(title="Key")] + value: Annotated[str, Field(title="Value")] + updated_at: Annotated[datetime, Field(title="Updated At")] + + class AssetWatcherResponse(BaseModel): """ Asset watcher serializer for responses. @@ -108,6 +129,32 @@ class BulkActionResponse(BaseModel): ] = [] +class BulkDAGRunBody(BaseModel): + """ + Request body for bulk delete operations on Dag Runs. + """ + + model_config = ConfigDict( + extra="forbid", + ) + dag_run_id: Annotated[str, Field(title="Dag Run Id")] + dag_id: Annotated[str | None, Field(title="Dag Id")] = None + + +class BulkDeleteActionBulkDAGRunBody(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + action: Annotated[ + Literal["delete"], Field(description="The action to be performed on the entities.", title="Action") + ] + entities: Annotated[ + list[str | BulkDAGRunBody], + Field(description="A list of entity id/key or entity objects to be deleted.", title="Entities"), + ] + action_on_non_existence: BulkActionNotOnExistence | None = "fail" + + class BulkResponse(BaseModel): """ Serializer for responses to bulk entity operations. @@ -135,6 +182,26 @@ class Note(RootModel[str]): root: Annotated[str, Field(max_length=1000, title="Note")] +class BulkUpdateActionBulkDAGRunBody(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + action: Annotated[ + Literal["update"], Field(description="The action to be performed on the entities.", title="Action") + ] + entities: Annotated[ + list[BulkDAGRunBody], Field(description="A list of entities to be updated.", title="Entities") + ] + update_mask: Annotated[ + list[str] | None, + Field( + description="A list of field names to update for each entity.Only these fields will be applied from the request body to the database model.Any extra fields provided will be ignored.", + title="Update Mask", + ), + ] = None + action_on_non_existence: BulkActionNotOnExistence | None = "fail" + + class TaskIds(RootModel[list]): root: Annotated[list, Field(max_length=2, min_length=2)] @@ -168,11 +235,12 @@ class ClearTaskInstancesBody(BaseModel): run_on_latest_version: Annotated[ bool | None, Field( - description="(Experimental) Run on the latest bundle version of the dag after clearing the task instances.", + description="(Experimental) Run on the latest bundle version of the dag after clearing the task instances. If not specified, falls back to the DAG-level ``rerun_with_latest_version`` parameter, then the ``[core] rerun_with_latest_version`` config option, and finally ``False`` (the historical default for clear/rerun).", title="Run On Latest Version", ), - ] = False + ] = None prevent_running_task: Annotated[bool | None, Field(title="Prevent Running Task")] = False + note: Annotated[Note | None, Field(title="Note")] = None class Value(RootModel[list]): @@ -287,23 +355,20 @@ class DAGRunClearBody(BaseModel): ) dry_run: Annotated[bool | None, Field(title="Dry Run")] = True only_failed: Annotated[bool | None, Field(title="Only Failed")] = False + only_new: Annotated[ + bool | None, + Field( + description="Only queue newly added tasks in the latest Dag version without clearing existing tasks.", + title="Only New", + ), + ] = False run_on_latest_version: Annotated[ bool | None, Field( - description="(Experimental) Run on the latest bundle version of the Dag after clearing the Dag Run.", + description="(Experimental) Run on the latest bundle version of the Dag after clearing the Dag Run. If not specified, falls back to the DAG-level ``rerun_with_latest_version`` parameter, then the ``[core] rerun_with_latest_version`` config option, and finally ``False`` (the historical default for clear/rerun).", title="Run On Latest Version", ), - ] = False - - -class DAGRunPatchStates(str, Enum): - """ - Enum for Dag Run states when updating a Dag Run. - """ - - QUEUED = "queued" - SUCCESS = "success" - FAILED = "failed" + ] = None class DAGSourceResponse(BaseModel): @@ -356,6 +421,16 @@ class DagRunAssetReference(BaseModel): partition_key: Annotated[str | None, Field(title="Partition Key")] = None +class DagRunMutableStates(str, Enum): + """ + Dag Run states from which the run may be mutated (patched, deleted). + """ + + QUEUED = "queued" + SUCCESS = "success" + FAILED = "failed" + + class DagRunState(str, Enum): """ All possible states that a DagRun can be in. @@ -394,6 +469,7 @@ class DagRunType(str, Enum): BACKFILL = "backfill" SCHEDULED = "scheduled" MANUAL = "manual" + OPERATOR_TRIGGERED = "operator_triggered" ASSET_TRIGGERED = "asset_triggered" ASSET_MATERIALIZATION = "asset_materialization" @@ -638,6 +714,15 @@ class MaterializeAssetBody(BaseModel): partition_key: Annotated[str | None, Field(title="Partition Key")] = None +class NewTaskResponse(BaseModel): + """ + Lightweight response for new tasks that don't have TaskInstances yet. + """ + + task_id: Annotated[str, Field(title="Task Id")] + task_display_name: Annotated[str, Field(title="Task Display Name")] + + class PluginImportErrorResponse(BaseModel): """ Plugin Import Error serializer for responses. @@ -888,6 +973,28 @@ class TaskOutletAssetReference(BaseModel): updated_at: Annotated[datetime, Field(title="Updated At")] +class TaskStateBody(BaseModel): + """ + Request body for setting a task state value. + """ + + model_config = ConfigDict( + extra="forbid", + ) + value: Annotated[str, Field(max_length=65535, title="Value")] + + +class TaskStateResponse(BaseModel): + """ + A single task state key/value pair with metadata. + """ + + key: Annotated[str, Field(title="Key")] + value: Annotated[str, Field(title="Value")] + updated_at: Annotated[datetime, Field(title="Updated At")] + expires_at: Annotated[datetime | None, Field(title="Expires At")] = None + + class TimeDelta(BaseModel): """ TimeDelta can be used to interact with datetime.timedelta objects. @@ -1118,6 +1225,15 @@ class AssetResponse(BaseModel): last_asset_event: LastAssetEventResponse | None = None +class AssetStateCollectionResponse(BaseModel): + """ + All asset state entries for an asset. + """ + + asset_states: Annotated[list[AssetStateResponse], Field(title="Asset States")] + total_entries: Annotated[int, Field(title="Total Entries")] + + class BackfillPostBody(BaseModel): """ Object used for create backfill request. @@ -1130,10 +1246,16 @@ class BackfillPostBody(BaseModel): from_date: Annotated[datetime, Field(title="From Date")] to_date: Annotated[datetime, Field(title="To Date")] run_backwards: Annotated[bool | None, Field(title="Run Backwards")] = False - dag_run_conf: Annotated[dict[str, Any] | None, Field(title="Dag Run Conf")] = {} + dag_run_conf: Annotated[dict[str, Any] | None, Field(title="Dag Run Conf")] = None reprocess_behavior: ReprocessBehavior | None = "none" max_active_runs: Annotated[int | None, Field(title="Max Active Runs")] = 10 - run_on_latest_version: Annotated[bool | None, Field(title="Run On Latest Version")] = True + run_on_latest_version: Annotated[ + bool | None, + Field( + description="Run on the latest bundle version of the Dag for each backfilled run. If not specified, falls back to the DAG-level ``rerun_with_latest_version`` parameter, then the ``[core] rerun_with_latest_version`` config option, and finally ``True`` (the historical default for backfills).", + title="Run On Latest Version", + ), + ] = None class BackfillResponse(BaseModel): @@ -1155,6 +1277,19 @@ class BackfillResponse(BaseModel): dag_display_name: Annotated[str, Field(title="Dag Display Name")] +class BulkCreateActionBulkDAGRunBody(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + action: Annotated[ + Literal["create"], Field(description="The action to be performed on the entities.", title="Action") + ] + entities: Annotated[ + list[BulkDAGRunBody], Field(description="A list of entities to be created.", title="Entities") + ] + action_on_existence: BulkActionOnExistence | None = "fail" + + class BulkCreateActionConnectionBody(BaseModel): model_config = ConfigDict( extra="forbid", @@ -1376,6 +1511,7 @@ class DAGDetailsResponse(BaseModel): timetable_summary: Annotated[str | None, Field(title="Timetable Summary")] = None timetable_description: Annotated[str | None, Field(title="Timetable Description")] = None timetable_partitioned: Annotated[bool, Field(title="Timetable Partitioned")] + timetable_periodic: Annotated[bool, Field(title="Timetable Periodic")] tags: Annotated[list[DagTagResponse], Field(title="Tags")] max_active_tasks: Annotated[int, Field(title="Max Active Tasks")] max_active_runs: Annotated[int | None, Field(title="Max Active Runs")] = None @@ -1405,9 +1541,13 @@ class DAGDetailsResponse(BaseModel): timezone: Annotated[str | None, Field(title="Timezone")] = None last_parsed: Annotated[datetime | None, Field(title="Last Parsed")] = None default_args: Annotated[dict[str, Any] | None, Field(title="Default Args")] = None + rerun_with_latest_version: Annotated[bool | None, Field(title="Rerun With Latest Version")] = None owner_links: Annotated[dict[str, str] | None, Field(title="Owner Links")] = None is_favorite: Annotated[bool | None, Field(title="Is Favorite")] = False active_runs_count: Annotated[int | None, Field(title="Active Runs Count")] = 0 + is_backfillable: Annotated[ + bool, Field(description="Whether this Dag's schedule supports backfilling.", title="Is Backfillable") + ] file_token: Annotated[str, Field(description="Return file token.", title="File Token")] concurrency: Annotated[ int, @@ -1441,6 +1581,7 @@ class DAGResponse(BaseModel): timetable_summary: Annotated[str | None, Field(title="Timetable Summary")] = None timetable_description: Annotated[str | None, Field(title="Timetable Description")] = None timetable_partitioned: Annotated[bool, Field(title="Timetable Partitioned")] + timetable_periodic: Annotated[bool, Field(title="Timetable Periodic")] tags: Annotated[list[DagTagResponse], Field(title="Tags")] max_active_tasks: Annotated[int, Field(title="Max Active Tasks")] max_active_runs: Annotated[int | None, Field(title="Max Active Runs")] = None @@ -1457,6 +1598,9 @@ class DAGResponse(BaseModel): next_dagrun_run_after: Annotated[datetime | None, Field(title="Next Dagrun Run After")] = None allowed_run_types: Annotated[list[DagRunType] | None, Field(title="Allowed Run Types")] = None owners: Annotated[list[str], Field(title="Owners")] + is_backfillable: Annotated[ + bool, Field(description="Whether this Dag's schedule supports backfilling.", title="Is Backfillable") + ] file_token: Annotated[str, Field(description="Return file token.", title="File Token")] @@ -1468,7 +1612,7 @@ class DAGRunPatchBody(BaseModel): model_config = ConfigDict( extra="forbid", ) - state: DAGRunPatchStates | None = None + state: DagRunMutableStates | None = None note: Annotated[Note | None, Field(title="Note")] = None @@ -1631,7 +1775,7 @@ class JobCollectionResponse(BaseModel): class PatchTaskInstanceBody(BaseModel): """ - Request body for Clear Task Instances endpoint. + Request body for patching task instance state. """ model_config = ConfigDict( @@ -1828,6 +1972,15 @@ class TaskResponse(BaseModel): ] +class TaskStateCollectionResponse(BaseModel): + """ + All task state entries for a task instance. + """ + + task_states: Annotated[list[TaskStateResponse], Field(title="Task States")] + total_entries: Annotated[int, Field(title="Total Entries")] + + class VariableCollectionResponse(BaseModel): """ Variable Collection serializer for responses. @@ -1873,6 +2026,18 @@ class BackfillCollectionResponse(BaseModel): total_entries: Annotated[int, Field(title="Total Entries")] +class BulkBodyBulkDAGRunBody(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + actions: Annotated[ + list[ + BulkCreateActionBulkDAGRunBody | BulkUpdateActionBulkDAGRunBody | BulkDeleteActionBulkDAGRunBody + ], + Field(title="Actions"), + ] + + class BulkBodyConnectionBody(BaseModel): model_config = ConfigDict( extra="forbid", @@ -1932,6 +2097,15 @@ class BulkDeleteActionBulkTaskInstanceBody(BaseModel): action_on_non_existence: BulkActionNotOnExistence | None = "fail" +class ClearTaskInstanceCollectionResponse(BaseModel): + """ + Response for clear dag run dry run, which may contain new tasks without full TaskInstance data. + """ + + task_instances: Annotated[list[TaskInstanceResponse | NewTaskResponse], Field(title="Task Instances")] + total_entries: Annotated[int, Field(title="Total Entries")] + + class DAGCollectionResponse(BaseModel): """ Dag Collection serializer for responses. @@ -1943,11 +2117,38 @@ class DAGCollectionResponse(BaseModel): class DAGRunCollectionResponse(BaseModel): """ - Dag Run Collection serializer for responses. + Dag Run collection response supporting both offset and cursor pagination. + + A single flat model is used instead of a discriminated union + (``Annotated[Offset | Cursor, Field(discriminator=...)]``) because + the OpenAPI ``oneOf`` + ``discriminator`` construct is not handled + correctly by ``@hey-api/openapi-ts`` / ``@7nohe/openapi-react-query-codegen``: + return types degrade to ``unknown`` in JSDoc and can produce + incorrect TypeScript types (see hey-api/openapi-ts#1613, #3270). """ dag_runs: Annotated[list[DAGRunResponse], Field(title="Dag Runs")] - total_entries: Annotated[int, Field(title="Total Entries")] + total_entries: Annotated[ + int | None, + Field( + description="Total number of matching items. Populated for offset pagination, ``null`` when using cursor pagination.", + title="Total Entries", + ), + ] = None + next_cursor: Annotated[ + str | None, + Field( + description="Token pointing to the next page. Populated for cursor pagination, ``null`` when using offset pagination or when there is no next page.", + title="Next Cursor", + ), + ] = None + previous_cursor: Annotated[ + str | None, + Field( + description="Token pointing to the previous page. Populated for cursor pagination, ``null`` when using offset pagination or when on the first page.", + title="Previous Cursor", + ), + ] = None class DAGWarningCollectionResponse(BaseModel): @@ -2039,11 +2240,38 @@ class TaskCollectionResponse(BaseModel): class TaskInstanceCollectionResponse(BaseModel): """ - Task Instance Collection serializer for responses. + Task instance collection response supporting both offset and cursor pagination. + + A single flat model is used instead of a discriminated union + (``Annotated[Offset | Cursor, Field(discriminator=...)]``) because + the OpenAPI ``oneOf`` + ``discriminator`` construct is not handled + correctly by ``@hey-api/openapi-ts`` / ``@7nohe/openapi-react-query-codegen``: + return types degrade to ``unknown`` in JSDoc and can produce + incorrect TypeScript types (see hey-api/openapi-ts#1613, #3270). """ task_instances: Annotated[list[TaskInstanceResponse], Field(title="Task Instances")] - total_entries: Annotated[int, Field(title="Total Entries")] + total_entries: Annotated[ + int | None, + Field( + description="Total number of matching items. Populated for offset pagination, ``null`` when using cursor pagination.", + title="Total Entries", + ), + ] = None + next_cursor: Annotated[ + str | None, + Field( + description="Token pointing to the next page. Populated for cursor pagination, ``null`` when using offset pagination or when there is no next page.", + title="Next Cursor", + ), + ] = None + previous_cursor: Annotated[ + str | None, + Field( + description="Token pointing to the previous page. Populated for cursor pagination, ``null`` when using offset pagination or when on the first page.", + title="Previous Cursor", + ), + ] = None class TaskInstanceHistoryCollectionResponse(BaseModel): diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index f52ba055c1c72..e250b66e127dd 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -450,7 +450,7 @@ def create( """Create a connection.""" try: self.response = self.client.post( - "connections", json=connection.model_dump(mode="json", exclude_none=True) + "connections", json=connection.model_dump(mode="json", by_alias=True, exclude_none=True) ) return ConnectionResponse.model_validate_json(self.response.content) except ServerResponseError as e: @@ -459,7 +459,9 @@ def create( def bulk(self, connections: BulkBodyConnectionBody) -> BulkResponse | ServerResponseError: """CRUD multiple connections.""" try: - self.response = self.client.patch("connections", json=connections.model_dump(mode="json")) + self.response = self.client.patch( + "connections", json=connections.model_dump(mode="json", by_alias=True) + ) return BulkResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e @@ -487,7 +489,8 @@ def update( """Update a connection.""" try: self.response = self.client.patch( - f"connections/{connection.connection_id}", json=connection.model_dump(mode="json") + f"connections/{connection.connection_id}", + json=connection.model_dump(mode="json", by_alias=True), ) return ConnectionResponse.model_validate_json(self.response.content) except ServerResponseError as e: @@ -499,7 +502,9 @@ def test( ) -> ConnectionTestResponse | ServerResponseError: """Test a connection.""" try: - self.response = self.client.post("connections/test", json=connection.model_dump(mode="json")) + self.response = self.client.post( + "connections/test", json=connection.model_dump(mode="json", by_alias=True) + ) return ConnectionTestResponse.model_validate_json(self.response.content) except ServerResponseError as e: raise e diff --git a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py index 5dcb63ce23277..02958740a3693 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py @@ -56,6 +56,7 @@ def import_(args, api_client=NEW_API_CLIENT) -> None: port=v.get("port"), extra=v.get("extra"), description=v.get("description", ""), + **({"schema": v["schema"]} if "schema" in v else {}), ) for k, v in connections_json.items() } diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py b/airflow-ctl/tests/airflow_ctl/api/test_operations.py index d01f70eedfea7..52faecee73ea0 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py @@ -682,6 +682,28 @@ def handle_request(request: httpx.Request) -> httpx.Response: response = client.connections.create(connection=self.connection) assert response == self.connection_response + def test_create_uses_schema_alias_in_request_body(self): + connection = ConnectionBody( + connection_id=self.connection_id, + conn_type=self.conn_type, + schema=self.schema_, + ) + + def handle_request(request: httpx.Request) -> httpx.Response: + assert request.url.path == "/api/v2/connections" + request_body = json.loads(request.content.decode()) + assert request_body == { + "connection_id": self.connection_id, + "conn_type": self.conn_type, + "schema": self.schema_, + } + assert "schema_" not in request_body + return httpx.Response(200, json=json.loads(self.connection_response.model_dump_json())) + + client = make_api_client(transport=httpx.MockTransport(handle_request)) + response = client.connections.create(connection=connection) + assert response == self.connection_response + def test_bulk(self): def handle_request(request: httpx.Request) -> httpx.Response: assert request.url.path == "/api/v2/connections" @@ -691,6 +713,34 @@ def handle_request(request: httpx.Request) -> httpx.Response: response = client.connections.bulk(connections=self.connection_bulk_body) assert response == self.connection_bulk_response + def test_bulk_uses_schema_alias_in_request_body(self): + connection = ConnectionBody( + connection_id=self.connection_id, + conn_type=self.conn_type, + schema=self.schema_, + ) + connection_bulk_body = BulkBodyConnectionBody( + actions=[ + BulkCreateActionConnectionBody( + action="create", + entities=[connection], + action_on_existence=BulkActionOnExistence.FAIL, + ) + ] + ) + + def handle_request(request: httpx.Request) -> httpx.Response: + assert request.url.path == "/api/v2/connections" + request_body = json.loads(request.content.decode()) + entity = request_body["actions"][0]["entities"][0] + assert entity["schema"] == self.schema_ + assert "schema_" not in entity + return httpx.Response(200, json=json.loads(self.connection_bulk_response.model_dump_json())) + + client = make_api_client(transport=httpx.MockTransport(handle_request)) + response = client.connections.bulk(connections=connection_bulk_body) + assert response == self.connection_bulk_response + def test_delete(self): def handle_request(request: httpx.Request) -> httpx.Response: assert request.url.path == f"/api/v2/connections/{self.connection_id}" @@ -709,6 +759,35 @@ def handle_request(request: httpx.Request) -> httpx.Response: response = client.connections.update(connection=self.connection) assert response == self.connection_response + def test_update_uses_schema_alias_in_request_body(self): + connection = ConnectionBody( + connection_id=self.connection_id, + conn_type=self.conn_type, + schema=self.schema_, + ) + + def handle_request(request: httpx.Request) -> httpx.Response: + assert request.url.path == f"/api/v2/connections/{self.connection_id}" + request_body = json.loads(request.content.decode()) + assert request_body == { + "connection_id": self.connection_id, + "conn_type": self.conn_type, + "description": None, + "host": None, + "login": None, + "schema": self.schema_, + "port": None, + "password": None, + "extra": None, + "team_name": None, + } + assert "schema_" not in request_body + return httpx.Response(200, json=json.loads(self.connection_response.model_dump_json())) + + client = make_api_client(transport=httpx.MockTransport(handle_request)) + response = client.connections.update(connection=connection) + assert response == self.connection_response + def test_test(self): connection_test_response = ConnectionTestResponse( status=True, @@ -723,6 +802,39 @@ def handle_request(request: httpx.Request) -> httpx.Response: response = client.connections.test(connection=self.connection) assert response == connection_test_response + def test_test_uses_schema_alias_in_request_body(self): + connection = ConnectionBody( + connection_id=self.connection_id, + conn_type=self.conn_type, + schema=self.schema_, + ) + connection_test_response = ConnectionTestResponse( + status=True, + message="message", + ) + + def handle_request(request: httpx.Request) -> httpx.Response: + assert request.url.path == "/api/v2/connections/test" + request_body = json.loads(request.content.decode()) + assert request_body == { + "connection_id": self.connection_id, + "conn_type": self.conn_type, + "description": None, + "host": None, + "login": None, + "schema": self.schema_, + "port": None, + "password": None, + "extra": None, + "team_name": None, + } + assert "schema_" not in request_body + return httpx.Response(200, json=json.loads(connection_test_response.model_dump_json())) + + client = make_api_client(transport=httpx.MockTransport(handle_request)) + response = client.connections.test(connection=connection) + assert response == connection_test_response + class TestDagOperations: dag_id = "dag_id" @@ -739,6 +851,7 @@ class TestDagOperations: timetable_summary="timetable_summary", timetable_description="timetable_description", timetable_partitioned=False, + timetable_periodic=True, tags=[], max_active_tasks=1, max_active_runs=1, @@ -750,6 +863,7 @@ class TestDagOperations: next_dagrun_data_interval_end=datetime.datetime(2025, 1, 1, 0, 0, 0), next_dagrun_run_after=datetime.datetime(2025, 1, 1, 0, 0, 0), owners=["apache-airflow"], + is_backfillable=True, file_token="file_token", bundle_name="bundle_name", is_stale=False, @@ -767,6 +881,7 @@ class TestDagOperations: timetable_summary="timetable_summary", timetable_description="timetable_description", timetable_partitioned=False, + timetable_periodic=True, tags=[], max_active_tasks=1, max_active_runs=1, @@ -778,6 +893,7 @@ class TestDagOperations: next_dagrun_data_interval_end=datetime.datetime(2025, 1, 1, 0, 0, 0), next_dagrun_run_after=datetime.datetime(2025, 1, 1, 0, 0, 0), owners=["apache-airflow"], + is_backfillable=True, catchup=False, dag_run_timeout=datetime.timedelta(days=1), asset_expression=None, diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py index bdfb759d0a91d..ba803ddd8e0cd 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py @@ -20,6 +20,7 @@ from unittest import mock from unittest.mock import patch +import httpx import pytest from airflowctl.api.client import Client, ClientKind @@ -180,6 +181,40 @@ def test_import_without_extra_field(self, api_client_maker, tmp_path, monkeypatc description="", ) + def test_import_preserves_schema(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + json_path = tmp_path / self.export_file_name + connection_file = { + self.connection_id: { + "conn_type": "postgres", + "host": "test_host", + "schema": "warehouse", + } + } + + json_path.write_text(json.dumps(connection_file)) + + def handle_request(request: httpx.Request) -> httpx.Response: + assert request.method == "PATCH" + assert request.url.path == "/api/v2/connections" + request_body = json.loads(request.content.decode()) + entity = request_body["actions"][0]["entities"][0] + assert entity["schema"] == "warehouse" + assert "schema_" not in entity + return httpx.Response(200, json=self.bulk_response_success.model_dump()) + + api_client = Client( + base_url="test://server", + transport=httpx.MockTransport(handle_request), + token="", + kind=ClientKind.CLI, + ) + + connection_command.import_( + self.parser.parse_args(["connections", "import", json_path.as_posix()]), + api_client=api_client, + ) + @pytest.mark.parametrize( ("action_on_existing_key", "expected_enum"), [ diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py index d00eaa5791589..405eb030065f8 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py @@ -42,6 +42,7 @@ class TestDagCommands: timetable_summary="timetable_summary", timetable_description="timetable_description", timetable_partitioned=False, + timetable_periodic=True, tags=[], max_active_tasks=1, max_active_runs=1, @@ -53,6 +54,7 @@ class TestDagCommands: next_dagrun_data_interval_end=datetime.datetime(2025, 1, 1, 0, 0, 0), next_dagrun_run_after=datetime.datetime(2025, 1, 1, 0, 0, 0), owners=["apache-airflow"], + is_backfillable=True, file_token="file_token", bundle_name="bundle_name", is_stale=False, @@ -70,6 +72,7 @@ class TestDagCommands: timetable_summary="timetable_summary", timetable_description="timetable_description", timetable_partitioned=False, + timetable_periodic=True, tags=[], max_active_tasks=1, max_active_runs=1, @@ -81,6 +84,7 @@ class TestDagCommands: next_dagrun_data_interval_end=datetime.datetime(2025, 1, 1, 0, 0, 0), next_dagrun_run_after=datetime.datetime(2025, 1, 1, 0, 0, 0), owners=["apache-airflow"], + is_backfillable=True, file_token="file_token", bundle_name="bundle_name", is_stale=False, From f3bac6c9f960e4e732cc3e035215dc7ca42010c2 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Tue, 26 May 2026 23:24:43 +0200 Subject: [PATCH 10/10] [airflow-ctl/v0-1-test] Add airflowctl 0.1.5 release notes (#67562) (#67576) * Add airflowctl 0.1.5 release notes * Add double backticks * Apply suggestions from code review * Amend release notes without backports and duplicates, add misc as commented visibility --------- (cherry picked from commit 1a1c145bc0a8c310d3027194395eda722d588d65) Co-authored-by: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> --- airflow-ctl/RELEASE_NOTES.rst | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/airflow-ctl/RELEASE_NOTES.rst b/airflow-ctl/RELEASE_NOTES.rst index c34065580548f..3dc63b5e95d90 100644 --- a/airflow-ctl/RELEASE_NOTES.rst +++ b/airflow-ctl/RELEASE_NOTES.rst @@ -15,6 +15,66 @@ specific language governing permissions and limitations under the License. +.. This file is populated while releasing after cutting the release candidate. Please do not edit in PRs. + +airflowctl 0.1.5 (2026-05-26) +----------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +- Add dags next execution command #66172 (#66188) +- Add bulk delete Dag Runs (#67095) +- Add ``rerun_with_latest_version`` config hierarchy for clear/rerun behavior (#63884) +- Implement patching of task group instances in API (#62812) +- Allow remote version check without authentication (#65099) +- Add cursor based pagination for get_dag_runs endpoint (#65604) +- Enable queue up new tasks (#63484) +- Add cursor based pagination for get_task_instances endpoint (#64845) +- Add ``is_backfillable`` property to DAG API responses (#64644) +- 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 ``_) (#66768) + +Bug Fixes +^^^^^^^^^ + +- Fix connections import schema handling (#67063) +- Fix broken download URLs and variable names in docs (#67046) +- Fix missing pyyaml runtime dependency (#65489) +- Fix dagrun list crash when --state is omitted (#65608) +- Fix backfill params not overriding existing DAG run conf (#64939) +- Fix ruff on client-py (#64868) + +Improvements +^^^^^^^^^^^^ + +- AIP-103: Add Core API endpoints for task state and asset state (#67041) +- Comment to not edit RELEASE_NOTES.rst manually in PRs for airflowctl (#67128) +- Align Dag capitalization from "DAG" to "Dag" for airflow-ctl/ (#66112) +- Send backfill create and dry-run payloads as JSON (#65158) +- Use existing safe_load function in airflowctl utils to load help texts (#65841) +- Cap airflow-ctl httpx dependency below 1.0 (#65607) +- Remove dead airflow-ctl/newsfragments directory (unused by changelog tooling) (#65507) +- Incorrect fallback logic (#64586) +- Run non-provider mypy as regular prek static checks instead of separate CI jobs (#64780) +- Clear, Mark Success/Fail and delete multiple Task Instances (#64141) + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + - Upgrade important CI environment (#67313) + - Upgrade important CI environment (#66843) + - Upgrade important CI environment (#66068) + - Bump uv to 0.11.8 to adopt uv.lock-conflict fix (#66042) + - Upgrade important CI environment (#65933) + - Upgrade important CI environment (#65521) + - Upgrade important CI environment (#65525) + - CI: Upgrade important CI environment (#64458) + - CI: Upgrade important CI environment (#64451) + - Add 4-day cooldown for uv dependency resolution (#64249) + - Upgrade important CI environment (#64239) + airflowctl 0.1.4 (2026-04-21) -----------------------------