From 567ad643c9525e0cd08d1e9a912436e50b568027 Mon Sep 17 00:00:00 2001 From: Jake Roach <116606359+jroachgolf84@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:37:17 -0400 Subject: [PATCH 001/595] issue-57891: Adding sftp_remote_host to S3 transfer Operators (#63147) --- .../amazon/aws/transfers/s3_to_sftp.py | 8 ++- .../amazon/aws/transfers/sftp_to_s3.py | 8 ++- .../amazon/aws/transfers/test_s3_to_sftp.py | 55 +++++++++++++++++++ .../amazon/aws/transfers/test_sftp_to_s3.py | 51 +++++++++++++++++ 4 files changed, 120 insertions(+), 2 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py b/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py index 0b3b5e1cb8417..87d8454af963b 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_sftp.py @@ -42,6 +42,8 @@ class S3ToSFTPOperator(BaseOperator): establishing a connection to the SFTP server. :param sftp_path: The sftp remote path. This is the specified file path for uploading file to the SFTP server. + :param sftp_remote_host: The remote host of the SFTP server. Overrides host in + Connection. :param aws_conn_id: The Airflow connection used for AWS credentials. If this is None or empty then the default boto3 behaviour is used. If running Airflow in a distributed manner and aws_conn_id is None or @@ -65,6 +67,7 @@ def __init__( s3_key: str, sftp_path: str, sftp_conn_id: str = "ssh_default", + sftp_remote_host: str = "", aws_conn_id: str | None = "aws_default", confirm: bool = True, **kwargs, @@ -74,6 +77,7 @@ def __init__( self.sftp_path = sftp_path self.s3_bucket = s3_bucket self.s3_key = s3_key + self.sftp_remote_host = sftp_remote_host self.aws_conn_id = aws_conn_id self.confirm = confirm @@ -85,7 +89,9 @@ def get_s3_key(s3_key: str) -> str: def execute(self, context: Context) -> None: self.s3_key = self.get_s3_key(self.s3_key) - ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id) + + # SSHHook will handle a None/"" sftp_remote_host + ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id, remote_host=self.sftp_remote_host) s3_hook = S3Hook(self.aws_conn_id) s3_client = s3_hook.get_conn() diff --git a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py index 0b3aada19501d..4897ccca25c91 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sftp_to_s3.py @@ -40,6 +40,8 @@ class SFTPToS3Operator(BaseOperator): :param sftp_conn_id: The sftp connection id. The name or identifier for establishing a connection to the SFTP server. + :param sftp_remote_host: The remote host of the SFTP server. Overrides host in + Connection. :param sftp_path: The sftp remote path. This is the specified file path for downloading the file from the SFTP server. :param s3_conn_id: The s3 connection id. The name or identifier for @@ -63,6 +65,7 @@ def __init__( s3_key: str, sftp_path: str, sftp_conn_id: str = "ssh_default", + sftp_remote_host: str = "", s3_conn_id: str = "aws_default", use_temp_file: bool = True, fail_on_file_not_exist: bool = True, @@ -71,6 +74,7 @@ def __init__( super().__init__(**kwargs) self.sftp_conn_id = sftp_conn_id self.sftp_path = sftp_path + self.sftp_remote_host = sftp_remote_host self.s3_bucket = s3_bucket self.s3_key = s3_key self.s3_conn_id = s3_conn_id @@ -85,7 +89,9 @@ def get_s3_key(s3_key: str) -> str: def execute(self, context: Context) -> None: self.s3_key = self.get_s3_key(self.s3_key) - ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id) + + # SSHHook will handle a None/"" sftp_remote_host + ssh_hook = SSHHook(ssh_conn_id=self.sftp_conn_id, remote_host=self.sftp_remote_host) s3_hook = S3Hook(self.s3_conn_id) sftp_client = ssh_hook.get_conn().open_sftp() diff --git a/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py b/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py index fecf207c6f429..257b898922ccd 100644 --- a/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py +++ b/providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py @@ -256,5 +256,60 @@ def test_s3_to_sftp_operation_confirm_false(self): conn.delete_bucket(Bucket=self.s3_bucket) assert not s3_hook.check_for_bucket(self.s3_bucket) + @mock_aws + @conf_vars({("core", "enable_xcom_pickling"): "True"}) + def test_s3_to_sftp_operator_sftp_remote_host(self): + """Test that sftp_remote_host overrides the connection host when provided.""" + s3_hook = S3Hook(aws_conn_id=None) + test_remote_file_content = ( + "This is remote file content for sftp_remote_host test \n which is also multiline " + "another line here \n this is last line. EOF" + ) + + # Test for creation of s3 bucket + conn = boto3.client("s3") + conn.create_bucket(Bucket=self.s3_bucket) + assert s3_hook.check_for_bucket(self.s3_bucket) + + with open(LOCAL_FILE_PATH, "w") as file: + file.write(test_remote_file_content) + s3_hook.load_file(LOCAL_FILE_PATH, self.s3_key, bucket_name=BUCKET) + + # Check if object was created in s3 + objects_in_dest_bucket = conn.list_objects(Bucket=self.s3_bucket, Prefix=self.s3_key) + assert len(objects_in_dest_bucket["Contents"]) == 1 + assert objects_in_dest_bucket["Contents"][0]["Key"] == self.s3_key + + # Execute with sftp_remote_host overriding the connection host to the same localhost + run_task = S3ToSFTPOperator( + s3_bucket=BUCKET, + s3_key=S3_KEY, + sftp_path=SFTP_PATH, + sftp_conn_id=SFTP_CONN_ID, + sftp_remote_host="localhost", + task_id=TASK_ID + "_remote_host", + dag=self.dag, + ) + assert run_task is not None + + run_task.execute(None) + + # Check that the file is created remotely with correct content + check_file_task = SSHOperator( + task_id="test_check_file_remote_host", + ssh_hook=self.hook, + command=f"cat {self.sftp_path}", + do_xcom_push=True, + dag=self.dag, + ) + assert check_file_task is not None + result = check_file_task.execute(None) + assert result.strip() == test_remote_file_content.encode("utf-8") + + # Clean up after finishing with test + conn.delete_object(Bucket=self.s3_bucket, Key=self.s3_key) + conn.delete_bucket(Bucket=self.s3_bucket) + assert not s3_hook.check_for_bucket(self.s3_bucket) + def teardown_method(self): self.delete_remote_resource() diff --git a/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py b/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py index e8fd3be4905da..feb85e33a3c17 100644 --- a/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py +++ b/providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py @@ -157,3 +157,54 @@ def test_sftp_to_s3_fail_on_file_not_exist(self, fail_on_file_not_exist): conn.delete_object(Bucket=self.s3_bucket, Key=self.s3_key) conn.delete_bucket(Bucket=self.s3_bucket) assert not s3_hook.check_for_bucket(self.s3_bucket) + + @mock_aws + @conf_vars({("core", "enable_xcom_pickling"): "True"}) + def test_sftp_to_s3_sftp_remote_host(self): + """Test that sftp_remote_host overrides the connection host when provided.""" + test_remote_file_content = ( + "This is remote file content for sftp_remote_host test \n which is also multiline " + "another line here \n this is last line. EOF" + ) + + # Create a test file remotely + create_file_task = SSHOperator( + task_id="test_create_file_remote_host", + ssh_hook=self.hook, + command=f"echo '{test_remote_file_content}' > {self.sftp_path}", + do_xcom_push=True, + dag=self.dag, + ) + assert create_file_task is not None + create_file_task.execute(None) + + # Test for creation of s3 bucket + s3_hook = S3Hook(aws_conn_id=None) + conn = boto3.client("s3") + conn.create_bucket(Bucket=self.s3_bucket) + assert s3_hook.check_for_bucket(self.s3_bucket) + + # Execute with sftp_remote_host overriding the connection host to the same localhost + run_task = SFTPToS3Operator( + s3_bucket=BUCKET, + s3_key=S3_KEY, + sftp_path=SFTP_PATH, + sftp_conn_id=SFTP_CONN_ID, + sftp_remote_host="localhost", + s3_conn_id=S3_CONN_ID, + task_id="test_sftp_to_s3_remote_host", + dag=self.dag, + ) + assert run_task is not None + + run_task.execute(None) + + # Check if object was created in s3 + objects_in_dest_bucket = conn.list_objects(Bucket=self.s3_bucket, Prefix=self.s3_key) + assert len(objects_in_dest_bucket["Contents"]) == 1 + assert objects_in_dest_bucket["Contents"][0]["Key"] == self.s3_key + + # Clean up after finishing with test + conn.delete_object(Bucket=self.s3_bucket, Key=self.s3_key) + conn.delete_bucket(Bucket=self.s3_bucket) + assert not s3_hook.check_for_bucket(self.s3_bucket) From b528e502eff321ec07464fc9c0e05ede30c8d5aa Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 9 Mar 2026 17:44:25 +0100 Subject: [PATCH 002/595] Further limit setuptools after 82.0.1 is released (until redoc fixes it) (#63202) --- devel-common/pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/devel-common/pyproject.toml b/devel-common/pyproject.toml index 350546df09faf..a5020cebff4b8 100644 --- a/devel-common/pyproject.toml +++ b/devel-common/pyproject.toml @@ -103,7 +103,9 @@ dependencies = [ "sphinxcontrib-redoc>=1.6.0", "sphinxcontrib-serializinghtml>=1.1.5", "sphinxcontrib-spelling>=8.0.0", - "setuptools!=82.0.0", # until https://github.com/sphinx-contrib/redoc/issues/53 is resolved + # setuptools 82.0.0+ causes redoc to fail due to pkg_resources removal + # until https://github.com/sphinx-contrib/redoc/issues/53 is resolved + "setuptools<82.0.0", ] "docs-gen" = [ "diagrams>=0.24.4", From 30524f85c44a839aa9cabcec05cd61ead8045692 Mon Sep 17 00:00:00 2001 From: Elad Kalif <45845474+eladkal@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:45:38 +0200 Subject: [PATCH 003/595] make hive cli `zooKeeperNamespace` and `ssl` parameters configurable (#63193) * make hive cli zooKeeperNamespace and Ssl parameters configurable * fix static checks * Update providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py Co-authored-by: GPK * fix tests --------- Co-authored-by: romsharon98 Co-authored-by: Jarek Potiuk Co-authored-by: GPK --- providers/apache/hive/docs/connections/hive_cli.rst | 6 ++++++ .../src/airflow/providers/apache/hive/hooks/hive.py | 10 +++++++++- .../hive/tests/unit/apache/hive/hooks/test_hive.py | 12 ++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/providers/apache/hive/docs/connections/hive_cli.rst b/providers/apache/hive/docs/connections/hive_cli.rst index 5e88df971d989..00807a07502e6 100644 --- a/providers/apache/hive/docs/connections/hive_cli.rst +++ b/providers/apache/hive/docs/connections/hive_cli.rst @@ -77,6 +77,12 @@ High Availability (optional) Specify as ``True`` if you want to connect to a Hive installation running in high availability mode. Specify host accordingly. +Ssl (optional) + Specify as ``True`` to enable SSL for your high availability connection. + +Zoo Keeper Namespace (optional) + Zoo keeper namespace for high availability. + When specifying the connection in environment variable you should specify it using URI syntax. diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py index 5745c5e4a615f..efa0397c0d7ea 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py @@ -140,6 +140,10 @@ def get_connection_form_widgets(cls) -> dict[str, Any]: lazy_gettext("Principal"), widget=BS3TextFieldWidget(), default="hive/_HOST@EXAMPLE.COM" ), "high_availability": BooleanField(lazy_gettext("High Availability mode"), default=False), + "ssl": BooleanField(lazy_gettext("Ssl"), default=True), + "zoo_keeper_namespace": StringField( + lazy_gettext("Zoo Keeper Namespace"), widget=BS3TextFieldWidget(), default="hiveserver2" + ), } @classmethod @@ -188,7 +192,11 @@ def _prepare_cli_cmd(self) -> list[Any]: if self.high_availability: if not jdbc_url.endswith(";"): jdbc_url += ";" - jdbc_url += "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2" + ssl = conn.extra_dejson.get("ssl", True) + zoo_keeper_namespace = conn.extra_dejson.get("zoo_keeper_namespace", "hiveserver2") + jdbc_url += ( + f"serviceDiscoveryMode=zooKeeper;ssl={ssl};zooKeeperNamespace={zoo_keeper_namespace}" + ) elif self.auth: jdbc_url += ";auth=" + self.auth diff --git a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py index beb82a926b072..94a573a2259c8 100644 --- a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py +++ b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py @@ -1023,18 +1023,22 @@ def test_get_wrong_principal(self): [ ( {"high_availability": "true"}, - "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2", + "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2", ), ( {"high_availability": "false"}, - "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2", + "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2", ), - ({}, "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2"), + ( + {"high_availability": "true", "ssl": "false", "zoo_keeper_namespace": "custom_hive_server"}, + "serviceDiscoveryMode=zooKeeper;ssl=false;zooKeeperNamespace=custom_hive_server", + ), + ({}, "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2"), # with proxy user ( {"proxy_user": "a_user_proxy", "high_availability": "true"}, "hive.server2.proxy.user=a_user_proxy;" - "serviceDiscoveryMode=zooKeeper;ssl=true;zooKeeperNamespace=hiveserver2", + "serviceDiscoveryMode=zooKeeper;ssl=True;zooKeeperNamespace=hiveserver2", ), ], ) From e491aac92a1d50f322a08d92d83616e7c79b3f2e Mon Sep 17 00:00:00 2001 From: jerry Date: Tue, 10 Mar 2026 00:46:10 +0800 Subject: [PATCH 004/595] fix(providers/amazon): S3DagBundle does not delete stale dag recursively (#63104) * Refactor S3Hook's local file synchronization logic to mach GCSHook. Update tests to cover nested directories and ensure proper logging of deleted files and directories. * Update S3Hook logging level for deleted files and directories from info to debug to reduce log verbosity. --- .../airflow/providers/amazon/aws/hooks/s3.py | 34 +++++++------------ .../tests/unit/amazon/aws/hooks/test_s3.py | 14 ++++++++ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py b/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py index f9beb0d040003..d0f8ce8b78460 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py @@ -1735,27 +1735,19 @@ def delete_bucket_tagging(self, bucket_name: str | None = None) -> None: s3_client.delete_bucket_tagging(Bucket=bucket_name) def _sync_to_local_dir_delete_stale_local_files(self, current_s3_objects: list[Path], local_dir: Path): - current_s3_keys = {key for key in current_s3_objects} - - for item in local_dir.iterdir(): - item: Path # type: ignore[no-redef] - absolute_item_path = item.resolve() - - if absolute_item_path not in current_s3_keys: - try: - if item.is_file(): - item.unlink(missing_ok=True) - self.log.debug("Deleted stale local file: %s", item) - elif item.is_dir(): - # delete only when the folder is empty - if not os.listdir(item): - item.rmdir() - self.log.debug("Deleted stale empty directory: %s", item) - else: - self.log.debug("Skipping stale item of unknown type: %s", item) - except OSError as e: - self.log.error("Error deleting stale item %s: %s", item, e) - raise e + current_s3_keys = {key.resolve() for key in current_s3_objects} + + for item in local_dir.rglob("*"): + if item.is_file() and item.resolve() not in current_s3_keys: + self.log.debug("Deleted stale local file: %s", item) + item.unlink() + # Clean up empty directories + for root, dirs, _ in os.walk(local_dir, topdown=False): + for d in dirs: + dir_path = os.path.join(root, d) + if not os.listdir(dir_path): + self.log.debug("Deleted stale empty directory: %s", dir_path) + os.rmdir(dir_path) def _sync_to_local_dir_if_changed(self, s3_bucket, s3_object, local_target_path: Path): should_download = False diff --git a/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py b/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py index 14371da9cb4b3..99fd45aff8903 100644 --- a/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py +++ b/providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py @@ -1870,14 +1870,28 @@ def get_logs_string(call_args_list): local_file_that_should_be_deleted.write_text("test dag") local_folder_should_be_deleted = Path(sync_local_dir).joinpath("local_folder_should_be_deleted") local_folder_should_be_deleted.mkdir(exist_ok=True) + nested_stale_file = Path(sync_local_dir).joinpath("subproject1", "stale_nested.py") + nested_stale_file.write_text("stale nested file") + deep_nested_dir = Path(sync_local_dir).joinpath("subproject1", "deep") + deep_nested_dir.mkdir() + deep_stale_file = deep_nested_dir.joinpath("stale_deep.py") + deep_stale_file.write_text("stale deep file") hook.log.debug = MagicMock() hook.sync_to_local_dir( bucket_name=s3_bucket, local_dir=sync_local_dir, s3_prefix="", delete_stale=True ) logs_string = get_logs_string(hook.log.debug.call_args_list) assert f"Deleted stale local file: {local_file_that_should_be_deleted.as_posix()}" in logs_string + assert f"Deleted stale local file: {nested_stale_file.as_posix()}" in logs_string + assert f"Deleted stale local file: {deep_stale_file.as_posix()}" in logs_string assert f"Deleted stale empty directory: {local_folder_should_be_deleted.as_posix()}" in logs_string + assert f"Deleted stale empty directory: {deep_nested_dir.as_posix()}" in logs_string + assert not nested_stale_file.exists() + assert not deep_stale_file.exists() + assert not deep_nested_dir.exists() + assert Path(sync_local_dir).joinpath("dag_01.py").exists() + assert Path(sync_local_dir).joinpath("subproject1", "dag_a.py").exists() s3_client.put_object(Bucket=s3_bucket, Key="dag_03.py", Body=b"test data-changed") hook.log.debug = MagicMock() From fe5730407ee4c7e172399fcca25b2c984ebcb5b5 Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Mon, 9 Mar 2026 18:12:12 +0100 Subject: [PATCH 005/595] Add retry mechanism to airflowctl and remove flaky integration mark (#63016) * tenacity added to include retry over httpx --- .../test_airflowctl_commands.py | 2 - .../docs/cli-and-env-variables-ref.rst | 18 ++++++ airflow-ctl/pyproject.toml | 1 + airflow-ctl/src/airflowctl/api/client.py | 39 ++++++++++++ .../tests/airflow_ctl/api/test_client.py | 62 +++++++++++++++++++ 5 files changed, 120 insertions(+), 2 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 3f89a7c969818..d9d002e9eda76 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py @@ -131,7 +131,6 @@ def date_param(): ] -@pytest.mark.flaky(reruns=3, reruns_delay=1) @pytest.mark.parametrize( "command", TEST_COMMANDS_DEBUG_MODE, @@ -144,7 +143,6 @@ def test_airflowctl_commands(command: str, run_command): run_command(command, env_vars, skip_login=True) -@pytest.mark.flaky(reruns=3, reruns_delay=1) @pytest.mark.parametrize( "command", TEST_COMMANDS_SKIP_KEYRING, diff --git a/airflow-ctl/docs/cli-and-env-variables-ref.rst b/airflow-ctl/docs/cli-and-env-variables-ref.rst index 2d63d47d5be53..66e3638a4a7b6 100644 --- a/airflow-ctl/docs/cli-and-env-variables-ref.rst +++ b/airflow-ctl/docs/cli-and-env-variables-ref.rst @@ -60,3 +60,21 @@ Environment Variables It disables some features such as keyring integration and save credentials to file. It is only meant to use if either you are developing airflowctl or running API integration tests. Please do not use this variable unless you know what you are doing. + +.. envvar:: AIRFLOW_CLI_API_RETRIES + + The number of times to retry an API call if it fails. This is + only used if you are using the Airflow API and have not set up + authentication using a different method. The default value is 3. + +.. envvar:: AIRFLOW_CLI_API_RETRY_WAIT_MIN + + The minimum amount of time to wait between API retries in seconds. + This is only used if you are using the Airflow API and have not set up + authentication using a different method. The default value is 1 second. + +.. envvar:: AIRFLOW_CLI_API_RETRY_WAIT_MAX + + The maximum amount of time to wait between API retries in seconds. + This is only used if you are using the Airflow API and have not set up + authentication using a different method. The default value is 10 seconds. diff --git a/airflow-ctl/pyproject.toml b/airflow-ctl/pyproject.toml index 45e42a0c5b337..fec7206a23092 100644 --- a/airflow-ctl/pyproject.toml +++ b/airflow-ctl/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "structlog>=25.4.0", "uuid6>=2024.7.10", "tabulate>=0.9.0", + "tenacity>=9.1.4", ] classifiers = [ diff --git a/airflow-ctl/src/airflowctl/api/client.py b/airflow-ctl/src/airflowctl/api/client.py index 7c03dae30554a..0ef5d7cb16441 100644 --- a/airflow-ctl/src/airflowctl/api/client.py +++ b/airflow-ctl/src/airflowctl/api/client.py @@ -21,6 +21,7 @@ import enum import getpass import json +import logging import os import sys from collections.abc import Callable @@ -32,6 +33,13 @@ import structlog from httpx import URL from keyring.errors import NoKeyringError +from tenacity import ( + before_log, + retry, + retry_if_exception, + stop_after_attempt, + wait_random_exponential, +) from uuid6 import uuid7 from airflowctl import __version__ as version @@ -261,6 +269,20 @@ def auth_flow(self, request: httpx.Request): yield request +def _should_retry_api_request(exception: BaseException) -> bool: + """Determine if an API request should be retried based on the exception type.""" + if isinstance(exception, httpx.HTTPStatusError): + return exception.response.status_code >= 500 + + return isinstance(exception, httpx.RequestError) + + +# API Client Retry Configuration +API_RETRIES = int(os.getenv("AIRFLOW_CLI_API_RETRIES", "3")) +API_RETRY_WAIT_MIN = int(os.getenv("AIRFLOW_CLI_API_RETRY_WAIT_MIN", "1")) +API_RETRY_WAIT_MAX = int(os.getenv("AIRFLOW_CLI_API_RETRY_WAIT_MAX", "10")) + + class Client(httpx.Client): """Client for the Airflow REST API.""" @@ -298,6 +320,23 @@ def _get_base_url( return f"{base_url}/auth" return f"{base_url}/api/v2" + @retry( + retry=retry_if_exception(_should_retry_api_request), + stop=stop_after_attempt(API_RETRIES), + wait=wait_random_exponential(min=API_RETRY_WAIT_MIN, max=API_RETRY_WAIT_MAX), + before_sleep=before_log(log, logging.WARNING), + reraise=True, + ) + def request(self, *args, **kwargs): + """Implement a convenience for httpx.Client.request with a retry layer.""" + # Set content type as convenience if not already set + if kwargs.get("content", None) is not None and "content-type" not in ( + kwargs.get("headers", {}) or {} + ): + kwargs["headers"] = {"content-type": "application/json"} + + return super().request(*args, **kwargs) + @lru_cache() # type: ignore[prop-decorator] @property def login(self): diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py b/airflow-ctl/tests/airflow_ctl/api/test_client.py index f79322d16fb74..0617d62276a1c 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_client.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py @@ -25,6 +25,7 @@ import httpx import pytest +import time_machine from httpx import URL from airflowctl.api.client import Client, ClientKind, Credentials, _bounded_get_new_password @@ -32,6 +33,15 @@ from airflowctl.exceptions import AirflowCtlCredentialNotFoundException, AirflowCtlKeyringException +def make_client_w_responses(responses: list[httpx.Response]) -> Client: + """Get a client with custom responses.""" + + def handle_request(request: httpx.Request) -> httpx.Response: + return responses.pop(0) + + return Client(base_url="", token="", mounts={"'http://": httpx.MockTransport(handle_request)}) + + @pytest.fixture(autouse=True) def unique_config_dir(): temp_dir = tempfile.mkdtemp() @@ -314,3 +324,55 @@ def test_save_skips_patch_for_non_encrypted_backend(self, mock_keyring): assert not hasattr(mock_backend, "_get_new_password") mock_keyring.set_password.assert_called_once_with("airflowctl", "api_token_production", "token") + + def test_retry_handling_unrecoverable_error(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + *[httpx.Response(500, text="Internal Server Error")] * 6, + httpx.Response(200, json={"detail": "Recovered from error - but will fail before"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + with pytest.raises(httpx.HTTPStatusError) as err: + client.get("http://error") + assert not isinstance(err.value, ServerResponseError) + assert len(responses) == 5 + + def test_retry_handling_recovered(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + *[httpx.Response(500, text="Internal Server Error")] * 2, + httpx.Response(200, json={"detail": "Recovered from error"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + response = client.get("http://error") + assert response.status_code == 200 + assert len(responses) == 1 + + def test_retry_handling_non_retry_error(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + httpx.Response(422, json={"detail": "Somehow this is a bad request"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + with pytest.raises(ServerResponseError) as err: + client.get("http://error") + assert len(responses) == 1 + assert err.value.args == ("Client error message: {'detail': 'Somehow this is a bad request'}",) + + def test_retry_handling_ok(self): + with time_machine.travel("2023-01-01T00:00:00Z", tick=False): + responses: list[httpx.Response] = [ + httpx.Response(200, json={"detail": "Recovered from error"}), + httpx.Response(400, json={"detail": "Should not get here"}), + ] + client = make_client_w_responses(responses) + + response = client.get("http://error") + assert response.status_code == 200 + assert len(responses) == 1 From 5319a5adcbeac27e14f424680466f2fd8f6d6501 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:00:56 -0400 Subject: [PATCH 006/595] chore(deps): bump @hey-api/openapi-ts (#63211) Bumps the core-ui-package-updates group with 1 update in the /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui directory: [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts). Updates `@hey-api/openapi-ts` from 0.93.1 to 0.94.0 - [Release notes](https://github.com/hey-api/openapi-ts/releases) - [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.93.1...@hey-api/openapi-ts@0.94.0) --- updated-dependencies: - dependency-name: "@hey-api/openapi-ts" dependency-version: 0.94.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../auth/managers/simple/ui/package.json | 2 +- .../auth/managers/simple/ui/pnpm-lock.yaml | 34 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json index c6b9fc2381ba7..58189eccfb0cc 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json @@ -20,7 +20,7 @@ "dependencies": { "@chakra-ui/react": "^3.34.0", "@hey-api/client-axios": "^0.9.1", - "@hey-api/openapi-ts": "^0.93.1", + "@hey-api/openapi-ts": "^0.94.0", "@tanstack/react-query": "^5.90.21", "axios": "^1.13.6", "next-themes": "^0.4.6", diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml index d1ded3d02e464..a7b8967fc2afb 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml @@ -20,10 +20,10 @@ importers: version: 3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@hey-api/client-axios': specifier: ^0.9.1 - version: 0.9.1(@hey-api/openapi-ts@0.93.1(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6) + version: 0.9.1(@hey-api/openapi-ts@0.94.0(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6) '@hey-api/openapi-ts': - specifier: ^0.93.1 - version: 0.93.1(magicast@0.3.5)(typescript@5.9.3) + specifier: ^0.94.0 + version: 0.94.0(magicast@0.3.5)(typescript@5.9.3) '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21(react@19.2.4) @@ -555,8 +555,8 @@ packages: resolution: {integrity: sha512-T8T3yCl2+AiVVDP6tvfnU/rXOkEHddMTOYCZXUVbydj7URVErh5BelIa8UWBkFYZBP2/mi2nViScNhe9eBolPw==} deprecated: Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts. - '@hey-api/codegen-core@0.7.0': - resolution: {integrity: sha512-HglL4B4QwpzocE+c8qDU6XK8zMf8W8Pcv0RpFDYxHuYALWLTnpDUuEsglC7NQ4vC1maoXsBpMbmwpco0N4QviA==} + '@hey-api/codegen-core@0.7.1': + resolution: {integrity: sha512-X5qG+rr/BJvr+pEGcoW6l2azoZGrVuxsviEIhuf+3VwL9bk0atfubT65Xwo+4jDxXvjbhZvlwS0Ty3I7mLE2fg==} engines: {node: '>=20.19.0'} peerDependencies: typescript: '>=5.5.3' @@ -572,15 +572,15 @@ packages: peerDependencies: typescript: ^5.x - '@hey-api/openapi-ts@0.93.1': - resolution: {integrity: sha512-oQJPHiVkJKesZFpoW3jfQhrSQ7xdgzai7895ENl6ZDjCaIK6bOUTly7bsu+7+0ONsGH9jbtGbkoUzC+MtY+RKg==} + '@hey-api/openapi-ts@0.94.0': + resolution: {integrity: sha512-dbg3GG+v7sg9/Ahb7yFzwzQIJwm151JAtsnh9KtFyqiN0rGkMGA3/VqogEUq1kJB9XWrlMQwigwzhiEQ33VCSg==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: typescript: '>=5.5.3' - '@hey-api/shared@0.2.1': - resolution: {integrity: sha512-uWI9047e9OVe3Ss+6vPMnRiixjRcjcBbdgpeq4IQymet3+wsn0+N/4RLDHBz1h57SemaxayPRUA0JOOsuC1qyA==} + '@hey-api/shared@0.2.2': + resolution: {integrity: sha512-vMqCS+j7F9xpWoXC7TBbqZkaelwrdeuSB+s/3elu54V5iq++S59xhkSq5rOgDIpI1trpE59zZQa6dpyUxItOgw==} engines: {node: '>=20.19.0'} peerDependencies: typescript: '>=5.5.3' @@ -3420,14 +3420,14 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@hey-api/client-axios@0.9.1(@hey-api/openapi-ts@0.93.1(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6)': + '@hey-api/client-axios@0.9.1(@hey-api/openapi-ts@0.94.0(magicast@0.3.5)(typescript@5.9.3))(axios@1.13.6)': dependencies: - '@hey-api/openapi-ts': 0.93.1(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/openapi-ts': 0.94.0(magicast@0.3.5)(typescript@5.9.3) axios: 1.13.6 '@hey-api/client-fetch@0.4.0': {} - '@hey-api/codegen-core@0.7.0(magicast@0.3.5)(typescript@5.9.3)': + '@hey-api/codegen-core@0.7.1(magicast@0.3.5)(typescript@5.9.3)': dependencies: '@hey-api/types': 0.1.3(typescript@5.9.3) ansi-colors: 4.1.3 @@ -3453,11 +3453,11 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/openapi-ts@0.93.1(magicast@0.3.5)(typescript@5.9.3)': + '@hey-api/openapi-ts@0.94.0(magicast@0.3.5)(typescript@5.9.3)': dependencies: - '@hey-api/codegen-core': 0.7.0(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/codegen-core': 0.7.1(magicast@0.3.5)(typescript@5.9.3) '@hey-api/json-schema-ref-parser': 1.3.1 - '@hey-api/shared': 0.2.1(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/shared': 0.2.2(magicast@0.3.5)(typescript@5.9.3) '@hey-api/types': 0.1.3(typescript@5.9.3) ansi-colors: 4.1.3 color-support: 1.1.3 @@ -3466,9 +3466,9 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/shared@0.2.1(magicast@0.3.5)(typescript@5.9.3)': + '@hey-api/shared@0.2.2(magicast@0.3.5)(typescript@5.9.3)': dependencies: - '@hey-api/codegen-core': 0.7.0(magicast@0.3.5)(typescript@5.9.3) + '@hey-api/codegen-core': 0.7.1(magicast@0.3.5)(typescript@5.9.3) '@hey-api/json-schema-ref-parser': 1.3.1 '@hey-api/types': 0.1.3(typescript@5.9.3) ansi-colors: 4.1.3 From 48d7ac5f4488eedbe3e45ef7af0b851d05af58dd Mon Sep 17 00:00:00 2001 From: Andrii Roiko <57030016+roykoand@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:06:48 +0200 Subject: [PATCH 007/595] Improve DAG processor timeout logging clarity (#62328) * Add 'seconds' to logging message * Improve DAG processor timeout logging clarity * Use floats in log message --- airflow-core/src/airflow/dag_processing/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index 6131248980e9d..5ad3618048f13 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -1159,10 +1159,11 @@ def _kill_timed_out_processors(self): duration = now - processor.start_time if duration > self.processor_timeout: self.log.error( - "Processor for %s with PID %s started %d ago killing it.", + "Processor for %s with PID %s has been running for %.2f seconds, exceeding the timeout of %.2f seconds. Killing it!", file, processor.pid, duration, + self.processor_timeout, ) file_name = str(file.rel_path) Stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "timeout"}) From e1632535f785fe83715bcd39da998f459ad400c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Mirowski?= Date: Mon, 9 Mar 2026 21:09:59 +0100 Subject: [PATCH 008/595] Refactor Git-Sync livenessProbe & deprecate readinessProbe & add startupProbe (#62334) * Deploy Git-Sync liveness service * Add recommendedProbeSetting flag * Refactor git-sync probes tests * Remove usage of GitSync readinessProbe * Refactor git-sync liveness probe * Add git-sync startup probe * Misc * Fix spellcheck --- chart/newsfragments/62334.significant.rst | 5 + chart/templates/NOTES.txt | 16 ++ chart/templates/_helpers.yaml | 30 ++- chart/values.schema.json | 73 +++++++- chart/values.yaml | 27 +++ .../airflow_aux/test_airflow_common.py | 18 ++ .../other/test_git_sync_scheduler.py | 174 ++++++++++++++++-- .../other/test_git_sync_triggerer.py | 155 ++++++++++++++-- .../helm_tests/other/test_git_sync_worker.py | 155 ++++++++++++++-- 9 files changed, 613 insertions(+), 40 deletions(-) create mode 100644 chart/newsfragments/62334.significant.rst diff --git a/chart/newsfragments/62334.significant.rst b/chart/newsfragments/62334.significant.rst new file mode 100644 index 0000000000000..924e77869e43b --- /dev/null +++ b/chart/newsfragments/62334.significant.rst @@ -0,0 +1,5 @@ +As Git-Sync is not service-type object, the readiness probe will be removed. To enable feature behaviour set ``dags.gitSync.recommendedProbeSetting`` to ``true``. Section itself will be removed in future release as to not break setups during upgrades. + +As Git-Sync has dedicated liveness service, the liveness probe behaviour will be changed. To enable feature behaviour set ``dags.gitSync.recommendedProbeSetting`` to ``true``. + +Please update your configuration accordingly. diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt index 79d967919dd9b..ba0ec138db661 100644 --- a/chart/templates/NOTES.txt +++ b/chart/templates/NOTES.txt @@ -196,6 +196,14 @@ https://airflow.apache.org/docs/helm-chart/stable/production-guide.html#knownhos {{- end }} +{{- if and .Values.dags.gitSync.enabled .Values.dags.gitSync.readinessProbe }} + + DEPRECATION WARNING: + `dags.gitSync.readinessProbe` section has been removed as Git-Sync is not service-type object. + Please remove overwrite values of section, if defined, as support it will be removed in a future release. + +{{- end }} + {{- if .Values.flower.extraNetworkPolicies }} DEPRECATION WARNING: @@ -501,6 +509,14 @@ DEPRECATION WARNING: {{- end }} +{{- if not .Values.dags.gitSync.recommendedProbeSetting }} + + DEPRECATION WARNING: + Dags Git-Sync bevaiour with `dags.gitSync.recommendedProbeSetting` equal `false` is deprecated and will be removed in future. + Please change your values as support for the old name will be dropped in a future release. + +{{- end }} + {{- if not (or .Values.webserverSecretKey .Values.webserverSecretKeySecretName .Values.apiSecretKey .Values.apiSecretKeySecretName) }} {{ if (semverCompare ">=3.0.0" .Values.airflowVersion) }} ##################################################### diff --git a/chart/templates/_helpers.yaml b/chart/templates/_helpers.yaml index f3f92a87c42c2..42848a9e22e14 100644 --- a/chart/templates/_helpers.yaml +++ b/chart/templates/_helpers.yaml @@ -299,17 +299,43 @@ If release name contains chart name it will be used as a full name. value: "true" - name: GITSYNC_ONE_TIME value: "true" + {{- else }} + - name: GIT_SYNC_HTTP_BIND + value: ":{{ .Values.dags.gitSync.httpPort }}" + - name: GITSYNC_HTTP_BIND + value: ":{{ .Values.dags.gitSync.httpPort }}" {{- end }} {{- with .Values.dags.gitSync.env }} {{- toYaml . | nindent 4 }} {{- end }} resources: {{ toYaml .Values.dags.gitSync.resources | nindent 4 }} - {{- if and .Values.dags.gitSync.livenessProbe (not .is_init) }} + {{- if not .is_init }} + {{- if .Values.dags.gitSync.startupProbe.enabled }} + startupProbe: + httpGet: + path: / + port: {{ .Values.dags.gitSync.httpPort }} + timeoutSeconds: {{ .Values.dags.gitSync.startupProbe.timeoutSeconds }} + initialDelaySeconds: {{ .Values.dags.gitSync.startupProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.dags.gitSync.startupProbe.periodSeconds }} + failureThreshold: {{ .Values.dags.gitSync.startupProbe.failureThreshold }} + {{- end }} + {{- if and .Values.dags.gitSync.recommendedProbeSetting (hasKey .Values.dags.gitSync.livenessProbe "enabled") .Values.dags.gitSync.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.dags.gitSync.httpPort }} + timeoutSeconds: {{ .Values.dags.gitSync.livenessProbe.timeoutSeconds | default 1 }} + initialDelaySeconds: {{ .Values.dags.gitSync.livenessProbe.initialDelaySeconds | default 0 }} + periodSeconds: {{ .Values.dags.gitSync.livenessProbe.periodSeconds | default 5 }} + failureThreshold: {{ .Values.dags.gitSync.livenessProbe.failureThreshold | default 10 }} + {{- else if .Values.dags.gitSync.livenessProbe }} livenessProbe: {{ tpl (toYaml .Values.dags.gitSync.livenessProbe) . | nindent 4 }} {{- end }} - {{- if and .Values.dags.gitSync.readinessProbe (not .is_init) }} + {{- if and .Values.dags.gitSync.readinessProbe (not .Values.dags.gitSync.recommendedProbeSetting) }} readinessProbe: {{ tpl (toYaml .Values.dags.gitSync.readinessProbe) . | nindent 4 }} {{- end }} + {{- end }} volumeMounts: - name: dags mountPath: /git diff --git a/chart/values.schema.json b/chart/values.schema.json index 74d931666da40..5a8dfcd9123a1 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -10649,13 +10649,82 @@ } ] }, + "httpPort": { + "description": "Git-Sync liveness service http bind port.", + "type": "integer", + "default": 1234 + }, + "recommendedProbeSetting": { + "description": "Setting this to true, will remove readiness probe usage and configure liveness probe to use a dedicated Git-Sync liveness service.", + "type": "boolean", + "default": false + }, + "startupProbe": { + "description": "Startup probe configuration for git sync container.", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable GitSync Kubernetes Startup Probe.", + "type": "boolean", + "default": true + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Minimum value is 1 seconds.", + "type": "integer", + "default": 1 + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before startup probe is initiated.", + "type": "integer", + "default": 0 + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Minimum value is 1.", + "type": "integer", + "default": 5 + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Minimum value is 1.", + "type": "integer", + "default": 10 + } + } + }, "livenessProbe": { "description": "Liveness probe configuration for git sync container.", "type": "object", - "$ref": "#/definitions/io.k8s.api.core.v1.Probe" + "additionalProperties": true, + "properties": { + "enabled": { + "description": "Enable GitSync Kubernetes Liveness Probe.", + "type": "boolean", + "default": true + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Minimum value is 1 seconds.", + "type": "integer", + "default": 1 + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before startup probe is initiated.", + "type": "integer", + "default": 0 + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Minimum value is 1.", + "type": "integer", + "default": 5 + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Minimum value is 1.", + "type": "integer", + "default": 10 + } + } }, "readinessProbe": { - "description": "Readiness probe configuration for git sync container.", + "description": "Readiness probe configuration for git sync container. As Git-Sync is not service-type object, the usage of the section was removed. Section itself will be removed in future release as to not break setups during upgrades.", "type": "object", "$ref": "#/definitions/io.k8s.api.core.v1.Probe" }, diff --git a/chart/values.yaml b/chart/values.yaml index 255db1700bc83..bab02bdef30c0 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -3605,8 +3605,35 @@ dags: # container level lifecycle hooks containerLifecycleHooks: {} + # Git-Sync liveness service http bind port + httpPort: 1234 + + # Setting this to true, will remove readinessProbe usage and configure livenessProbe to + # use a dedicated Git-Sync liveness service. In future, behaviour with value true will be + # default one and old one will be removed + recommendedProbeSetting: false + + startupProbe: + enabled: true + timeoutSeconds: 1 + initialDelaySeconds: 0 + periodSeconds: 5 + failureThreshold: 10 + + # As Git-Sync is not service-type object, the usage of this section will be removed. + # By setting dags.gitSync.recommendedProbeSetting to true, you will enable future behaviour. readinessProbe: {} + + # The behaviour of the livenessProbe will change with the next release of Helm Chart. + # To enable future behaviour set dags.gitSync.recommendedProbeSetting to true. + # New behaviour uses the recommended liveness configuration by using Git-Sync built-in + # liveness service livenessProbe: {} + # enabled: true + # timeoutSeconds: 1 + # initialDelaySeconds: 0 + # periodSeconds: 5 + # failureThreshold: 10 # Mount additional volumes into git-sync. It can be templated like in the following example: # extraVolumeMounts: diff --git a/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py b/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py index c5eac7ce2c726..b3ad40e42abd7 100644 --- a/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py +++ b/helm-tests/tests/helm_tests/airflow_aux/test_airflow_common.py @@ -146,6 +146,24 @@ def test_dags_mount(self, dag_values, expected_mount): for doc in docs: assert expected_mount in jmespath.search("spec.template.spec.containers[0].volumeMounts", doc) + def test_git_sync_http_port(self): + docs = render_chart( + values={ + "dags": {"gitSync": {"enabled": True, "httpPort": 10}}, + }, + show_only=[ + "templates/dag-processor/dag-processor-deployment.yaml", + "templates/triggerer/triggerer-deployment.yaml", + "templates/workers/worker-deployment.yaml", + ], + ) + + assert len(docs) == 3 + for doc in docs: + envs = jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].env", doc) + assert {"name": "GIT_SYNC_HTTP_BIND", "value": ":10"} in envs + assert {"name": "GITSYNC_HTTP_BIND", "value": ":10"} in envs + @pytest.mark.parametrize( "workers_values", [ diff --git a/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py b/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py index 9c007a3f49ccc..71b5f69abd0f6 100644 --- a/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py +++ b/helm-tests/tests/helm_tests/other/test_git_sync_scheduler.py @@ -108,9 +108,18 @@ def test_validate_the_git_sync_container_spec(self): {"name": "GITSYNC_PERIOD", "value": "66s"}, {"name": "GIT_SYNC_MAX_SYNC_FAILURES", "value": "70"}, {"name": "GITSYNC_MAX_FAILURES", "value": "70"}, + {"name": "GIT_SYNC_HTTP_BIND", "value": ":1234"}, + {"name": "GITSYNC_HTTP_BIND", "value": ":1234"}, ], "volumeMounts": [{"mountPath": "/git", "name": "dags"}], "resources": {}, + "startupProbe": { + "failureThreshold": 10, + "httpGet": {"path": "/", "port": 1234}, + "initialDelaySeconds": 0, + "periodSeconds": 5, + "timeoutSeconds": 1, + }, } def test_validate_the_git_sync_container_spec_if_wait_specified(self): @@ -172,9 +181,18 @@ def test_validate_the_git_sync_container_spec_if_wait_specified(self): {"name": "GITSYNC_PERIOD", "value": "66s"}, {"name": "GIT_SYNC_MAX_SYNC_FAILURES", "value": "70"}, {"name": "GITSYNC_MAX_FAILURES", "value": "70"}, + {"name": "GIT_SYNC_HTTP_BIND", "value": ":1234"}, + {"name": "GITSYNC_HTTP_BIND", "value": ":1234"}, ], "volumeMounts": [{"mountPath": "/git", "name": "dags"}], "resources": {}, + "startupProbe": { + "failureThreshold": 10, + "httpGet": {"path": "/", "port": 1234}, + "initialDelaySeconds": 0, + "periodSeconds": 5, + "timeoutSeconds": 1, + }, } def test_validate_if_ssh_params_are_added(self): @@ -409,7 +427,7 @@ def test_resources_are_configurable(self): ) assert jmespath.search("spec.template.spec.containers[1].resources.requests.cpu", docs[0]) == "300m" - def test_liveliness_and_readiness_probes_are_configurable(self): + def test_liveness_probe_configuration(self): livenessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -418,6 +436,25 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "livenessProbe": livenessProbe, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], + ) + + assert livenessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) + + def test_readiness_probe_configuration(self): readinessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -426,28 +463,141 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + docs = render_chart( values={ "airflowVersion": "2.11.0", "dags": { "gitSync": { "enabled": True, - "livenessProbe": livenessProbe, "readinessProbe": readinessProbe, }, }, }, show_only=["templates/scheduler/scheduler-deployment.yaml"], ) - container_search_result = jmespath.search( - "spec.template.spec.containers[?name == 'git-sync']", docs[0] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + == readinessProbe + ) + + def test_readiness_probe_configuration_recommended(self): + readinessProbe = { + "failureThreshold": 10, + "exec": {"command": ["/bin/true"]}, + "initialDelaySeconds": 0, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 5, + } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "recommendedProbeSetting": True, + "readinessProbe": readinessProbe, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + is None + ) + + def test_liveness_probe_configuration_recommended(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "livenessProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].livenessProbe", docs[0] + ) + is None ) - init_container_search_result = jmespath.search( - "spec.template.spec.initContainers[?name == 'git-sync-init']", docs[0] + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } + + def test_startup_probe_configuration(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "startupProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/scheduler/scheduler-deployment.yaml"], ) - assert "livenessProbe" in container_search_result[0] - assert "readinessProbe" in container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert livenessProbe == container_search_result[0]["livenessProbe"] - assert readinessProbe == container_search_result[0]["readinessProbe"] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].startupProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].startupProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } diff --git a/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py b/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py index f074cc1bdd4a4..15a2a3b49b38c 100644 --- a/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py +++ b/helm-tests/tests/helm_tests/other/test_git_sync_triggerer.py @@ -77,7 +77,7 @@ def test_validate_if_ssh_params_are_added_with_git_ssh_key(self): "secret": {"secretName": "release-name-ssh-secret", "defaultMode": 288}, } in jmespath.search("spec.template.spec.volumes", docs[0]) - def test_liveliness_and_readiness_probes_are_configurable(self): + def test_liveness_probe_configuration(self): livenessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -86,6 +86,24 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + + docs = render_chart( + values={ + "dags": { + "gitSync": { + "enabled": True, + "livenessProbe": livenessProbe, + }, + } + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert livenessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) + + def test_readiness_probe_configuration(self): readinessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -94,27 +112,140 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + docs = render_chart( values={ "dags": { "gitSync": { "enabled": True, - "livenessProbe": livenessProbe, "readinessProbe": readinessProbe, }, } }, show_only=["templates/triggerer/triggerer-deployment.yaml"], ) - container_search_result = jmespath.search( - "spec.template.spec.containers[?name == 'git-sync']", docs[0] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None ) - init_container_search_result = jmespath.search( - "spec.template.spec.initContainers[?name == 'git-sync-init']", docs[0] + + assert readinessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0] ) - assert "livenessProbe" in container_search_result[0] - assert "readinessProbe" in container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert livenessProbe == container_search_result[0]["livenessProbe"] - assert readinessProbe == container_search_result[0]["readinessProbe"] + + def test_readiness_probe_configuration_recommended(self): + readinessProbe = { + "failureThreshold": 10, + "exec": {"command": ["/bin/true"]}, + "initialDelaySeconds": 0, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 5, + } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "recommendedProbeSetting": True, + "readinessProbe": readinessProbe, + }, + }, + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + is None + ) + + def test_liveness_probe_configuration_recommended(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "livenessProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].livenessProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } + + def test_startup_probe_configuration(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "startupProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/triggerer/triggerer-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].startupProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].startupProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } diff --git a/helm-tests/tests/helm_tests/other/test_git_sync_worker.py b/helm-tests/tests/helm_tests/other/test_git_sync_worker.py index 32fd71e49f21c..03fdfc1217c7c 100644 --- a/helm-tests/tests/helm_tests/other/test_git_sync_worker.py +++ b/helm-tests/tests/helm_tests/other/test_git_sync_worker.py @@ -202,7 +202,7 @@ def test_container_lifecycle_hooks(self): "preStop": {"exec": {"command": ["/bin/sh", "-c", "echo preStop handler > /git/message_start"]}}, } - def test_liveliness_and_readiness_probes_are_configurable(self): + def test_liveness_probe_configuration(self): livenessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -211,6 +211,24 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + + docs = render_chart( + values={ + "dags": { + "gitSync": { + "enabled": True, + "livenessProbe": livenessProbe, + }, + } + }, + show_only=["templates/workers/worker-deployment.yaml"], + ) + + assert livenessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) + + def test_readiness_probe_configuration(self): readinessProbe = { "failureThreshold": 10, "exec": {"command": ["/bin/true"]}, @@ -219,27 +237,140 @@ def test_liveliness_and_readiness_probes_are_configurable(self): "successThreshold": 1, "timeoutSeconds": 5, } + docs = render_chart( values={ "dags": { "gitSync": { "enabled": True, - "livenessProbe": livenessProbe, "readinessProbe": readinessProbe, }, } }, show_only=["templates/workers/worker-deployment.yaml"], ) - container_search_result = jmespath.search( - "spec.template.spec.containers[?name == 'git-sync']", docs[0] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert readinessProbe == jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0] ) - init_container_search_result = jmespath.search( - "spec.template.spec.initContainers[?name == 'git-sync-init']", docs[0] + + def test_readiness_probe_configuration_recommended(self): + readinessProbe = { + "failureThreshold": 10, + "exec": {"command": ["/bin/true"]}, + "initialDelaySeconds": 0, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 5, + } + + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "recommendedProbeSetting": True, + "readinessProbe": readinessProbe, + }, + }, + }, + show_only=["templates/workers/worker-deployment.yaml"], ) - assert "livenessProbe" in container_search_result[0] - assert "readinessProbe" in container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert "readinessProbe" not in init_container_search_result[0] - assert livenessProbe == container_search_result[0]["livenessProbe"] - assert readinessProbe == container_search_result[0]["readinessProbe"] + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].readinessProbe", docs[0] + ) + is None + ) + + assert ( + jmespath.search("spec.template.spec.containers[?name=='git-sync'] | [0].readinessProbe", docs[0]) + is None + ) + + def test_liveness_probe_configuration_recommended(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "livenessProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/workers/worker-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].livenessProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].livenessProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } + + def test_startup_probe_configuration(self): + docs = render_chart( + values={ + "airflowVersion": "2.11.0", + "dags": { + "gitSync": { + "enabled": True, + "httpPort": 10, + "recommendedProbeSetting": True, + "startupProbe": { + "enabled": True, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + }, + }, + }, + }, + show_only=["templates/workers/worker-deployment.yaml"], + ) + + assert ( + jmespath.search( + "spec.template.spec.initContainers[?name=='git-sync-init'] | [0].startupProbe", docs[0] + ) + is None + ) + + assert jmespath.search( + "spec.template.spec.containers[?name=='git-sync'] | [0].startupProbe", docs[0] + ) == { + "httpGet": {"path": "/", "port": 10}, + "timeoutSeconds": 11, + "initialDelaySeconds": 12, + "periodSeconds": 13, + "failureThreshold": 14, + } From c2fe5544de70dc70e4ecbd9e84f0284de28af953 Mon Sep 17 00:00:00 2001 From: Daniel Reeves <31971762+dwreeves@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:14:40 -0400 Subject: [PATCH 009/595] [Snowflake] [Feat] Allow SnowflakeHook + SnowflakeSqlApiHook `private_key_content` to use raw key in addition to base64 encoding (#62378) * allow SnowflakeHook private_key_content to use raw key instead of only base64 encoding. * Make code more DRY by moving get_private_key() to SnowflakeHook. * Fix get_connection call in SnowflakeHook * assert private_key is b64decoded when passed in as b64encoded value * Update Snowflake provider docs to clarify new private_key_content behavior --- .../snowflake/docs/connections/snowflake.rst | 2 +- .../providers/snowflake/hooks/snowflake.py | 85 +++++++++++------- .../snowflake/hooks/snowflake_sql_api.py | 43 +-------- .../unit/snowflake/hooks/test_snowflake.py | 90 ++++++++++++++++++- .../snowflake/hooks/test_snowflake_sql_api.py | 16 ++-- 5 files changed, 150 insertions(+), 86 deletions(-) diff --git a/providers/snowflake/docs/connections/snowflake.rst b/providers/snowflake/docs/connections/snowflake.rst index 900ed04a1ed73..584bbcfa7c497 100644 --- a/providers/snowflake/docs/connections/snowflake.rst +++ b/providers/snowflake/docs/connections/snowflake.rst @@ -64,7 +64,7 @@ Extra (optional) * ``refresh_token``: Specify refresh_token for OAuth connection. * ``azure_conn_id``: Azure Connection ID to be used for retrieving the OAuth token using Azure Entra authentication. Login and Password fields aren't required when using this method. Scope for the Azure OAuth token can be set in the config option ``azure_oauth_scope`` under the section ``[snowflake]``. Requires `apache-airflow-providers-microsoft-azure>=12.8.0`. * ``private_key_file``: Specify the path to the private key file. - * ``private_key_content``: Specify the content of the private key file in base64 encoded format. You can use the following Python code to encode the private key: + * ``private_key_content``: Specify the content of the private key file, either in plain text or base64 encoded. When using the Airflow UI to manage the Snowflake connection, you should base64 encode the ``private_key_content``. You can use the following Python code to encode the private key: .. code-block:: python diff --git a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py index 6199bd6ff54b0..3a9205fd8f519 100644 --- a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py +++ b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py @@ -56,6 +56,7 @@ if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from snowflake.connector import SnowflakeConnection from airflow.providers.openlineage.extractors import OperatorLineage @@ -400,40 +401,9 @@ def _get_static_conn_params(self) -> dict[str, str | None]: if client_store_temporary_credential: conn_config["client_store_temporary_credential"] = client_store_temporary_credential - # If private_key_file is specified in the extra json, load the contents of the file as a private key. - # If private_key_content is specified in the extra json, use it as a private key. - # As a next step, specify this private key in the connection configuration. - # The connection password then becomes the passphrase for the private key. - # If your private key is not encrypted (not recommended), then leave the password empty. - - private_key_file = self._get_field(extra_dict, "private_key_file") - private_key_content = self._get_field(extra_dict, "private_key_content") - - private_key_pem = None - if private_key_content and private_key_file: - raise AirflowException( - "The private_key_file and private_key_content extra fields are mutually exclusive. " - "Please remove one." - ) - if private_key_file: - private_key_file_path = Path(private_key_file) - if not private_key_file_path.is_file() or private_key_file_path.stat().st_size == 0: - raise ValueError("The private_key_file path points to an empty or invalid file.") - if private_key_file_path.stat().st_size > 4096: - raise ValueError("The private_key_file size is too big. Please keep it less than 4 KB.") - private_key_pem = Path(private_key_file_path).read_bytes() - elif private_key_content: - private_key_pem = base64.b64decode(private_key_content) - - if private_key_pem: - passphrase = None - if conn.password: - passphrase = conn.password.strip().encode() - - p_key = serialization.load_pem_private_key( - private_key_pem, password=passphrase, backend=default_backend() - ) + p_key = self.get_private_key() + if p_key: pkb = p_key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, @@ -587,6 +557,55 @@ def _request_oauth_token( response.raise_for_status() return response + def get_private_key(self) -> PrivateKeyTypes | None: + """Get the private key from snowflake connection.""" + conn = self.get_connection(self.get_conn_id()) + extra_dict = conn.extra_dejson + + # If private_key_file is specified in the extra json, load the contents of the file as a private key. + # If private_key_content is specified in the extra json, use it as a private key. + # As a next step, specify this private key in the connection configuration. + # The connection password then becomes the passphrase for the private key. + # If your private key is not encrypted (not recommended), then leave the password empty. + + private_key_file = self._get_field(extra_dict, "private_key_file") + private_key_content = self._get_field(extra_dict, "private_key_content") + + passphrase = None + if conn.password: + passphrase = conn.password.strip().encode() + + private_key_pem = None + p_key = None + + if private_key_content and private_key_file: + raise AirflowException( + "The private_key_file and private_key_content extra fields are mutually exclusive. " + "Please remove one." + ) + if private_key_file: + private_key_file_path = Path(private_key_file) + if not private_key_file_path.is_file() or private_key_file_path.stat().st_size == 0: + raise ValueError("The private_key_file path points to an empty or invalid file.") + if private_key_file_path.stat().st_size > 4096: + raise ValueError("The private_key_file size is too big. Please keep it less than 4 KB.") + private_key_pem = Path(private_key_file_path).read_bytes() + elif private_key_content: + try: + p_key = serialization.load_pem_private_key( + private_key_content.encode(), password=passphrase, backend=default_backend() + ) + except (TypeError, ValueError): + # Assume base64 encoding if string is not valid private key + private_key_pem = base64.b64decode(private_key_content) + + if private_key_pem: + p_key = serialization.load_pem_private_key( + private_key_pem, password=passphrase, backend=default_backend() + ) + + return p_key + def get_uri(self) -> str: """Override DbApiHook get_uri method for get_sqlalchemy_engine().""" conn_params = self._get_conn_params() diff --git a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py index efe45ed9d13bf..2f52be48b8887 100644 --- a/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py +++ b/providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake_sql_api.py @@ -17,19 +17,15 @@ from __future__ import annotations import asyncio -import base64 import time import uuid import warnings from datetime import timedelta -from pathlib import Path from typing import Any import aiohttp import requests from aiohttp import ClientConnectionError, ClientResponseError -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization from requests.exceptions import ConnectionError, HTTPError, Timeout from tenacity import ( AsyncRetrying, @@ -134,43 +130,6 @@ def __init__( self.aiohttp_session_kwargs = aiohttp_session_kwargs or {} self.aiohttp_request_kwargs = aiohttp_request_kwargs or {} - def get_private_key(self) -> None: - """Get the private key from snowflake connection.""" - conn = self.get_connection(self.snowflake_conn_id) - - # If private_key_file is specified in the extra json, load the contents of the file as a private key. - # If private_key_content is specified in the extra json, use it as a private key. - # As a next step, specify this private key in the connection configuration. - # The connection password then becomes the passphrase for the private key. - # If your private key is not encrypted (not recommended), then leave the password empty. - - private_key_file = conn.extra_dejson.get( - "extra__snowflake__private_key_file" - ) or conn.extra_dejson.get("private_key_file") - private_key_content = conn.extra_dejson.get( - "extra__snowflake__private_key_content" - ) or conn.extra_dejson.get("private_key_content") - - private_key_pem = None - if private_key_content and private_key_file: - raise AirflowException( - "The private_key_file and private_key_content extra fields are mutually exclusive. " - "Please remove one." - ) - if private_key_file: - private_key_pem = Path(private_key_file).read_bytes() - elif private_key_content: - private_key_pem = base64.b64decode(private_key_content) - - if private_key_pem: - passphrase = None - if conn.password: - passphrase = conn.password.strip().encode() - - self.private_key = serialization.load_pem_private_key( - private_key_pem, password=passphrase, backend=default_backend() - ) - def execute_query( self, sql: str, statement_count: int, query_tag: str = "", bindings: dict[str, Any] | None = None ) -> list[str]: @@ -272,7 +231,7 @@ def get_headers(self) -> dict[str, Any]: # Alternatively, get the JWT token from the connection details and the private key if not self.private_key: - self.get_private_key() + self.private_key = self.get_private_key() token = JWTGenerator( conn_config["account"], # type: ignore[arg-type] diff --git a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py index fa3f5a65a033c..901ac925c0700 100644 --- a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py +++ b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake.py @@ -372,9 +372,93 @@ def test_hook_should_support_prepare_basic_conn_params_and_uri( assert SnowflakeHook(snowflake_conn_id="test_conn").get_uri() == expected_uri assert SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() == expected_conn_params + def test_plain_text_unencrypted_private_key_is_not_base64_encoded( + self, unencrypted_temporary_private_key: Path + ): + """Test get_private_key function skips base64 encoding if private key is plain text.""" + private_key_content = unencrypted_temporary_private_key.read_text() + + p_key = serialization.load_pem_private_key( + private_key_content.encode(), + password=None, + backend=default_backend(), + ) + + pkb = p_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + connection_kwargs: Any = { + **BASE_CONNECTION_KWARGS, + "password": None, + "extra": { + "database": "db", + "account": "airflow", + "warehouse": "af_wh", + "region": "af_region", + "role": "af_role", + "private_key_content": private_key_content, + }, + } + with mock.patch.dict("os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri()): + conn_params = SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + assert "private_key" in conn_params + assert pkb == conn_params["private_key"] + + def test_plain_text_encrypted_private_key_is_not_base64_encoded( + self, encrypted_temporary_private_key: Path + ): + """Test get_private_key function skips base64 encoding if private key is plain text.""" + private_key_content = encrypted_temporary_private_key.read_text() + + p_key = serialization.load_pem_private_key( + private_key_content.encode(), + password=_PASSWORD.encode(), + backend=default_backend(), + ) + + pkb = p_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + connection_kwargs: Any = { + **BASE_CONNECTION_KWARGS, + "password": _PASSWORD, + "extra": { + "database": "db", + "account": "airflow", + "warehouse": "af_wh", + "region": "af_region", + "role": "af_role", + "private_key_content": private_key_content, + }, + } + with mock.patch.dict("os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri()): + conn_params = SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + assert "private_key" in conn_params + assert pkb == conn_params["private_key"] + def test_get_conn_params_should_support_private_auth_in_connection( - self, base64_encoded_encrypted_private_key: Path + self, base64_encoded_encrypted_private_key: str, encrypted_temporary_private_key: Path ): + private_key_content = encrypted_temporary_private_key.read_text() + + p_key = serialization.load_pem_private_key( + private_key_content.encode(), + password=_PASSWORD.encode(), + backend=default_backend(), + ) + + pkb = p_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + connection_kwargs: Any = { **BASE_CONNECTION_KWARGS, "password": _PASSWORD, @@ -388,7 +472,9 @@ def test_get_conn_params_should_support_private_auth_in_connection( }, } with mock.patch.dict("os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri()): - assert "private_key" in SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + conn_params = SnowflakeHook(snowflake_conn_id="test_conn")._get_conn_params() + assert "private_key" in conn_params + assert conn_params["private_key"] == pkb @pytest.mark.parametrize("include_params", [True, False]) def test_hook_param_beats_extra(self, include_params): diff --git a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py index a5e332df41c17..32e475fa15b1e 100644 --- a/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py +++ b/providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py @@ -561,8 +561,8 @@ def test_get_private_key_should_support_private_auth_in_connection( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None def test_get_private_key_raise_exception( self, encrypted_temporary_private_key: Path, base64_encoded_encrypted_private_key: str @@ -617,8 +617,8 @@ def test_get_private_key_should_support_private_auth_with_encrypted_key( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None def test_get_private_key_should_support_private_auth_with_unencrypted_key( self, @@ -640,15 +640,15 @@ def test_get_private_key_should_support_private_auth_with_unencrypted_key( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None connection_kwargs["password"] = "" with unittest.mock.patch.dict( "os.environ", AIRFLOW_CONN_TEST_CONN=Connection(**connection_kwargs).get_uri() ): hook = SnowflakeSqlApiHook(snowflake_conn_id="test_conn") - hook.get_private_key() - assert hook.private_key is not None + private_key = hook.get_private_key() + assert private_key is not None connection_kwargs["password"] = _PASSWORD with ( unittest.mock.patch.dict( From f388da127bcb8b44865cd1b0c78cbff9e8cc107d Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Mon, 9 Mar 2026 21:22:39 +0100 Subject: [PATCH 010/595] Run test-coverage when airflowctl command has any change (#63216) --- airflow-ctl/.pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airflow-ctl/.pre-commit-config.yaml b/airflow-ctl/.pre-commit-config.yaml index 1f029eb314a83..e63268b077ef8 100644 --- a/airflow-ctl/.pre-commit-config.yaml +++ b/airflow-ctl/.pre-commit-config.yaml @@ -60,4 +60,5 @@ repos: pass_filenames: false files: (?x) - ^src/airflowctl/api/operations\.py$ + ^src/airflowctl/api/operations\.py$| + ^docs/images/command_hashes.txt$ From d614dc73b4f5d7ee8188edf10a668d8a1148684f Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Mon, 9 Mar 2026 16:56:19 -0400 Subject: [PATCH 011/595] Weekly updates, add more groups (#63219) --- .github/dependabot.yml | 66 +++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b8dc1d985aa5f..1362bc6ae6cc4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -81,15 +81,35 @@ updates: directories: - /airflow-core/src/airflow/ui schedule: - interval: daily + interval: "weekly" groups: + react: + patterns: + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" + chakra-ui: + patterns: + - "@chakra-ui/*" + - "@emotion/*" + - "framer-motion" + eslint: + patterns: + - "eslint*" + - "@eslint/*" + typescript: + patterns: + - "typescript*" + - "@typescript-eslint/*" + - "@types/typescript" core-ui-package-updates: patterns: - "*" update-types: - "minor" - "patch" - major-version-updates: + core-ui-major-version-updates: patterns: - "*" applies-to: security-updates @@ -102,15 +122,15 @@ updates: directories: - /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui schedule: - interval: daily + interval: "weekly" groups: - core-ui-package-updates: + auth-ui-package-updates: patterns: - "*" update-types: - "minor" - "patch" - major-version-updates: + auth-ui-major-version-updates: patterns: - "*" applies-to: security-updates @@ -123,7 +143,7 @@ updates: directories: - /dev/react-plugin-tools/react_plugin_template schedule: - interval: daily + interval: "weekly" groups: ui-plugin-template-package-updates: patterns: @@ -131,12 +151,9 @@ updates: update-types: - "minor" - "patch" - ui-plugin-template-major-version-updates: - patterns: - - "*" - applies-to: security-updates - update-types: - - "major" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] - package-ecosystem: npm cooldown: @@ -144,7 +161,7 @@ updates: directories: - /providers/edge3/src/airflow/providers/edge3/plugins/www schedule: - interval: daily + interval: "weekly" groups: edge-ui-package-updates: patterns: @@ -168,15 +185,15 @@ updates: directories: - /registry schedule: - interval: daily + interval: "weekly" groups: - core-ui-package-updates: + registry-package-updates: patterns: - "*" update-types: - "minor" - "patch" - major-version-updates: + registry-major-version-updates: patterns: - "*" applies-to: security-updates @@ -211,10 +228,10 @@ updates: directories: - /airflow-core/src/airflow/ui schedule: - interval: daily + interval: "weekly" target-branch: v3-1-test groups: - core-ui-package-updates: + 3-1-core-ui-package-updates: patterns: - "*" update-types: @@ -230,10 +247,10 @@ updates: directories: - /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui schedule: - interval: daily + interval: "weekly" target-branch: v3-1-test groups: - core-ui-package-updates: + 3-1-auth-ui-package-updates: patterns: - "*" update-types: @@ -253,7 +270,7 @@ updates: - /docker_tests - / schedule: - interval: daily + interval: "weekly" target-branch: v2-11-test groups: pip-dependency-updates: @@ -266,12 +283,15 @@ updates: directories: - /airflow/www/ schedule: - interval: daily + interval: "weekly" target-branch: v2-11-test groups: - core-ui-package-updates: + legacy-ui-package-updates: patterns: - "*" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] - package-ecosystem: "uv" cooldown: From 3118fcdbba0275a977d09072b25f1229646fb414 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:06:09 +0100 Subject: [PATCH 012/595] chore(deps): bump github/codeql-action (#63224) Bumps the github-actions-updates group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.29.0 to 4.32.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ce28f5bb42b7a9f2c824e633a3f6ee835bab6858...0d579ffd059c29b07949a3cce3983f0780820c98) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.32.6 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 81a9bf0d9a23a..0c21c6e43d652 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -52,15 +52,15 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: # Provide more context to the SARIF output (shows up in run.automationDetails.id field) category: "/language:${{matrix.language}}" From 9f7cb12449a54c24d62f9e392d7265b616275d91 Mon Sep 17 00:00:00 2001 From: Vincent <97131062+vincbeck@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:08:01 -0400 Subject: [PATCH 013/595] Update provider release guide to mention Github labels (#63218) --- dev/README_RELEASE_PROVIDERS.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dev/README_RELEASE_PROVIDERS.md b/dev/README_RELEASE_PROVIDERS.md index 2d6e2fda5ceb1..da841c6ae34aa 100644 --- a/dev/README_RELEASE_PROVIDERS.md +++ b/dev/README_RELEASE_PROVIDERS.md @@ -259,7 +259,12 @@ changelogs. If there are, you need to add them to PR and classify the changes ma * if needed adjust version of provider - in changelog and provider.yaml, in case the new change changes classification of the upgrade (patchlevel/minor/major) -Commit the changes and merge the PR, be careful to do it quickly so that no new PRs are merged for +Commit the changes and create the PR. You need to apply the following labels to the PR: + +* `skip common compat check` +* `allow provider dependency bump` + +Once approved, merge it, be careful to do it quickly so that no new PRs are merged for providers in the meantime - if they are, you will miss them in the changelog. In case you want to also release a pre-installed provider that is in ``not-ready`` state (i.e. when From 139cd980afbe5e8853507ed30d4205d353e20d93 Mon Sep 17 00:00:00 2001 From: "leon.jeon" <58650453+Leondon9@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:21:10 +0000 Subject: [PATCH 014/595] Add `--action-on-existing-key` to `pools import` and `connections import` (#62702) closes: #62695 Co-authored-by: claude-flow --- airflow-ctl/src/airflowctl/ctl/cli_config.py | 14 +++--- .../ctl/commands/connection_command.py | 2 +- .../airflowctl/ctl/commands/pool_command.py | 6 +-- .../ctl/commands/test_connections_command.py | 48 ++++++++++++++++++- .../ctl/commands/test_pool_command.py | 36 ++++++++++++-- 5 files changed, 91 insertions(+), 15 deletions(-) diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 28dce22805dd5..5ebfb3809e023 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -254,12 +254,11 @@ def string_lower_type(val): help="The DAG ID of the DAG to pause or unpause", ) -# Variable Commands Args -ARG_VARIABLE_ACTION_ON_EXISTING_KEY = Arg( +ARG_ACTION_ON_EXISTING_KEY = Arg( flags=("-a", "--action-on-existing-key"), type=str, default="overwrite", - help="Action to take if we encounter a variable key that already exists.", + help="Action to take if the entity already exists.", choices=("overwrite", "fail", "skip"), ) @@ -865,7 +864,10 @@ def merge_commands( name="import", help="Import connections from a file exported with local CLI.", func=lazy_load_command("airflowctl.ctl.commands.connection_command.import_"), - args=(Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON file"),), + args=( + Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON file"), + ARG_ACTION_ON_EXISTING_KEY, + ), ), ) @@ -895,7 +897,7 @@ def merge_commands( name="import", help="Import pools", func=lazy_load_command("airflowctl.ctl.commands.pool_command.import_"), - args=(ARG_FILE,), + args=(ARG_FILE, ARG_ACTION_ON_EXISTING_KEY), ), ActionCommand( name="export", @@ -913,7 +915,7 @@ def merge_commands( name="import", help="Import variables from a file exported with local CLI.", func=lazy_load_command("airflowctl.ctl.commands.variable_command.import_"), - args=(ARG_FILE, ARG_VARIABLE_ACTION_ON_EXISTING_KEY), + args=(ARG_FILE, ARG_ACTION_ON_EXISTING_KEY), ), ) diff --git a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py index b689083faa28e..b1a8a820998ac 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py @@ -62,7 +62,7 @@ def import_(args, api_client=NEW_API_CLIENT) -> None: connection_create_action = BulkCreateActionConnectionBody( action="create", entities=list(connections_data.values()), - action_on_existence=BulkActionOnExistence("fail"), + action_on_existence=BulkActionOnExistence(args.action_on_existing_key), ) response = api_client.connections.bulk(BulkBodyConnectionBody(actions=[connection_create_action])) if response.create.errors: diff --git a/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py b/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py index 1437b4f78de7c..08e56eed87b0b 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py @@ -41,7 +41,7 @@ def import_(args, api_client: Client = NEW_API_CLIENT) -> None: if not filepath.exists(): raise SystemExit(f"Missing pools file {args.file}") - success, errors = _import_helper(api_client, filepath) + success, errors = _import_helper(api_client, filepath, BulkActionOnExistence(args.action_on_existing_key)) if errors: raise SystemExit(f"Failed to update pool(s): {errors}") rich.print(success) @@ -83,7 +83,7 @@ def export(args, api_client: Client = NEW_API_CLIENT) -> None: raise SystemExit(f"Failed to export pools: {e}") -def _import_helper(api_client: Client, filepath: Path): +def _import_helper(api_client: Client, filepath: Path, action_on_existence: BulkActionOnExistence): """Help import pools from the json file.""" try: with open(filepath) as f: @@ -113,7 +113,7 @@ def _import_helper(api_client: Client, filepath: Path): BulkCreateActionPoolBody( action="create", entities=pools_to_update, - action_on_existence=BulkActionOnExistence.FAIL, + action_on_existence=action_on_existence, ) ] ) 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 02b56eda99b0e..f944e66ab1f24 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 @@ -17,12 +17,14 @@ from __future__ import annotations import json +from unittest import mock from unittest.mock import patch import pytest -from airflowctl.api.client import ClientKind +from airflowctl.api.client import Client, ClientKind from airflowctl.api.datamodels.generated import ( + BulkActionOnExistence, BulkActionResponse, BulkResponse, ConnectionBody, @@ -176,3 +178,47 @@ def test_import_without_extra_field(self, api_client_maker, tmp_path, monkeypatc extra=None, description="", ) + + @pytest.mark.parametrize( + ("action_on_existing_key", "expected_enum"), + [ + ("overwrite", BulkActionOnExistence.OVERWRITE), + ("skip", BulkActionOnExistence.SKIP), + ("fail", BulkActionOnExistence.FAIL), + ], + ) + def test_import_action_on_existing_key(self, tmp_path, action_on_existing_key, expected_enum): + expected_json_path = tmp_path / self.export_file_name + connection_file = { + self.connection_id: { + "conn_type": "test_type", + "host": "test_host", + "extra": "{}", + "connection_id": self.connection_id, + } + } + expected_json_path.write_text(json.dumps(connection_file)) + + mock_client = mock.MagicMock(spec=Client) + mock_response = mock.MagicMock() + mock_response.create.success = [self.connection_id] + mock_response.create.errors = [] + mock_client.connections.bulk.return_value = mock_response + + connection_command.import_( + self.parser.parse_args( + [ + "connections", + "import", + expected_json_path.as_posix(), + "--action-on-existing-key", + action_on_existing_key, + ] + ), + api_client=mock_client, + ) + + mock_client.connections.bulk.assert_called_once() + bulk_body = mock_client.connections.bulk.call_args[0][0] + action = bulk_body.actions[0] + assert action.action_on_existence == expected_enum diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py index 84152d59c813e..0bc2438929454 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py @@ -48,21 +48,21 @@ def test_import_missing_file(self, mock_client, tmp_path): """Test import with missing file.""" non_existent = tmp_path / "non_existent.json" with pytest.raises(SystemExit, match=f"Missing pools file {non_existent}"): - pool_command.import_(mock.MagicMock(file=non_existent)) + pool_command.import_(mock.MagicMock(file=non_existent, action_on_existing_key="fail")) def test_import_invalid_json(self, mock_client, tmp_path): """Test import with invalid JSON file.""" invalid_json = tmp_path / "invalid.json" invalid_json.write_text("invalid json") with pytest.raises(SystemExit, match="Invalid json file"): - pool_command.import_(mock.MagicMock(file=invalid_json)) + pool_command.import_(mock.MagicMock(file=invalid_json, action_on_existing_key="fail")) def test_import_invalid_pool_config(self, mock_client, tmp_path): """Test import with invalid pool configuration.""" invalid_pool = tmp_path / "invalid_pool.json" invalid_pool.write_text(json.dumps([{"invalid": "config"}])) with pytest.raises(SystemExit, match="Invalid pool configuration: {'invalid': 'config'}"): - pool_command.import_(mock.MagicMock(file=invalid_pool)) + pool_command.import_(mock.MagicMock(file=invalid_pool, action_on_existing_key="fail")) def test_import_success(self, mock_client, tmp_path, capsys): """Test successful pool import.""" @@ -87,7 +87,7 @@ def test_import_success(self, mock_client, tmp_path, capsys): mock_client.pools.bulk.return_value = mock_bulk_builder - pool_command.import_(mock.MagicMock(file=pools_file)) + pool_command.import_(mock.MagicMock(file=pools_file, action_on_existing_key="fail")) # Verify bulk operation was called with correct parameters mock_client.pools.bulk.assert_called_once() @@ -108,6 +108,34 @@ def test_import_success(self, mock_client, tmp_path, capsys): captured = capsys.readouterr() assert str(["test_pool"]) in captured.out + @pytest.mark.parametrize( + ("action_on_existing_key", "expected_enum"), + [ + ("overwrite", BulkActionOnExistence.OVERWRITE), + ("skip", BulkActionOnExistence.SKIP), + ("fail", BulkActionOnExistence.FAIL), + ], + ) + def test_import_action_on_existing_key( + self, mock_client, tmp_path, action_on_existing_key, expected_enum + ): + """Test that --action-on-existing-key is passed through to the bulk API.""" + pools_file = tmp_path / "pools.json" + pools_file.write_text(json.dumps([{"name": "test_pool", "slots": 1}])) + + mock_response = mock.MagicMock() + mock_response.success = ["test_pool"] + mock_response.errors = [] + mock_bulk_builder = mock.MagicMock() + mock_bulk_builder.create = mock_response + mock_client.pools.bulk.return_value = mock_bulk_builder + + pool_command.import_(mock.MagicMock(file=pools_file, action_on_existing_key=action_on_existing_key)) + + call_args = mock_client.pools.bulk.call_args[1] + action = call_args["pools"].actions[0] + assert action.action_on_existence == expected_enum + class TestPoolExportCommand: """Test cases for pool export command.""" From a551e03c68ec8dee23a42db88ae750f1c670953b Mon Sep 17 00:00:00 2001 From: "leon.jeon" <58650453+Leondon9@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:22:00 +0000 Subject: [PATCH 015/595] Send `limit` parameter in `execute_list` server requests (#63048) * Send limit parameter in execute_list server requests execute_list uses a local limit (default 50) for offset arithmetic but did not include it in server requests. The server falls back to its own fallback_page_limit which happens to also default to 50, so current behavior is correct. However, if the server config differs, offsets diverge and pages overlap. Include limit in shared_params so pagination is robust regardless of server configuration. Co-Authored-By: claude-flow * Update airflow-ctl/tests/airflow_ctl/api/test_operations.py Co-authored-by: Henry Chen --------- Co-authored-by: claude-flow Co-authored-by: Henry Chen --- airflow-ctl/src/airflowctl/api/operations.py | 2 +- .../tests/airflow_ctl/api/test_operations.py | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index e4a046ed77421..445d490c08dc3 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -164,7 +164,7 @@ def execute_list( limit: int = 50, params: dict | None = None, ) -> T | ServerResponseError: - shared_params = {**(params or {})} + shared_params = {"limit": limit, **(params or {})} self.response = self.client.get(path, params=shared_params) first_pass = data_model.model_validate_json(self.response.content) total_entries = first_pass.total_entries # type: ignore[attr-defined] diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py b/airflow-ctl/tests/airflow_ctl/api/test_operations.py index 65f058d6656af..f75a3c9678fd2 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py @@ -194,6 +194,38 @@ def test_execute_list(self, total_entries, limit, expected_response): assert expected_response == response + def test_execute_list_sends_limit_to_server(self): + """``limit`` must be included in request params so the server returns + the expected page size. Without it the server uses its own default + (e.g. 100) which causes duplicate entries when ``limit`` differs.""" + mock_client = Mock() + mock_client.get.return_value = Mock( + content=json.dumps({"hellos": [{"name": "hello"}] * 3, "total_entries": 3}) + ) + base_operation = BaseOperations(client=mock_client) + + base_operation.execute_list(path="hello", data_model=HelloCollectionResponse, limit=50) + + call_params = mock_client.get.call_args_list[0] + assert call_params.kwargs["params"]["limit"] == 50 + + def test_execute_list_sends_limit_on_subsequent_pages(self): + """Every paginated request must include ``limit`` so that offset + arithmetic stays consistent with the actual page size returned.""" + mock_client = Mock() + mock_client.get.side_effect = [ + Mock(content=json.dumps({"hellos": [{"name": "a"}, {"name": "b"}], "total_entries": 3})), + Mock(content=json.dumps({"hellos": [{"name": "c"}], "total_entries": 3})), + ] + base_operation = BaseOperations(client=mock_client) + + response = base_operation.execute_list(path="hello", data_model=HelloCollectionResponse, limit=2) + + assert len(response.hellos) == 3 + # Verify limit is sent on both the first and second request + for call in mock_client.get.call_args_list: + assert call.kwargs["params"]["limit"] == 2 + class TestAssetsOperations: asset_id: int = 1 From 504210ef859c8020fd53b2c3a6ac750ec05cfdb1 Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Mon, 9 Mar 2026 16:32:18 -0500 Subject: [PATCH 016/595] Add airflowctl auth token command to print JWT access tokens (#62843) * Add airflowctl auth token command to print JWT access tokens This follows the project's convention of focusing on user impact. The command authenticates with username/password and prints the access token to stdout, which is useful for scripting and piping into other tools. * Add token command to integration tests --------- Co-authored-by: bugraoz93 --- .../test_airflowctl_commands.py | 5 +- airflow-ctl/docs/images/command_hashes.txt | 2 +- airflow-ctl/docs/images/output_auth.svg | 82 ++++++++++--------- airflow-ctl/src/airflowctl/ctl/cli_config.py | 14 ++++ .../airflowctl/ctl/commands/auth_command.py | 17 ++++ 5 files changed, 79 insertions(+), 41 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 d9d002e9eda76..73a2b28c3f0b4 100644 --- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py +++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py @@ -44,10 +44,13 @@ def date_param(): # Passing password via command line is insecure but acceptable for testing purposes # Please do not do this in production, it enables possibility of exposing your credentials -LOGIN_COMMAND = "auth login --username airflow --password airflow" +CREDENTIAL_SUFFIX = "--username airflow --password airflow" +LOGIN_COMMAND = f"auth login {CREDENTIAL_SUFFIX}" LOGIN_COMMAND_SKIP_KEYRING = "auth login --skip-keyring" LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!" TEST_COMMANDS = [ + # Auth commands + f"auth token {CREDENTIAL_SUFFIX}", # Assets commands "assets list", "assets get --asset-id=1", diff --git a/airflow-ctl/docs/images/command_hashes.txt b/airflow-ctl/docs/images/command_hashes.txt index 5922aa473cf1e..b0089d41d1f91 100644 --- a/airflow-ctl/docs/images/command_hashes.txt +++ b/airflow-ctl/docs/images/command_hashes.txt @@ -1,6 +1,6 @@ main:65249416abad6ad24c276fb44326ae15 assets:b3ae2b933e54528bf486ff28e887804d -auth:82bc73405e153df5112f05c4811ab92b +auth:d79e9c7d00c432bdbcbc2a86e2e32053 backfill:bbce9859a2d1ce054ad22db92dea8c05 config:cb175bedf29e8a2c2c6a2ebd13d770a7 connections:e34b6b93f64714986139958c1f370428 diff --git a/airflow-ctl/docs/images/output_auth.svg b/airflow-ctl/docs/images/output_auth.svg index 9f0f482e18623..7e697a2bb1a0c 100644 --- a/airflow-ctl/docs/images/output_auth.svg +++ b/airflow-ctl/docs/images/output_auth.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - + - - Usage:airflowctl auth [-hCOMMAND... - -Manage authentication for CLI. Either pass token from environment  -variable/parameter or pass username and password. - -Positional Arguments: -COMMAND -list-envs -List all CLI environments that the user has logged into -loginLogin to the metadata database for personal usage. JWT Token  -must be provided via parameter. - -Options: --h--helpshow this help message and exit + + Usage:airflowctl auth [-hCOMMAND... + +Manage authentication for CLI. Either pass token from environment  +variable/parameter or pass username and password. + +Positional Arguments: +COMMAND +list-envs +List all CLI environments that the user has logged into +loginLogin to the metadata database for personal usage. JWT Token  +must be provided via parameter. +tokenGenerate and print a JWT token for the given credentials + +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 5ebfb3809e023..bdcd8b2fb9060 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -841,6 +841,20 @@ def merge_commands( func=lazy_load_command("airflowctl.ctl.commands.auth_command.list_envs"), args=(ARG_OUTPUT,), ), + ActionCommand( + name="token", + help="Generate and print a JWT token for the given credentials", + description=( + "Authenticate with username and password and print the access token to stdout. " + "Username and password are prompted interactively if not provided." + ), + func=lazy_load_command("airflowctl.ctl.commands.auth_command.get_token"), + args=( + ARG_AUTH_URL, + ARG_AUTH_USERNAME, + ARG_AUTH_PASSWORD, + ), + ), ) CONFIG_COMMANDS = ( diff --git a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py index 809cde294e8ca..236b8d5c6b8de 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py @@ -104,6 +104,23 @@ def login(args, api_client=NEW_API_CLIENT) -> None: rich.print(success_message) +@provide_api_client(kind=ClientKind.AUTH) +def get_token(args, api_client=NEW_API_CLIENT) -> None: + """Generate and print a JWT token for the given credentials to stdout.""" + username = args.username or input("Username: ") + password = args.password or getpass.getpass("Password: ") + + try: + api_client.refresh_base_url(base_url=args.api_url, kind=ClientKind.AUTH) + login_response = api_client.login.login_with_username_and_password( + LoginBody(username=username, password=password) + ) + print(login_response.access_token) + except Exception as e: + rich.print(f"[red]Token generation failed: {e}[/red]") + sys.exit(1) + + def list_envs(args) -> None: """List all CLI environments that the user has logged into.""" # Get AIRFLOW_HOME From 9ae1875a19a84d8f55948dd85c872ee89ce86036 Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:46:49 +0100 Subject: [PATCH 017/595] Clarify docs on max_active_tasks parameter on a Dag (#63217) --- task-sdk/src/airflow/sdk/definitions/dag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/task-sdk/src/airflow/sdk/definitions/dag.py b/task-sdk/src/airflow/sdk/definitions/dag.py index 8670800c162aa..94e447e37a56d 100644 --- a/task-sdk/src/airflow/sdk/definitions/dag.py +++ b/task-sdk/src/airflow/sdk/definitions/dag.py @@ -362,7 +362,7 @@ class DAG: accessible in templates, namespaced under `params`. These params can be overridden at the task level. :param max_active_tasks: the number of task instances allowed to run - concurrently + concurrently per Dag run. Note that in Airflow 2 this was a global limit on the Dag, since Airflow 3 it is per run. :param max_active_runs: maximum number of active DAG runs, beyond this number of DAG runs in a running state, the scheduler won't create new active DAG runs From 09487854ece5d11832ff437dfbd192f3ec2aa48a Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Mon, 9 Mar 2026 17:07:54 -0500 Subject: [PATCH 018/595] Fix _execution_api_server_url() reading edge.api_url when execution_api_server_url is already set (#63192) Previously, _execution_api_server_url() unconditionally read edge.api_url even when core.execution_api_server_url was explicitly configured, causing an unnecessary AirflowConfigException if only the latter was set. This also caused test_supervise_launch and test_supervise_launch_fail to fail since the test environment does not load provider config. Reorder config reads so core.execution_api_server_url is checked first and edge.api_url is only read as a fallback when needed. Mock _execution_api_server_url in test_supervise_launch and test_supervise_launch_fail to isolate them from config requirements. Add a test case covering core.execution_api_server_url set without edge.api_url. --- .../src/airflow/providers/edge3/cli/worker.py | 4 ++-- .../edge3/tests/unit/edge3/cli/test_worker.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/providers/edge3/src/airflow/providers/edge3/cli/worker.py b/providers/edge3/src/airflow/providers/edge3/cli/worker.py index 6ac43e388f82c..34352717350d4 100644 --- a/providers/edge3/src/airflow/providers/edge3/cli/worker.py +++ b/providers/edge3/src/airflow/providers/edge3/cli/worker.py @@ -82,10 +82,10 @@ def _edge_hostname() -> str: @cache def _execution_api_server_url() -> str: """Get the execution api server url from config or environment.""" - api_url = conf.get("edge", "api_url") execution_api_server_url = conf.get("core", "execution_api_server_url", fallback="") - if not execution_api_server_url and api_url: + if not execution_api_server_url: # Derive execution api url from edge api url as fallback + api_url = conf.get("edge", "api_url") execution_api_server_url = api_url.replace("edge_worker/v1/rpcapi", "execution") logger.info("Using execution api server url: %s", execution_api_server_url) return execution_api_server_url diff --git a/providers/edge3/tests/unit/edge3/cli/test_worker.py b/providers/edge3/tests/unit/edge3/cli/test_worker.py index a9c16d108221f..b2b97034b36ba 100644 --- a/providers/edge3/tests/unit/edge3/cli/test_worker.py +++ b/providers/edge3/tests/unit/edge3/cli/test_worker.py @@ -161,6 +161,10 @@ def mock_edgeworker(self) -> EdgeWorkerModel: }, "https://other-endpoint", ), + ( + {("core", "execution_api_server_url"): "https://direct-execution-endpoint"}, + "https://direct-execution-endpoint", + ), ], ) def test_execution_api_server_url( @@ -173,11 +177,16 @@ def test_execution_api_server_url( url = _execution_api_server_url() assert url == expected_url + @patch( + "airflow.providers.edge3.cli.worker._execution_api_server_url", + return_value="https://mock-execution-api", + ) @patch("airflow.sdk.execution_time.supervisor.supervise") @pytest.mark.asyncio async def test_supervise_launch( self, mock_supervise, + mock_execution_api_url, worker_with_job: EdgeWorker, ): edge_job = worker_with_job.jobs.pop().edge_job @@ -187,11 +196,16 @@ async def test_supervise_launch( assert result == 0 q.put.assert_not_called() + @patch( + "airflow.providers.edge3.cli.worker._execution_api_server_url", + return_value="https://mock-execution-api", + ) @patch("airflow.sdk.execution_time.supervisor.supervise") @pytest.mark.asyncio async def test_supervise_launch_fail( self, mock_supervise, + mock_execution_api_url, worker_with_job: EdgeWorker, ): mock_supervise.side_effect = Exception("Supervise failed") From 9ccf765f8b8d0df837aca7faa3f579dd455816fa Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 9 Mar 2026 23:16:04 +0100 Subject: [PATCH 019/595] CI: Upgrade important CI environment (#63231) --- .github/actions/install-prek/action.yml | 2 +- .pre-commit-config.yaml | 2 +- Dockerfile.ci | 2 +- dev/breeze/doc/ci/02_images.md | 2 +- dev/breeze/pyproject.toml | 2 +- .../commands/release_management_commands.py | 2 +- dev/breeze/uv.lock | 68 +++++++++---------- 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/actions/install-prek/action.yml b/.github/actions/install-prek/action.yml index 6832db8290031..ffa5cd2fd13b1 100644 --- a/.github/actions/install-prek/action.yml +++ b/.github/actions/install-prek/action.yml @@ -27,7 +27,7 @@ inputs: default: "0.10.9" # Keep this comment to allow automatic replacement of uv version prek-version: description: 'prek version to use' - default: "0.3.4" # Keep this comment to allow automatic replacement of prek version + default: "0.3.5" # Keep this comment to allow automatic replacement of prek version save-cache: description: "Whether to save prek cache" required: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66c93463168bb..a68b3080d8836 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -337,7 +337,7 @@ repos: - --line-length - '99999' - repo: https://github.com/codespell-project/codespell - rev: 63c8f8312b7559622c0d82815639671ae42132ac # frozen: v2.4.1 + rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 hooks: - id: codespell name: Run codespell diff --git a/Dockerfile.ci b/Dockerfile.ci index f8851c78e99d3..5b0e6f7eaefc0 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1734,7 +1734,7 @@ COPY --from=scripts common.sh install_packaging_tools.sh install_additional_depe ARG AIRFLOW_PIP_VERSION=26.0.1 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" ARG AIRFLOW_UV_VERSION=0.10.9 -ARG AIRFLOW_PREK_VERSION="0.3.4" +ARG AIRFLOW_PREK_VERSION="0.3.5" # UV_LINK_MODE=copy is needed since we are using cache mounted from the host ENV AIRFLOW_PIP_VERSION=${AIRFLOW_PIP_VERSION} \ diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md index e77fc4b36df96..a24490262c85e 100644 --- a/dev/breeze/doc/ci/02_images.md +++ b/dev/breeze/doc/ci/02_images.md @@ -444,7 +444,7 @@ can be used for CI images: | `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.10.9` | `uv` version used. | -| `AIRFLOW_PREK_VERSION` | `0.3.4` | `prek` version used. | +| `AIRFLOW_PREK_VERSION` | `0.3.5` | `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/pyproject.toml b/dev/breeze/pyproject.toml index 671e91bd95378..d830e8fb296cd 100644 --- a/dev/breeze/pyproject.toml +++ b/dev/breeze/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ "jinja2>=3.1.5", "jsonschema>=4.19.1", "packaging>=25.0", - "prek>=0.3.4", + "prek>=0.3.5", "psutil>=5.9.6", "pygithub>=2.1.1", "pytest-xdist>=3.3.1", 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 65853c1a3fe45..b136318093a65 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py @@ -264,7 +264,7 @@ class VersionedFile(NamedTuple): AIRFLOW_USE_UV = False GITPYTHON_VERSION = "3.1.46" RICH_VERSION = "14.3.3" -PREK_VERSION = "0.3.4" +PREK_VERSION = "0.3.5" HATCH_VERSION = "1.16.5" PYYAML_VERSION = "6.0.3" diff --git a/dev/breeze/uv.lock b/dev/breeze/uv.lock index 98a7d6f468048..497066c112ffb 100644 --- a/dev/breeze/uv.lock +++ b/dev/breeze/uv.lock @@ -74,7 +74,7 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.5" }, { name = "jsonschema", specifier = ">=4.19.1" }, { name = "packaging", specifier = ">=25.0" }, - { name = "prek", specifier = ">=0.3.4" }, + { name = "prek", specifier = ">=0.3.5" }, { name = "psutil", specifier = ">=5.9.6" }, { name = "pygithub", specifier = ">=2.1.1" }, { name = "pytest", specifier = ">=9.0.0" }, @@ -260,30 +260,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.63" +version = "1.42.64" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/2a/33d5d4b16fd97dfd629421ebed2456392eae1553cc401d9f86010c18065e/boto3-1.42.63.tar.gz", hash = "sha256:cd008cfd0d7ea30f1c5e22daf0998c55b7c6c68cb68eea05110e33fe641173d5", size = 112778, upload-time = "2026-03-06T22:47:55.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3e/3f5f58100340f6576aa93da0fe46cabd91ea19baa746b80bd1d46498b0db/boto3-1.42.64.tar.gz", hash = "sha256:58d47897a26adbc22f6390d133dab772fb606ba72695291a8c9e20cba1c7fd23", size = 112773, upload-time = "2026-03-09T19:52:00.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/19/f1d8d2b24871d3d0ccb2cbd0b0cb64a3396d439384bd9643d2c25c641b84/boto3-1.42.63-py3-none-any.whl", hash = "sha256:d502a89a0acc701692ae020d15981f2a82e9eb3485acc651cfd0cf1a3afe79ee", size = 140554, upload-time = "2026-03-06T22:47:53.463Z" }, + { url = "https://files.pythonhosted.org/packages/4c/87/2f02a6db0828f4579aedef7e34ec15262e4aa402d31f31bdbc64ae8e471b/boto3-1.42.64-py3-none-any.whl", hash = "sha256:2ca6b472937a54ba74af0b4bede582ba98c070408db1061fc26d5c3aa8e6e7e6", size = 140557, upload-time = "2026-03-09T19:51:57.652Z" }, ] [[package]] name = "botocore" -version = "1.42.63" +version = "1.42.64" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/eb/a1c042f6638ada552399a9977335a6de2668a85bf80bece193c953531236/botocore-1.42.63.tar.gz", hash = "sha256:1fdfc33cff58d21e8622cf620ba2bba3cff324557932aaf935b5374e4610f059", size = 14965362, upload-time = "2026-03-06T22:47:44.158Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/3c/ac4bc939da695d2c648bf28f7b204ab741e4504e81749ccf943403cc07ca/botocore-1.42.64.tar.gz", hash = "sha256:4ee2aece227b9171ace8b749af694a77ab984fceab1639f2626bd0d6fb1aa69d", size = 14967869, upload-time = "2026-03-09T19:51:46.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/60/17a2d3b94658bb999c6aee7bba6c76b271905debf0c8c8e6ac63ca8491bc/botocore-1.42.63-py3-none-any.whl", hash = "sha256:83f39d04f2b316bdfc59a3cac2d12238bde7126ac99d9a57d910dbd86d58c528", size = 14639889, upload-time = "2026-03-06T22:47:39.347Z" }, + { url = "https://files.pythonhosted.org/packages/33/0f/a0feb9a93da8f583217432dce71ce1940d6d8aa5884bad340872a504ba3f/botocore-1.42.64-py3-none-any.whl", hash = "sha256:f77c5cb76ed30576ed0bc73b591265d03dddffff02a9208d3ee0c790f43d3cd2", size = 14641339, upload-time = "2026-03-09T19:51:41.244Z" }, ] [[package]] @@ -588,11 +588,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.0" +version = "3.25.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, ] [[package]] @@ -1221,26 +1221,26 @@ wheels = [ [[package]] name = "prek" -version = "0.3.4" +version = "0.3.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/51/2324eaad93a4b144853ca1c56da76f357d3a70c7b4fd6659e972d7bb8660/prek-0.3.4.tar.gz", hash = "sha256:56a74d02d8b7dfe3c774ecfcd8c1b4e5f1e1b84369043a8003e8e3a779fce72d", size = 356633, upload-time = "2026-02-28T03:47:13.452Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/d6/277e002e56eeab3a9d48f1ca4cc067d249d6326fc1783b770d70ad5ae2be/prek-0.3.5.tar.gz", hash = "sha256:ca40b6685a4192256bc807f32237af94bf9b8799c0d708b98735738250685642", size = 374806, upload-time = "2026-03-09T10:35:18.842Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/20/1a964cb72582307c2f1dc7f583caab90f42810ad41551e5220592406a4c3/prek-0.3.4-py3-none-linux_armv6l.whl", hash = "sha256:c35192d6e23fe7406bd2f333d1c7dab1a4b34ab9289789f453170f33550aa74d", size = 4641915, upload-time = "2026-02-28T03:47:03.772Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cb/4a21f37102bac37e415b61818344aa85de8d29a581253afa7db8c08d5a33/prek-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f784d78de72a8bbe58a5fe7bde787c364ae88f0aff5222c5c5c7287876c510a", size = 4649166, upload-time = "2026-02-28T03:47:06.164Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/a7c0d117a098d57931428bdb60fcb796e0ebc0478c59288017a2e22eca96/prek-0.3.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50a43f522625e8c968e8c9992accf9e29017abad6c782d6d176b73145ad680b7", size = 4274422, upload-time = "2026-02-28T03:46:59.356Z" }, - { url = "https://files.pythonhosted.org/packages/59/84/81d06df1724d09266df97599a02543d82fde7dfaefd192f09d9b2ccb092f/prek-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4bbb1d3912a88935f35c6ba4466b4242732e3e3a8c608623c708e83cea85de00", size = 4629873, upload-time = "2026-02-28T03:46:56.419Z" }, - { url = "https://files.pythonhosted.org/packages/09/cd/bb0aefa25cfacd8dbced75b9a9d9945707707867fa5635fb69ae1bbc2d88/prek-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca4d4134db8f6e8de3c418317becdf428957e3cab271807f475318105fd46d04", size = 4552507, upload-time = "2026-02-28T03:47:05.004Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c0/578a7af4861afb64ec81c03bfdcc1bb3341bb61f2fff8a094ecf13987a56/prek-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fb6395f6eb76133bb1e11fc718db8144522466cdc2e541d05e7813d1bbcae7d", size = 4865929, upload-time = "2026-02-28T03:47:09.231Z" }, - { url = "https://files.pythonhosted.org/packages/fc/48/f169406590028f7698ef2e1ff5bffd92ca05e017636c1163a2f5ef0f8275/prek-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae17813239ddcb4ae7b38418de4d49afff740f48f8e0556029c96f58e350412", size = 5390286, upload-time = "2026-02-28T03:47:10.796Z" }, - { url = "https://files.pythonhosted.org/packages/05/c5/98a73fec052059c3ae06ce105bef67caca42334c56d84e9ef75df72ba152/prek-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a621a690d9c127afc3d21c275030d364d1fbef3296c095068d3ae80a59546e", size = 4891028, upload-time = "2026-02-28T03:47:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b4/029966e35e59b59c142be7e1d2208ad261709ac1a66aa4a3ce33c5b9f91f/prek-0.3.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d978c31bc3b1f0b3d58895b7c6ac26f077e0ea846da54f46aeee4c7088b1b105", size = 4633986, upload-time = "2026-02-28T03:47:14.351Z" }, - { url = "https://files.pythonhosted.org/packages/1d/27/d122802555745b6940c99fcb41496001c192ddcdf56ec947ec10a0298e05/prek-0.3.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8e089a030f0a023c22a4bb2ec4ff3fcc153585d701cff67acbfca2f37e173ae", size = 4680722, upload-time = "2026-02-28T03:47:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/34/40/92318c96b3a67b4e62ed82741016ede34d97ea9579d3cc1332b167632222/prek-0.3.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8060c72b764f0b88112616763da9dd3a7c293e010f8520b74079893096160a2f", size = 4535623, upload-time = "2026-02-28T03:46:52.221Z" }, - { url = "https://files.pythonhosted.org/packages/df/f5/6b383d94e722637da4926b4f609d36fe432827bb6f035ad46ee02bde66b6/prek-0.3.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:65b23268456b5a763278d4e1ec532f2df33918f13ded85869a1ddff761eb9697", size = 4729879, upload-time = "2026-02-28T03:46:57.886Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/fdc705b807d813fd713ffa4f67f96741542ed1dafbb221206078c06f3df4/prek-0.3.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3975c61139c7b3200e38dc3955e050b0f2615701d3deb9715696a902e850509e", size = 5001569, upload-time = "2026-02-28T03:47:00.892Z" }, - { url = "https://files.pythonhosted.org/packages/84/92/b007a41f58e8192a1e611a21b396ad870d51d7873b7af12068ebae7fc15f/prek-0.3.4-py3-none-win32.whl", hash = "sha256:37449ae82f4dc08b72e542401e3d7318f05d1163e87c31ab260a40f425d6516e", size = 4297057, upload-time = "2026-02-28T03:47:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dc/bcb02de9b11461e8e0c7d3c8fdf8cfa15ac6efe73472a4375549ba5defd2/prek-0.3.4-py3-none-win_amd64.whl", hash = "sha256:60e9aa86ca65de963510ae28c5d94b9d7a97bcbaa6e4cdb5bf5083ed4c45dc71", size = 4655174, upload-time = "2026-02-28T03:46:53.749Z" }, - { url = "https://files.pythonhosted.org/packages/0b/86/98f5598569f4cd3de7161e266fab6a8981e65555f79d4704810c1502ad0a/prek-0.3.4-py3-none-win_arm64.whl", hash = "sha256:486bdae8f4512d3b4f6eb61b83e5b7595da2adca385af4b2b7823c0ab38d1827", size = 4367817, upload-time = "2026-02-28T03:46:55.264Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a9/16dd8d3a50362ebccffe58518af1f1f571c96f0695d7fcd8bbd386585f58/prek-0.3.5-py3-none-linux_armv6l.whl", hash = "sha256:44b3e12791805804f286d103682b42a84e0f98a2687faa37045e9d3375d3d73d", size = 5105604, upload-time = "2026-03-09T10:35:00.332Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/bc6036f5bf03860cda66ab040b32737e54802b71a81ec381839deb25df9e/prek-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3cb451cc51ac068974557491beb4c7d2d41dfde29ed559c1694c8ce23bf53e8", size = 5506155, upload-time = "2026-03-09T10:35:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/02/d9/a3745c2a10509c63b6a118ada766614dd705efefd08f275804d5c807aa4a/prek-0.3.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ad8f5f0d8da53dc94d00b76979af312b3dacccc9dcbc6417756c5dca3633c052", size = 5100383, upload-time = "2026-03-09T10:35:13.302Z" }, + { url = "https://files.pythonhosted.org/packages/43/8e/de965fc515d39309a332789cd3778161f7bc80cde15070bedf17f9f8cb93/prek-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4511e15d34072851ac88e4b2006868fbe13655059ad941d7a0ff9ee17138fd9f", size = 5334913, upload-time = "2026-03-09T10:35:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/44f07e8940256059cfd82520e3cbe0764ab06ddb4aa43148465db00b39ad/prek-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc0b63b8337e2046f51267facaac63ba755bc14aad53991840a5eccba3e5c28", size = 5033825, upload-time = "2026-03-09T10:35:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/94/85/3ff0f96881ff2360c212d310ff23c3cf5a15b223d34fcfa8cdcef203be69/prek-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5fc0d78c3896a674aeb8247a83bbda7efec85274dbdfbc978ceff8d37e4ed20", size = 5438586, upload-time = "2026-03-09T10:34:58.779Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/c6d08d31293400fcb5d427f8e7e6bacfc959988e868ad3a9d97b4d87c4b7/prek-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64cad21cb9072d985179495b77b312f6b81e7b45357d0c68dc1de66e0408eabc", size = 6359714, upload-time = "2026-03-09T10:34:57.454Z" }, + { url = "https://files.pythonhosted.org/packages/ba/18/321dcff9ece8065d42c8c1c7a53a23b45d2b4330aa70993be75dc5f2822f/prek-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45ee84199bb48e013bdfde0c84352c17a44cc42d5792681b86d94e9474aab6f8", size = 5717632, upload-time = "2026-03-09T10:35:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/1288226aa381d0cea403157f4e6b64b356e1a745f2441c31dd9d8a1d63da/prek-0.3.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f43275e5d564e18e52133129ebeb5cb071af7ce4a547766c7f025aa0955dfbb6", size = 5339040, upload-time = "2026-03-09T10:35:03.665Z" }, + { url = "https://files.pythonhosted.org/packages/22/94/cfec83df9c2b8e7ed1608087bcf9538a6a77b4c2e7365123e9e0a3162cd1/prek-0.3.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:abcee520d31522bcbad9311f21326b447694cd5edba33618c25fd023fc9865ec", size = 5162586, upload-time = "2026-03-09T10:35:11.564Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/741d62132f37a5f7cc0fad1168bd31f20dea9628f482f077f569547e0436/prek-0.3.5-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:499c56a94a155790c75a973d351a33f8065579d9094c93f6d451ada5d1e469be", size = 5002933, upload-time = "2026-03-09T10:35:16.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/83/630a5671df6550fcfa67c54955e8a8174eb9b4d97ac38fb05a362029245b/prek-0.3.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:de1065b59f194624adc9dea269d4ff6b50e98a1b5bb662374a9adaa496b3c1eb", size = 5304934, upload-time = "2026-03-09T10:35:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/de/79/67a7afd0c0b6c436630b7dba6e586a42d21d5d6e5778fbd9eba7bbd3dd26/prek-0.3.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a1c4869e45ee341735d07179da3a79fa2afb5959cef8b3c8a71906eb52dc6933", size = 5829914, upload-time = "2026-03-09T10:35:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/37/47/e2fe13b33e7b5fdd9dd1a312f5440208bfe1be6183e54c5c99c10f27d848/prek-0.3.5-py3-none-win32.whl", hash = "sha256:70b2152ecedc58f3f4f69adc884617b0cf44259f7414c44d6268ea6f107672eb", size = 4836910, upload-time = "2026-03-09T10:35:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ab/dc2a139fd4896d11f39631479ed385e86307af7f54059ebe9414bb0d00c6/prek-0.3.5-py3-none-win_amd64.whl", hash = "sha256:01d031b684f7e1546225393af1268d9b4451a44ef6cb9be4101c85c7862e08db", size = 5234234, upload-time = "2026-03-09T10:35:20.193Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/f7256b4b7581444f658e909c3b566f51bfabe56c03e80d107a6932d62040/prek-0.3.5-py3-none-win_arm64.whl", hash = "sha256:aa774168e3d868039ff79422bdef2df8d5a016ed804a9914607dcdd3d41da053", size = 5083330, upload-time = "2026-03-09T10:34:55.469Z" }, ] [[package]] @@ -1474,15 +1474,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.1.1" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/67/09765eacf4e44413c4f8943ba5a317fcb9c7b447c3b8b0b7fce7e3090b0b/python_discovery-1.1.1.tar.gz", hash = "sha256:584c08b141c5b7029f206b4e8b78b1a1764b22121e21519b89dec56936e95b0a", size = 56016, upload-time = "2026-03-07T00:00:56.354Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/16/6f3f5e9258f0733aaca19aa18e298cb3a629ae49363573e78d241abeef59/python_discovery-1.1.2.tar.gz", hash = "sha256:c500bd2153e3afc5f48a61d33ff570b6f3e710d36ceaaf882fa9bbe5cc2cec49", size = 56928, upload-time = "2026-03-09T20:02:28.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/0f/2bf7e3b5a4a65f623cb820feb5793e243fad58ae561015ee15a6152f67a2/python_discovery-1.1.1-py3-none-any.whl", hash = "sha256:69f11073fa2392251e405d4e847d60ffffd25fd762a0dc4d1a7d6b9c3f79f1a3", size = 30732, upload-time = "2026-03-07T00:00:55.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/48/8bdfaec240edb1a79b79201eff38b737fc3c29ce59e2e71271bdd8bafdda/python_discovery-1.1.2-py3-none-any.whl", hash = "sha256:d18edd61b382d62f8bcd004a71ebaabc87df31dbefb30aeed59f4fc6afa005be", size = 31486, upload-time = "2026-03-09T20:02:27.277Z" }, ] [[package]] @@ -2097,7 +2097,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.1.0" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2106,9 +2106,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] [[package]] From 4bbea814d100bf0e31ca8b0029c18ca1f966786b Mon Sep 17 00:00:00 2001 From: Bugra Ozturk Date: Mon, 9 Mar 2026 23:17:37 +0100 Subject: [PATCH 020/595] Add release notes for airflowctl 0.1.3 (#63229) --- airflow-ctl/RELEASE_NOTES.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/airflow-ctl/RELEASE_NOTES.rst b/airflow-ctl/RELEASE_NOTES.rst index 164d9bef98070..acbb4afe80739 100644 --- a/airflow-ctl/RELEASE_NOTES.rst +++ b/airflow-ctl/RELEASE_NOTES.rst @@ -15,6 +15,41 @@ specific language governing permissions and limitations under the License. +airflowctl 0.1.3 (2026-03-09) +----------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +- Add airflowctl auth token command to print JWT access tokens (#62843) +- Add ``--action-on-existing-key`` to ``pools import`` and ``connections import`` (#62702) +- Add retry mechanism to airflowctl and remove flaky integration mark (#63016) +- airflowctl auth login: prompt for credentials interactively when none are provided (#62549) +- feat(airflowctl): support on headless environments (#62217) + +Bug Fixes +^^^^^^^^^ + +- Fix ``airflowctl pools export`` ignoring ``--output`` table/yaml/plain (#62665) +- Fix ``airflowctl connections import`` failure when JSON omits ``extra`` field (#62662) + +Improvements +^^^^^^^^^^^^ + +- Send ``limit`` parameter in ``execute_list`` server requests (#63048) +- Run test coverage when airflowctl command has any change (#63216) +- airflow-ctl: add coverage tests for console formatting output (#62627) +- Clean up stale Python 3.9 workaround in airflow-ctl CLI config parser (#62206) +- Expose ``timetable_partitioned`` in UI API (#62777) + +Miscellaneous +^^^^^^^^^^^^^ + +- CI: upgrade important CI environment (#62610) +- Fix all build-system requirements including transitive dependencies (#62570) +- Add DagRunType for asset materializations (#62276) + + airflowctl 0.1.2 (2026-02-20) ----------------------------- From faa8ada688f2e07b3413e36e1029640e1d546932 Mon Sep 17 00:00:00 2001 From: Xiaodong DENG Date: Mon, 9 Mar 2026 15:19:26 -0700 Subject: [PATCH 021/595] Chart: Support Helm template expressions in podAnnotations (#63019) * Support Helm template expressions in podAnnotations Enable tpl evaluation for `podAnnotations` and `airflowPodAnnotations` across all chart templates, allowing users to use Helm template expressions such as `{{ .Release.Name }}` or `{{ include ... | sha256sum }}` in annotation values. This is useful for adding checksum annotations that trigger pod restarts when dependent ConfigMaps or Secrets change. closes: #62698 * Rename chart newsfragment to PR number 63019 * Fix doc nits in customizing-labels: trim underlines and clarify api-server/webserver wording * Address doc structure: add Customizing Pod Labels subsection under umbrella heading --- chart/docs/customizing-labels.rst | 74 ++++++++- .../pod-template-file.kubernetes-helm-yaml | 2 +- chart/newsfragments/63019.feature.rst | 1 + .../api-server/api-server-deployment.yaml | 4 +- chart/templates/cleanup/cleanup-cronjob.yaml | 4 +- .../dag-processor-deployment.yaml | 4 +- .../database-cleanup-cronjob.yaml | 4 +- chart/templates/flower/flower-deployment.yaml | 2 +- chart/templates/jobs/create-user-job.yaml | 4 +- .../templates/jobs/migrate-database-job.yaml | 4 +- .../pgbouncer/pgbouncer-deployment.yaml | 2 +- chart/templates/redis/redis-statefulset.yaml | 2 +- .../scheduler/scheduler-deployment.yaml | 4 +- chart/templates/statsd/statsd-deployment.yaml | 2 +- .../triggerer/triggerer-deployment.yaml | 4 +- .../webserver/webserver-deployment.yaml | 4 +- .../templates/workers/worker-deployment.yaml | 2 +- chart/values.schema.json | 30 ++-- chart/values.yaml | 19 ++- .../airflow_aux/test_annotations.py | 150 ++++++++++++++++++ 20 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 chart/newsfragments/63019.feature.rst diff --git a/chart/docs/customizing-labels.rst b/chart/docs/customizing-labels.rst index 0fcabeee5021f..6cdf2a3ab6f18 100644 --- a/chart/docs/customizing-labels.rst +++ b/chart/docs/customizing-labels.rst @@ -15,13 +15,16 @@ specific language governing permissions and limitations under the License. -Customizing Labels for Pods -=========================== +Customizing Labels and Annotations for Pods +=========================================== + +Customizing Pod Labels +---------------------- The Helm Chart allows you to customize labels for your Airflow objects. You can set global labels that apply to all objects and pods defined in the chart, as well as component-specific labels for individual Airflow components. Global Labels -------------- +~~~~~~~~~~~~~ Global labels can be set using the ``labels`` parameter in your values file. These labels will be applied to all Airflow objects and pods defined in the chart: @@ -32,7 +35,7 @@ Global labels can be set using the ``labels`` parameter in your values file. The environment: production Component-Specific Labels -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ You can also set specific labels for individual Airflow components, which will be merged with the global labels. Component-specific labels take precedence over global labels, allowing you to override them as needed. @@ -59,3 +62,66 @@ For example, to add specific labels to different components: apiServer: labels: role: ui + +Customizing Pod Annotations +--------------------------- + +Pod annotations can be customized similarly to labels using ``podAnnotations`` and ``airflowPodAnnotations``. + +Global Pod Annotations +~~~~~~~~~~~~~~~~~~~~~~ + +Global pod annotations can be set using ``airflowPodAnnotations``. These are applied to all Airflow component pods (scheduler, api-server/webserver, triggerer, dag-processor and workers): + +.. code-block:: yaml + :caption: values.yaml + + airflowPodAnnotations: + example.com/team: data-platform + +Component-Specific Pod Annotations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each component also supports its own ``podAnnotations``. Component-specific annotations take precedence over global ones: + +.. code-block:: yaml + :caption: values.yaml + + scheduler: + podAnnotations: + example.com/component: scheduler + +Templated Pod Annotations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both ``airflowPodAnnotations`` and ``podAnnotations`` support Helm template expressions. This allows annotations to reference release metadata or compute checksums of chart-managed resources, so that pods automatically restart when those resources change. + +For example, to restart scheduler pods whenever the chart's extra ConfigMaps change: + +.. code-block:: yaml + :caption: values.yaml + + extraConfigMaps: + my-listener-config: + data: | + listener.py: ... + + scheduler: + podAnnotations: + checksum/extra-configmaps: '{{ include (print $.Template.BasePath "/configmaps/extra-configmaps.yaml") . | sha256sum }}' + +You can also reference release metadata: + +.. code-block:: yaml + :caption: values.yaml + + airflowPodAnnotations: + release: '{{ .Release.Name }}' + +.. note:: + + The ``include``/``sha256sum`` pattern only works for resources managed by this chart + (e.g., those created via ``extraConfigMaps`` or ``extraSecrets``). + For ConfigMaps or Secrets created outside the chart, consider using a tool like + `Stakater Reloader `__ to trigger pod restarts + automatically. diff --git a/chart/files/pod-template-file.kubernetes-helm-yaml b/chart/files/pod-template-file.kubernetes-helm-yaml index 4fa1413037ed9..25c2de6f81318 100644 --- a/chart/files/pod-template-file.kubernetes-helm-yaml +++ b/chart/files/pod-template-file.kubernetes-helm-yaml @@ -43,7 +43,7 @@ metadata: {{- mustMerge .Values.workers.labels .Values.labels | toYaml | nindent 4 }} {{- end }} annotations: - {{- toYaml $podAnnotations | nindent 4 }} + {{- tpl (toYaml $podAnnotations) . | nindent 4 }} {{- if or .Values.workers.kubernetes.kerberosInitContainer.enabled .Values.workers.kerberosInitContainer.enabled }} checksum/kerberos-keytab: {{ include (print $.Template.BasePath "/secrets/kerberos-keytab-secret.yaml") . | sha256sum }} {{- end }} diff --git a/chart/newsfragments/63019.feature.rst b/chart/newsfragments/63019.feature.rst new file mode 100644 index 0000000000000..f91881be4bff6 --- /dev/null +++ b/chart/newsfragments/63019.feature.rst @@ -0,0 +1 @@ +Support Helm template expressions in ``podAnnotations`` and ``airflowPodAnnotations`` values. diff --git a/chart/templates/api-server/api-server-deployment.yaml b/chart/templates/api-server/api-server-deployment.yaml index 0d3d8c0cdd38c..4de80ded6ca3d 100644 --- a/chart/templates/api-server/api-server-deployment.yaml +++ b/chart/templates/api-server/api-server-deployment.yaml @@ -91,10 +91,10 @@ spec: checksum/jwt-secret: {{ include (print $.Template.BasePath "/secrets/jwt-secret.yaml") . | sha256sum }} {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.apiServer.podAnnotations }} - {{- toYaml .Values.apiServer.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.apiServer.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.apiServer.hostAliases }} diff --git a/chart/templates/cleanup/cleanup-cronjob.yaml b/chart/templates/cleanup/cleanup-cronjob.yaml index fa52af1da7a19..88ca64e8c429f 100644 --- a/chart/templates/cleanup/cleanup-cronjob.yaml +++ b/chart/templates/cleanup/cleanup-cronjob.yaml @@ -67,10 +67,10 @@ spec: {{- end }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 12 }} {{- end }} {{- if .Values.cleanup.podAnnotations }} - {{- toYaml .Values.cleanup.podAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.cleanup.podAnnotations) . | nindent 12 }} {{- end }} spec: restartPolicy: Never diff --git a/chart/templates/dag-processor/dag-processor-deployment.yaml b/chart/templates/dag-processor/dag-processor-deployment.yaml index 448682224a003..c5045e6ecefea 100644 --- a/chart/templates/dag-processor/dag-processor-deployment.yaml +++ b/chart/templates/dag-processor/dag-processor-deployment.yaml @@ -83,10 +83,10 @@ spec: cluster-autoscaler.kubernetes.io/safe-to-evict: "true" {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.dagProcessor.podAnnotations }} - {{- toYaml .Values.dagProcessor.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.dagProcessor.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.dagProcessor.priorityClassName }} diff --git a/chart/templates/database-cleanup/database-cleanup-cronjob.yaml b/chart/templates/database-cleanup/database-cleanup-cronjob.yaml index f0b25058d50e7..03e0ce08d4b14 100644 --- a/chart/templates/database-cleanup/database-cleanup-cronjob.yaml +++ b/chart/templates/database-cleanup/database-cleanup-cronjob.yaml @@ -67,10 +67,10 @@ spec: {{- end }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 12 }} {{- end }} {{- if .Values.databaseCleanup.podAnnotations }} - {{- toYaml .Values.databaseCleanup.podAnnotations | nindent 12 }} + {{- tpl (toYaml .Values.databaseCleanup.podAnnotations) . | nindent 12 }} {{- end }} spec: restartPolicy: Never diff --git a/chart/templates/flower/flower-deployment.yaml b/chart/templates/flower/flower-deployment.yaml index d79f55173f539..a68c8400c3ef0 100644 --- a/chart/templates/flower/flower-deployment.yaml +++ b/chart/templates/flower/flower-deployment.yaml @@ -69,7 +69,7 @@ spec: checksum/airflow-config: {{ include (print $.Template.BasePath "/configmaps/configmap.yaml") . | sha256sum }} checksum/flower-secret: {{ include (print $.Template.BasePath "/secrets/flower-secret.yaml") . | sha256sum }} {{- if or (.Values.airflowPodAnnotations) (.Values.flower.podAnnotations) }} - {{- mustMerge .Values.flower.podAnnotations .Values.airflowPodAnnotations | toYaml | nindent 8 }} + {{- tpl (mustMerge .Values.flower.podAnnotations .Values.airflowPodAnnotations | toYaml) . | nindent 8 }} {{- end }} spec: nodeSelector: {{- toYaml $nodeSelector | nindent 8 }} diff --git a/chart/templates/jobs/create-user-job.yaml b/chart/templates/jobs/create-user-job.yaml index 6626fb7ff5ba5..1d89502ae714f 100644 --- a/chart/templates/jobs/create-user-job.yaml +++ b/chart/templates/jobs/create-user-job.yaml @@ -66,10 +66,10 @@ spec: {{- if or .Values.airflowPodAnnotations .Values.createUserJob.annotations }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.createUserJob.annotations }} - {{- toYaml .Values.createUserJob.annotations | nindent 8 }} + {{- tpl (toYaml .Values.createUserJob.annotations) . | nindent 8 }} {{- end }} {{- end }} spec: diff --git a/chart/templates/jobs/migrate-database-job.yaml b/chart/templates/jobs/migrate-database-job.yaml index 84915ed039ec3..fe28f6bb0cb8e 100644 --- a/chart/templates/jobs/migrate-database-job.yaml +++ b/chart/templates/jobs/migrate-database-job.yaml @@ -66,10 +66,10 @@ spec: {{- if or .Values.airflowPodAnnotations .Values.migrateDatabaseJob.annotations }} annotations: {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.migrateDatabaseJob.annotations }} - {{- toYaml .Values.migrateDatabaseJob.annotations | nindent 8 }} + {{- tpl (toYaml .Values.migrateDatabaseJob.annotations) . | nindent 8 }} {{- end }} {{- end }} spec: diff --git a/chart/templates/pgbouncer/pgbouncer-deployment.yaml b/chart/templates/pgbouncer/pgbouncer-deployment.yaml index b568c259b1234..0ecbc1e208ff7 100644 --- a/chart/templates/pgbouncer/pgbouncer-deployment.yaml +++ b/chart/templates/pgbouncer/pgbouncer-deployment.yaml @@ -74,7 +74,7 @@ spec: checksum/pgbouncer-config-secret: {{ include (print $.Template.BasePath "/secrets/pgbouncer-config-secret.yaml") . | sha256sum }} checksum/pgbouncer-certificates-secret: {{ include (print $.Template.BasePath "/secrets/pgbouncer-certificates-secret.yaml") . | sha256sum }} {{- if .Values.pgbouncer.podAnnotations }} - {{- toYaml .Values.pgbouncer.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.pgbouncer.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.pgbouncer.priorityClassName }} diff --git a/chart/templates/redis/redis-statefulset.yaml b/chart/templates/redis/redis-statefulset.yaml index f3e5474369c83..77f0c7e458c97 100644 --- a/chart/templates/redis/redis-statefulset.yaml +++ b/chart/templates/redis/redis-statefulset.yaml @@ -67,7 +67,7 @@ spec: {{- if or .Values.redis.safeToEvict .Values.redis.podAnnotations }} annotations: {{- if .Values.redis.podAnnotations }} - {{- toYaml .Values.redis.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.redis.podAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.redis.safeToEvict }} cluster-autoscaler.kubernetes.io/safe-to-evict: "true" diff --git a/chart/templates/scheduler/scheduler-deployment.yaml b/chart/templates/scheduler/scheduler-deployment.yaml index 7f508a8078e10..3514180c874c5 100644 --- a/chart/templates/scheduler/scheduler-deployment.yaml +++ b/chart/templates/scheduler/scheduler-deployment.yaml @@ -109,10 +109,10 @@ spec: cluster-autoscaler.kubernetes.io/safe-to-evict: "true" {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.scheduler.podAnnotations }} - {{- toYaml .Values.scheduler.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.scheduler.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.scheduler.priorityClassName }} diff --git a/chart/templates/statsd/statsd-deployment.yaml b/chart/templates/statsd/statsd-deployment.yaml index b7e624b149eba..0b21999453c27 100644 --- a/chart/templates/statsd/statsd-deployment.yaml +++ b/chart/templates/statsd/statsd-deployment.yaml @@ -68,7 +68,7 @@ spec: annotations: checksum/statsd-config: {{ include (print $.Template.BasePath "/configmaps/statsd-configmap.yaml") . | sha256sum }} {{- if .Values.statsd.podAnnotations }} - {{- toYaml .Values.statsd.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.statsd.podAnnotations) . | nindent 8 }} {{- end }} {{- end }} spec: diff --git a/chart/templates/triggerer/triggerer-deployment.yaml b/chart/templates/triggerer/triggerer-deployment.yaml index dcfa1d1f428ef..41a2f0d3d5501 100644 --- a/chart/templates/triggerer/triggerer-deployment.yaml +++ b/chart/templates/triggerer/triggerer-deployment.yaml @@ -93,10 +93,10 @@ spec: cluster-autoscaler.kubernetes.io/safe-to-evict: "true" {{- end }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.triggerer.podAnnotations }} - {{- toYaml .Values.triggerer.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.triggerer.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.triggerer.priorityClassName }} diff --git a/chart/templates/webserver/webserver-deployment.yaml b/chart/templates/webserver/webserver-deployment.yaml index 08f6b30a4d819..7b958f581acbc 100644 --- a/chart/templates/webserver/webserver-deployment.yaml +++ b/chart/templates/webserver/webserver-deployment.yaml @@ -92,10 +92,10 @@ spec: checksum/extra-configmaps: {{ include (print $.Template.BasePath "/configmaps/extra-configmaps.yaml") . | sha256sum }} checksum/extra-secrets: {{ include (print $.Template.BasePath "/secrets/extra-secrets.yaml") . | sha256sum }} {{- if .Values.airflowPodAnnotations }} - {{- toYaml .Values.airflowPodAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.airflowPodAnnotations) . | nindent 8 }} {{- end }} {{- if .Values.webserver.podAnnotations }} - {{- toYaml .Values.webserver.podAnnotations | nindent 8 }} + {{- tpl (toYaml .Values.webserver.podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.webserver.hostAliases }} diff --git a/chart/templates/workers/worker-deployment.yaml b/chart/templates/workers/worker-deployment.yaml index 4d44c4fa824ec..c810581bf7485 100644 --- a/chart/templates/workers/worker-deployment.yaml +++ b/chart/templates/workers/worker-deployment.yaml @@ -124,7 +124,7 @@ spec: checksum/extra-configmaps: {{ include (print $.Template.BasePath "/configmaps/extra-configmaps.yaml") . | sha256sum }} checksum/extra-secrets: {{ include (print $.Template.BasePath "/secrets/extra-secrets.yaml") . | sha256sum }} {{- if $podAnnotations }} - {{- toYaml $podAnnotations | nindent 8 }} + {{- tpl (toYaml $podAnnotations) . | nindent 8 }} {{- end }} spec: {{- if .Values.workers.runtimeClassName }} diff --git a/chart/values.schema.json b/chart/values.schema.json index 5a8dfcd9123a1..0891db13bd5f7 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -768,7 +768,7 @@ } }, "airflowPodAnnotations": { - "description": "Extra annotations to apply to all Airflow pods.", + "description": "Extra annotations to apply to all Airflow pods (templated).", "type": "object", "default": {}, "x-docsSection": "Kubernetes", @@ -2410,7 +2410,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the Airflow Celery workers and pods created with pod-template-file.", + "description": "Annotations to add to the Airflow Celery workers and pods created with pod-template-file (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -3899,7 +3899,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the scheduler pods.", + "description": "Annotations to add to the scheduler pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -4467,7 +4467,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the triggerer pods.", + "description": "Annotations to add to the triggerer pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -5079,7 +5079,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the dag processor pods.", + "description": "Annotations to add to the dag processor pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -5388,7 +5388,7 @@ ] }, "annotations": { - "description": "Annotations to add to the create user job pod.", + "description": "Annotations to add to the create user job pod (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -5737,7 +5737,7 @@ ] }, "annotations": { - "description": "Annotations to add to the migrate database job pod.", + "description": "Annotations to add to the migrate database job pod (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -6632,7 +6632,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the API server pods.", + "description": "Annotations to add to the API server pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -7459,7 +7459,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the webserver pods.", + "description": "Annotations to add to the webserver pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -8000,7 +8000,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the Flower pods.", + "description": "Annotations to add to the Flower pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -8419,7 +8419,7 @@ } }, "podAnnotations": { - "description": "Annotations to add to the StatsD pods.", + "description": "Annotations to add to the StatsD pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -8623,7 +8623,7 @@ } }, "podAnnotations": { - "description": "Add annotations for the PgBouncer Pod.", + "description": "Add annotations for the PgBouncer Pod (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -9462,7 +9462,7 @@ "default": 0 }, "podAnnotations": { - "description": "Annotations to add to the redis pods.", + "description": "Annotations to add to the redis pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -9838,7 +9838,7 @@ "default": null }, "podAnnotations": { - "description": "Annotations to add to cleanup pods.", + "description": "Annotations to add to cleanup pods (templated).", "type": "object", "default": {}, "additionalProperties": { @@ -10191,7 +10191,7 @@ "default": null }, "podAnnotations": { - "description": "Annotations to add to database cleanup pods.", + "description": "Annotations to add to database cleanup pods (templated).", "type": "object", "default": {}, "additionalProperties": { diff --git a/chart/values.yaml b/chart/values.yaml index bab02bdef30c0..15fa5aeefdb4b 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -338,7 +338,7 @@ networkPolicies: enabled: false # Extra annotations to apply to all -# Airflow pods +# Airflow pods (templated) airflowPodAnnotations: {} # Extra annotations to apply to @@ -973,7 +973,7 @@ workers: # Annotations for the Airflow Celery worker resource annotations: {} - # Pod annotations for the Airflow Celery workers and pods created with pod-template-file + # Pod annotations for the Airflow Celery workers and pods created with pod-template-file (templated) podAnnotations: {} # Labels specific to Airflow Celery workers objects and pods created with pod-template-file @@ -1457,6 +1457,7 @@ scheduler: # annotations for scheduler deployment annotations: {} + # Pod annotations for scheduler pods (templated) podAnnotations: {} # Labels specific to scheduler objects and pods @@ -1541,7 +1542,7 @@ createUserJob: - "{{ if .Values.webserver.defaultUser }}{{ .Values.webserver.defaultUser.lastName }}{{ else }}{{ .Values.createUserJob.defaultUser.lastName }}{{ end }}" - "-p" - "{{ if .Values.webserver.defaultUser }}{{ .Values.webserver.defaultUser.password }}{{ else }}{{ .Values.createUserJob.defaultUser.password }}{{ end }}" - # Annotations on the create user job pod + # Annotations on the create user job pod (templated) annotations: {} # jobAnnotations are annotations on the create user job jobAnnotations: {} @@ -1636,7 +1637,7 @@ migrateDatabaseJob: airflow db migrate - # Annotations on the database migration pod + # Annotations on the database migration pod (templated) annotations: {} # jobAnnotations are annotations on the database migration job jobAnnotations: {} @@ -1852,6 +1853,7 @@ apiServer: # annotations for Airflow API server deployment annotations: {} + # Pod annotations for API server pods (templated) podAnnotations: {} networkPolicy: @@ -2136,6 +2138,7 @@ webserver: # annotations for webserver deployment annotations: {} + # Pod annotations for webserver pods (templated) podAnnotations: {} # Labels specific webserver app @@ -2299,6 +2302,7 @@ triggerer: # annotations for the triggerer deployment annotations: {} + # Pod annotations for triggerer pods (templated) podAnnotations: {} # Labels specific to triggerer objects and pods @@ -2532,6 +2536,7 @@ dagProcessor: # annotations for the dag processor deployment annotations: {} + # Pod annotations for dag processor pods (templated) podAnnotations: {} logGroomerSidecar: @@ -2716,6 +2721,7 @@ flower: # annotations for the flower deployment annotations: {} + # Pod annotations for flower pods (templated) podAnnotations: {} # Labels specific to flower objects and pods @@ -2833,6 +2839,7 @@ statsd: # So, If you use it, ensure all mapping item contains in it. overrideMappings: [] + # Pod annotations for StatsD pods (templated) podAnnotations: {} # Labels specific to statsd objects and pods @@ -2862,6 +2869,7 @@ pgbouncer: # annotations to be added to the PgBouncer deployment annotations: {} + # Pod annotations for PgBouncer pods (templated) podAnnotations: {} # Add custom annotations to the pgbouncer certificates secret @@ -3159,6 +3167,7 @@ redis: # Labels specific to redis objects and pods labels: {} + # Pod annotations for Redis pods (templated) podAnnotations: {} # Auth secret for a private registry (Deprecated - use `imagePullSecrets` instead) @@ -3257,6 +3266,7 @@ cleanup: topologySpreadConstraints: [] priorityClassName: ~ + # Pod annotations for cleanup pods (templated) podAnnotations: {} # Labels specific to cleanup objects and pods @@ -3346,6 +3356,7 @@ databaseCleanup: topologySpreadConstraints: [] priorityClassName: ~ + # Pod annotations for database cleanup pods (templated) podAnnotations: {} # Labels specific to database cleanup objects and pods diff --git a/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py b/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py index 1a2ae85ccd4e3..3df818effa7c2 100644 --- a/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py +++ b/helm-tests/tests/helm_tests/airflow_aux/test_annotations.py @@ -16,7 +16,11 @@ # under the License. from __future__ import annotations +import copy + +import jmespath import pytest +import yaml from chart_utils.helm_template_generator import render_chart @@ -479,6 +483,42 @@ def test_precedence(self, values, show_only, expected_annotations): assert k in annotations assert v == annotations[k] + def test_pod_annotations_are_templated(self, values, show_only, expected_annotations): + templated_values = copy.deepcopy(values) + for val in templated_values.values(): + if isinstance(val, dict) and "podAnnotations" in val: + val["podAnnotations"] = {"release-name": "{{ .Release.Name }}"} + + k8s_objects = render_chart( + values=templated_values, + show_only=[show_only], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + assert annotations["release-name"] == "release-name" + + def test_airflow_pod_annotations_are_templated(self, values, show_only, expected_annotations): + templated_values = copy.deepcopy(values) + templated_values["airflowPodAnnotations"] = {"global-release": "{{ .Release.Name }}"} + + k8s_objects = render_chart( + values=templated_values, + show_only=[show_only], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + # pgbouncer, statsd, and redis do not render airflowPodAnnotations + if "global-release" in annotations: + assert annotations["global-release"] == "release-name" + else: + assert show_only in ( + "templates/pgbouncer/pgbouncer-deployment.yaml", + "templates/statsd/statsd-deployment.yaml", + "templates/redis/redis-statefulset.yaml", + ) + class TestRedisAnnotations: """Tests Redis Annotations.""" @@ -504,3 +544,113 @@ def test_redis_annotations_are_added(self): for k, v in expected_annotations.items(): assert k in obj["metadata"]["annotations"] assert v == obj["metadata"]["annotations"][k] + + +class TestPodTemplateFileAnnotationsTemplating: + """Tests that podAnnotations are templated in the pod template file.""" + + def test_pod_template_file_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "executor": "KubernetesExecutor", + "workers": { + "podAnnotations": { + "release-name": "{{ .Release.Name }}", + }, + }, + }, + show_only=["templates/configmaps/configmap.yaml"], + ) + + assert len(k8s_objects) == 1 + pod_template = k8s_objects[0]["data"]["pod_template_file.yaml"] + annotations = jmespath.search( + "metadata.annotations", + yaml.safe_load(pod_template), + ) + assert annotations["release-name"] == "release-name" + + def test_pod_template_file_global_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "executor": "KubernetesExecutor", + "airflowPodAnnotations": { + "global-release": "{{ .Release.Name }}", + }, + }, + show_only=["templates/configmaps/configmap.yaml"], + ) + + assert len(k8s_objects) == 1 + pod_template = k8s_objects[0]["data"]["pod_template_file.yaml"] + annotations = jmespath.search( + "metadata.annotations", + yaml.safe_load(pod_template), + ) + assert annotations["global-release"] == "release-name" + + +class TestWebserverPodAnnotationsTemplating: + """Tests webserver podAnnotations templating (requires airflowVersion < 3.0.0).""" + + def test_webserver_pod_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "airflowVersion": "2.11.0", + "webserver": { + "podAnnotations": { + "release-name": "{{ .Release.Name }}", + }, + }, + }, + show_only=["templates/webserver/webserver-deployment.yaml"], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + assert annotations["release-name"] == "release-name" + + def test_webserver_airflow_pod_annotations_are_templated(self): + k8s_objects = render_chart( + values={ + "airflowVersion": "2.11.0", + "airflowPodAnnotations": { + "global-release": "{{ .Release.Name }}", + }, + }, + show_only=["templates/webserver/webserver-deployment.yaml"], + ) + + assert len(k8s_objects) == 1 + annotations = get_object_annotations(k8s_objects[0]) + assert annotations["global-release"] == "release-name" + + +class TestJobAnnotationsTemplating: + """Tests that annotations are templated in job templates.""" + + @pytest.mark.parametrize( + ("values", "show_only"), + [ + ( + {"createUserJob": {"annotations": {"job-ann": "{{ .Release.Name }}"}}}, + "templates/jobs/create-user-job.yaml", + ), + ( + {"migrateDatabaseJob": {"annotations": {"job-ann": "{{ .Release.Name }}"}}}, + "templates/jobs/migrate-database-job.yaml", + ), + ], + ) + def test_job_annotations_are_templated(self, values, show_only): + templated_values = copy.deepcopy(values) + templated_values["airflowPodAnnotations"] = {"global-ann": "{{ .Release.Name }}"} + k8s_objects = render_chart( + values=templated_values, + show_only=[show_only], + ) + + assert len(k8s_objects) == 1 + annotations = k8s_objects[0]["spec"]["template"]["metadata"]["annotations"] + assert annotations["global-ann"] == "release-name" + assert annotations["job-ann"] == "release-name" From 806b9aaf37ad2e35a45d6480da90235d7048e5de Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Mon, 9 Mar 2026 22:21:19 +0000 Subject: [PATCH 022/595] Remove unnecessary `id-token: write` from registry workflow (#63232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry build job uses static AWS credentials (access key + secret), not OIDC, so `id-token: write` is not needed. Removing it fixes the `workflow_call` from `publish-docs-to-s3.yml` which only grants `contents: read` — callers cannot escalate permissions for nested jobs. --- .github/workflows/registry-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index a5d92fa955ac6..094820c539f8b 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -70,7 +70,6 @@ jobs: REGISTRY_CACHE_CONTROL: public, max-age=300 permissions: contents: read - id-token: write if: > github.event_name == 'workflow_call' || contains(fromJSON('[ From b2e7e102250d47b866b0302e925c0aec57cb675e Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Mon, 9 Mar 2026 18:18:41 -0500 Subject: [PATCH 023/595] Add real-time concurrency control for edge workers via UI (#63142) * feat(edge3): Add real-time concurrency control for edge workers via UI Previously, operators had no way to dynamically adjust an edge worker's concurrency limit without SSH access and CLI intervention, creating an operational bottleneck in distributed task execution environments. This contribution adds a new REST API endpoint (PATCH /worker/{worker_name}/concurrency) and a React UI control to the Apache Airflow Edge Provider, enabling administrators to tune worker throughput in real time from the Airflow web interface. The change is propagated to the worker process on its next heartbeat cycle without requiring a restart. * Add back what prek removed --- .../plugins/www/openapi-gen/queries/common.ts | 1 + .../www/openapi-gen/queries/queries.ts | 9 +- .../www/openapi-gen/requests/schemas.gen.ts | 15 ++ .../www/openapi-gen/requests/services.gen.ts | 28 +++- .../www/openapi-gen/requests/types.gen.ts | 29 ++++ .../components/WorkerConcurrencyButton.tsx | 157 ++++++++++++++++++ .../www/src/components/WorkerOperations.tsx | 2 + .../edge3/worker_api/datamodels_ui.py | 6 + .../providers/edge3/worker_api/routes/ui.py | 25 +++ .../edge3/worker_api/v2-edge-generated.yaml | 47 ++++++ .../unit/edge3/worker_api/routes/test_ui.py | 27 +++ 11 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts index 4d553521aea43..2b90edb3b00fc 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts @@ -50,3 +50,4 @@ export type UiServiceUpdateWorkerMaintenanceMutationResult = Awaited>; export type UiServiceDeleteWorkerMutationResult = Awaited>; export type UiServiceRemoveWorkerQueueMutationResult = Awaited>; +export type UiServiceSetWorkerConcurrencyLimitMutationResult = Awaited>; diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts index 782a26bbf9114..83cae99c134b1 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts @@ -2,7 +2,7 @@ import { UseMutationOptions, UseQueryOptions, useMutation, useQuery } from "@tanstack/react-query"; import { JobsService, LogsService, MonitorService, UiService, WorkerService } from "../requests/services.gen"; -import { EdgeWorkerState, MaintenanceRequest, PushLogsBody, TaskInstanceState, WorkerQueueUpdateBody, WorkerQueuesBody, WorkerStateBody } from "../requests/types.gen"; +import { ConcurrencyRequest, EdgeWorkerState, MaintenanceRequest, PushLogsBody, TaskInstanceState, WorkerQueueUpdateBody, WorkerQueuesBody, WorkerStateBody } from "../requests/types.gen"; import * as Common from "./common"; export const useLogsServiceLogfilePath = = unknown[]>({ authorization, dagId, mapIndex, runId, taskId, tryNumber }: { authorization: string; @@ -139,3 +139,10 @@ export const useUiServiceRemoveWorkerQueue = ({ mutationFn: ({ queueName, workerName }) => UiService.removeWorkerQueue({ queueName, workerName }) as unknown as Promise, ...options }); +export const useUiServiceSetWorkerConcurrencyLimit = (options?: Omit, "mutationFn">) => useMutation({ mutationFn: ({ requestBody, workerName }) => UiService.setWorkerConcurrencyLimit({ requestBody, workerName }) as unknown as Promise, ...options }); diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts index 11e48528f69e6..51756edfb4452 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts @@ -257,6 +257,21 @@ export const $JobCollectionResponse = { description: 'Job Collection serializer.' } as const; +export const $ConcurrencyRequest = { + properties: { + concurrency: { + type: 'integer', + exclusiveMinimum: 0, + title: 'Concurrency', + description: 'New concurrency limit for the worker.' + } + }, + type: 'object', + required: ['concurrency'], + title: 'ConcurrencyRequest', + description: 'Request body for worker concurrency update.' +} as const; + export const $MaintenanceRequest = { properties: { maintenance_comment: { diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts index da00585e04498..2dd062ec1ff56 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts @@ -3,7 +3,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { FetchData, FetchResponse, StateData, StateResponse, LogfilePathData, LogfilePathResponse, PushLogsData, PushLogsResponse, RegisterData, RegisterResponse, SetStateData, SetStateResponse, UpdateQueuesData, UpdateQueuesResponse, HealthResponse, WorkerData, WorkerResponse, JobsResponse, RequestWorkerMaintenanceData, RequestWorkerMaintenanceResponse, UpdateWorkerMaintenanceData, UpdateWorkerMaintenanceResponse, ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse, RequestWorkerShutdownData, RequestWorkerShutdownResponse, DeleteWorkerData, DeleteWorkerResponse, AddWorkerQueueData, AddWorkerQueueResponse, RemoveWorkerQueueData, RemoveWorkerQueueResponse } from './types.gen'; +import type { FetchData, FetchResponse, StateData, StateResponse, LogfilePathData, LogfilePathResponse, PushLogsData, PushLogsResponse, RegisterData, RegisterResponse, SetStateData, SetStateResponse, UpdateQueuesData, UpdateQueuesResponse, HealthResponse, WorkerData, WorkerResponse, JobsResponse, RequestWorkerMaintenanceData, RequestWorkerMaintenanceResponse, UpdateWorkerMaintenanceData, UpdateWorkerMaintenanceResponse, ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse, RequestWorkerShutdownData, RequestWorkerShutdownResponse, DeleteWorkerData, DeleteWorkerResponse, AddWorkerQueueData, AddWorkerQueueResponse, RemoveWorkerQueueData, RemoveWorkerQueueResponse, SetWorkerConcurrencyLimitData, SetWorkerConcurrencyLimitResponse } from './types.gen'; export class JobsService { /** @@ -472,5 +472,29 @@ export class UiService { } }); } - + + /** + * Set Worker Concurrency Limit + * Set the concurrency limit for an edge worker. + * @param data The data for the request. + * @param data.workerName + * @param data.requestBody + * @returns unknown Successful Response + * @throws ApiError + */ + public static setWorkerConcurrencyLimit(data: SetWorkerConcurrencyLimitData): CancelablePromise { + return __request(OpenAPI, { + method: 'PATCH', + url: '/edge_worker/ui/worker/{worker_name}/concurrency', + path: { + worker_name: data.workerName + }, + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + } \ No newline at end of file diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts index 12e2a71fd645b..4ac26c50b1a40 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts @@ -523,6 +523,20 @@ export type RemoveWorkerQueueData = { export type RemoveWorkerQueueResponse = unknown; +export type ConcurrencyRequest = { + /** + * New concurrency limit for the worker. + */ + concurrency: number; +}; + +export type SetWorkerConcurrencyLimitData = { + workerName: string; + requestBody: ConcurrencyRequest; +}; + +export type SetWorkerConcurrencyLimitResponse = unknown; + export type $OpenApiTs = { '/edge_worker/v1/jobs/fetch/{worker_name}': { post: { @@ -831,4 +845,19 @@ export type $OpenApiTs = { }; }; }; + '/edge_worker/ui/worker/{worker_name}/concurrency': { + patch: { + req: SetWorkerConcurrencyLimitData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; }; \ No newline at end of file diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx new file mode 100644 index 0000000000000..90b8404cb8e50 --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerConcurrencyButton.tsx @@ -0,0 +1,157 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + Button, + CloseButton, + Dialog, + IconButton, + Input, + Portal, + Text, + VStack, + useDisclosure, +} from "@chakra-ui/react"; +import { useUiServiceSetWorkerConcurrencyLimit } from "openapi/queries"; +import type { Worker } from "openapi/requests/types.gen"; +import { useState } from "react"; +import { LuSlidersHorizontal } from "react-icons/lu"; + +interface WorkerConcurrencyButtonProps { + onConcurrencyUpdate: (toast: Record) => void; + worker: Worker; +} + +export const WorkerConcurrencyButton = ({ + onConcurrencyUpdate, + worker, +}: WorkerConcurrencyButtonProps) => { + const { onClose, onOpen, open } = useDisclosure(); + const workerName = worker.worker_name; + const currentConcurrency = worker.sysinfo?.concurrency; + const [concurrency, setConcurrency] = useState( + currentConcurrency !== undefined ? String(currentConcurrency) : "", + ); + + const setConcurrencyMutation = useUiServiceSetWorkerConcurrencyLimit({ + onError: (error: unknown) => { + onConcurrencyUpdate({ + description: `Unable to set concurrency for worker ${workerName}: ${error}`, + title: "Set Concurrency Failed", + type: "error", + }); + }, + onSuccess: () => { + onConcurrencyUpdate({ + description: `Concurrency for worker ${workerName} set to ${concurrency}.`, + title: "Concurrency Updated", + type: "success", + }); + onClose(); + }, + }); + + const handleSetConcurrency = () => { + const value = parseInt(concurrency, 10); + + if (!concurrency.trim() || isNaN(value) || value <= 0) { + onConcurrencyUpdate({ + description: "Please enter a valid concurrency value greater than 0.", + title: "Invalid Input", + type: "error", + }); + return; + } + + setConcurrencyMutation.mutate({ + requestBody: { concurrency: value }, + workerName, + }); + }; + + const handleOpen = () => { + setConcurrency(currentConcurrency !== undefined ? String(currentConcurrency) : ""); + onOpen(); + }; + + const concurrencyValue = parseInt(concurrency, 10); + const isValid = concurrency.trim() !== "" && !isNaN(concurrencyValue) && concurrencyValue > 0; + + return ( + <> + + + + + + + + + + + Set Concurrency for {workerName} + + + + Enter the new concurrency limit for this worker: + + + + + + + + + + + + + + + + + ); +}; diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx index 0d0aa727eea73..1b5322ce08bf5 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx @@ -26,6 +26,7 @@ import { MaintenanceEditCommentButton } from "./MaintenanceEditCommentButton"; import { MaintenanceEnterButton } from "./MaintenanceEnterButton"; import { MaintenanceExitButton } from "./MaintenanceExitButton"; import { RemoveQueueButton } from "./RemoveQueueButton"; +import { WorkerConcurrencyButton } from "./WorkerConcurrencyButton"; import { WorkerDeleteButton } from "./WorkerDeleteButton"; import { WorkerShutdownButton } from "./WorkerShutdownButton"; @@ -48,6 +49,7 @@ export const WorkerOperations = ({ onOperations, worker }: WorkerOperationsProps + diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py index 3aa2e12e9dfed..e38671928a60a 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py @@ -77,3 +77,9 @@ class QueueUpdateRequest(BaseModel): """Request body for queue operations.""" queue_name: Annotated[str, Field(description="Name of the queue to add or remove.")] + + +class ConcurrencyRequest(BaseModel): + """Request body for worker concurrency update.""" + + concurrency: Annotated[int, Field(description="New concurrency limit for the worker.", gt=0)] diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py index f24b1e19729fa..996a3a261283a 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py @@ -38,8 +38,10 @@ remove_worker_queues, request_maintenance, request_shutdown, + set_worker_concurrency, ) from airflow.providers.edge3.worker_api.datamodels_ui import ( + ConcurrencyRequest, Job, JobCollectionResponse, MaintenanceRequest, @@ -325,3 +327,26 @@ def remove_worker_queue( remove_worker_queues(worker_name, [queue_name], session=session) except Exception as e: raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@ui_router.patch( + "/worker/{worker_name}/concurrency", + dependencies=[ + Depends(requires_access_view(access_view=AccessView.JOBS)), + ], +) +def set_worker_concurrency_limit( + worker_name: str, + concurrency_request: ConcurrencyRequest, + session: SessionDep, +) -> None: + """Set the concurrency limit for an edge worker.""" + worker_query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name) + worker = session.scalar(worker_query) + if not worker: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Worker {worker_name} not found") + + try: + set_worker_concurrency(worker_name, concurrency_request.concurrency, session=session) + except Exception as e: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e)) diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml index a5daeacb86fbd..2904a2e0d3d5d 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/v2-edge-generated.yaml @@ -912,6 +912,41 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /edge_worker/ui/worker/{worker_name}/concurrency: + patch: + tags: + - UI + summary: Set Worker Concurrency Limit + description: Set the concurrency limit for an edge worker. + operationId: set_worker_concurrency_limit + security: + - OAuth2PasswordBearer: [] + - HTTPBearer: [] + parameters: + - name: worker_name + in: path + required: true + schema: + type: string + title: Worker Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ConcurrencyRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' components: schemas: BundleInfo: @@ -929,6 +964,18 @@ components: - name title: BundleInfo description: Schema for telling task which bundle to run with. + ConcurrencyRequest: + properties: + concurrency: + type: integer + exclusiveMinimum: 0.0 + title: Concurrency + description: New concurrency limit for the worker. + type: object + required: + - concurrency + title: ConcurrencyRequest + description: Request body for worker concurrency update. EdgeJobFetched: properties: dag_id: diff --git a/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py b/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py index f1934b5d0f6dd..256cdd977d396 100644 --- a/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py +++ b/providers/edge3/tests/unit/edge3/worker_api/routes/test_ui.py @@ -47,3 +47,30 @@ def test_worker(self, session: Session): assert worker_response.total_entries == 1 assert len(worker_response.workers) == 1 assert worker_response.workers[0].worker_name == "worker1" + + def test_set_worker_concurrency_limit(self, session: Session): + from airflow.providers.edge3.worker_api.datamodels_ui import ConcurrencyRequest + from airflow.providers.edge3.worker_api.routes.ui import set_worker_concurrency_limit + + set_worker_concurrency_limit( + worker_name="worker1", + concurrency_request=ConcurrencyRequest(concurrency=4), + session=session, + ) + worker_model = session.get(EdgeWorkerModel, "worker1") + assert worker_model is not None + assert worker_model.concurrency == 4 + + def test_set_worker_concurrency_limit_not_found(self, session: Session): + from fastapi import HTTPException + + from airflow.providers.edge3.worker_api.datamodels_ui import ConcurrencyRequest + from airflow.providers.edge3.worker_api.routes.ui import set_worker_concurrency_limit + + with pytest.raises(HTTPException) as exc_info: + set_worker_concurrency_limit( + worker_name="nonexistent_worker", + concurrency_request=ConcurrencyRequest(concurrency=4), + session=session, + ) + assert exc_info.value.status_code == 404 From 7bc23ef3b564b0b6d1069411b38aee01473266fe Mon Sep 17 00:00:00 2001 From: Subham Date: Tue, 10 Mar 2026 04:50:32 +0530 Subject: [PATCH 024/595] Fix grid view URL for dynamic task groups (#63205) Dynamic task groups with isMapped=true were getting /mapped appended to their URL in the grid view, producing URLs like /tasks/group/{groupId}/mapped which has no matching route (404). The graph view correctly handles this by not appending /mapped for groups. This fix adds the same guard to buildTaskInstanceUrl. closes: #63197 --- airflow-core/newsfragments/63205.bugfix.rst | 1 + .../src/airflow/ui/src/utils/links.test.ts | 19 +++++++++++++++++-- .../src/airflow/ui/src/utils/links.ts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 airflow-core/newsfragments/63205.bugfix.rst diff --git a/airflow-core/newsfragments/63205.bugfix.rst b/airflow-core/newsfragments/63205.bugfix.rst new file mode 100644 index 0000000000000..7e1781bc8ed32 --- /dev/null +++ b/airflow-core/newsfragments/63205.bugfix.rst @@ -0,0 +1 @@ +Fix grid view URL for dynamic task groups producing 404 by not appending ``/mapped`` to group URLs. diff --git a/airflow-core/src/airflow/ui/src/utils/links.test.ts b/airflow-core/src/airflow/ui/src/utils/links.test.ts index 47dd032ebc519..75d175e22a599 100644 --- a/airflow-core/src/airflow/ui/src/utils/links.test.ts +++ b/airflow-core/src/airflow/ui/src/utils/links.test.ts @@ -243,7 +243,7 @@ describe("buildTaskInstanceUrl", () => { }), ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group"); - // Groups should never preserve tabs even for mapped groups + // Groups should never get /mapped appended — no such route exists for task groups expect( buildTaskInstanceUrl({ currentPathname: "/dags/old/runs/old/tasks/group/old_group/events", @@ -254,6 +254,21 @@ describe("buildTaskInstanceUrl", () => { runId: "new_run", taskId: "new_group", }), - ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group/mapped/3"); + ).toBe("/dags/new_dag/runs/new_run/tasks/group/new_group"); + }); + + it("should not append /mapped for dynamic task groups from grid view", () => { + // Regression test for https://github.com/apache/airflow/issues/63197 + // Dynamic task groups have isMapped=true but no route exists for group/:groupId/mapped + expect( + buildTaskInstanceUrl({ + currentPathname: "/dags/my_dag/runs/run_1/tasks/group/my_group", + dagId: "my_dag", + isGroup: true, + isMapped: true, + runId: "run_1", + taskId: "my_group", + }), + ).toBe("/dags/my_dag/runs/run_1/tasks/group/my_group"); }); }); diff --git a/airflow-core/src/airflow/ui/src/utils/links.ts b/airflow-core/src/airflow/ui/src/utils/links.ts index 3beafb06afea1..23c438721a5b8 100644 --- a/airflow-core/src/airflow/ui/src/utils/links.ts +++ b/airflow-core/src/airflow/ui/src/utils/links.ts @@ -89,7 +89,7 @@ export const buildTaskInstanceUrl = (params: { let basePath = `/dags/${dagId}/runs/${runId}/tasks/${groupPath}${taskId}`; - if (isMapped) { + if (isMapped && !isGroup) { basePath += `/mapped`; if (mapIndex !== undefined && mapIndex !== "-1") { basePath += `/${mapIndex}`; From 51cd44d7b86f5c6abd0854866eb26f83e9b8ade5 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:35:23 -0700 Subject: [PATCH 025/595] fix(providers/oracle): use conn.schema as service_name fallback in OracleHook (#62895) * fix(providers/oracle): use conn.schema as service_name fallback in OracleHook When creating an Oracle connection via the UI with Host, Port, and Schema fields filled but without explicitly setting service_name in extras, get_conn() built the DSN without a service name, causing TNS errors. Now conn.schema is used as the service_name when neither service_name nor sid is set in connection extras. Fixes apache/airflow#62526 * ci: retrigger CI (unrelated static check failures) * fix: remove thick_mode from schema_as_service_name test to avoid CI Oracle client dependency --- .../airflow/providers/oracle/hooks/oracle.py | 4 +++ .../tests/unit/oracle/hooks/test_oracle.py | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py b/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py index 6bd444f460aa9..97721224c0b7e 100644 --- a/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py +++ b/providers/oracle/src/airflow/providers/oracle/hooks/oracle.py @@ -216,6 +216,10 @@ def get_conn(self) -> oracledb.Connection: # Set up DSN service_name = conn.extra_dejson.get("service_name") + # Fall back to conn.schema as service_name when not explicitly set in extras. + # The UI Schema field maps to conn.schema which is the Oracle service name. + if not service_name and not sid and schema: + service_name = schema port = conn.port if conn.port else DEFAULT_DB_PORT if conn.host and sid and not service_name: conn_config["dsn"] = oracledb.makedsn(conn.host, port, sid) diff --git a/providers/oracle/tests/unit/oracle/hooks/test_oracle.py b/providers/oracle/tests/unit/oracle/hooks/test_oracle.py index 2810b698acce5..e007361a26785 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_oracle.py +++ b/providers/oracle/tests/unit/oracle/hooks/test_oracle.py @@ -141,6 +141,36 @@ def test_get_conn_expire_time(self, mock_connect): assert args == () assert kwargs["expire_time"] == 10 + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") + def test_get_conn_schema_as_service_name(self, mock_connect): + """When service_name and sid are not in extras, conn.schema should be used as service_name.""" + self.connection.schema = "MY_SERVICE" + self.connection.extra = json.dumps({}) + self.db_hook.get_conn() + assert mock_connect.call_count == 1 + args, kwargs = mock_connect.call_args + assert kwargs["dsn"] == oracledb.makedsn("host", 1521, service_name="MY_SERVICE") + + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") + def test_get_conn_schema_not_used_when_service_name_set(self, mock_connect): + """Explicit service_name in extras takes precedence over conn.schema.""" + self.connection.schema = "MY_SCHEMA" + self.connection.extra = json.dumps({"service_name": "EXPLICIT_SVC"}) + self.db_hook.get_conn() + assert mock_connect.call_count == 1 + args, kwargs = mock_connect.call_args + assert kwargs["dsn"] == oracledb.makedsn("host", 1521, service_name="EXPLICIT_SVC") + + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") + def test_get_conn_schema_not_used_when_sid_set(self, mock_connect): + """Explicit sid in extras takes precedence over conn.schema.""" + self.connection.schema = "MY_SCHEMA" + self.connection.extra = json.dumps({"sid": "MY_SID"}) + self.db_hook.get_conn() + assert mock_connect.call_count == 1 + args, kwargs = mock_connect.call_args + assert kwargs["dsn"] == oracledb.makedsn("host", 1521, "MY_SID") + @mock.patch("airflow.providers.oracle.hooks.oracle.oracledb.connect") def test_set_current_schema(self, mock_connect): self.connection.schema = "schema_name" From 92ffe9e6c48b0d07b80321c6e2bca3c7be539225 Mon Sep 17 00:00:00 2001 From: Vincent <97131062+vincbeck@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:11:29 -0400 Subject: [PATCH 026/595] Prepare providers release 2026-03-09 (#63198) --- providers/.last_release_date.txt | 2 +- providers/airbyte/README.rst | 8 ++-- providers/airbyte/docs/changelog.rst | 14 ++++++ providers/airbyte/docs/index.rst | 6 +-- providers/airbyte/provider.yaml | 3 +- providers/airbyte/pyproject.toml | 6 +-- .../src/airflow/providers/airbyte/__init__.py | 2 +- providers/alibaba/README.rst | 6 +-- providers/alibaba/docs/changelog.rst | 13 ++++++ providers/alibaba/docs/index.rst | 6 +-- providers/alibaba/provider.yaml | 3 +- providers/alibaba/pyproject.toml | 6 +-- .../src/airflow/providers/alibaba/__init__.py | 2 +- providers/amazon/README.rst | 6 +-- providers/amazon/docs/changelog.rst | 27 ++++++++++++ providers/amazon/docs/index.rst | 6 +-- providers/amazon/provider.yaml | 3 +- providers/amazon/pyproject.toml | 6 +-- .../src/airflow/providers/amazon/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- providers/apache/flink/README.rst | 14 +++--- providers/apache/flink/docs/changelog.rst | 13 ++++++ providers/apache/flink/docs/index.rst | 6 +-- providers/apache/flink/provider.yaml | 3 +- providers/apache/flink/pyproject.toml | 6 +-- .../providers/apache/flink/__init__.py | 2 +- .../hdfs/docs/.latest-doc-only-change.txt | 2 +- providers/apache/hive/README.rst | 6 +-- providers/apache/hive/docs/changelog.rst | 17 +++++++ providers/apache/hive/docs/index.rst | 6 +-- providers/apache/hive/provider.yaml | 3 +- providers/apache/hive/pyproject.toml | 6 +-- .../airflow/providers/apache/hive/__init__.py | 2 +- providers/apache/iceberg/README.rst | 27 +++++------- providers/apache/iceberg/docs/changelog.rst | 18 ++++++++ .../impala/docs/.latest-doc-only-change.txt | 2 +- providers/apache/kafka/README.rst | 6 +-- providers/apache/kafka/docs/changelog.rst | 13 ++++++ providers/apache/kafka/docs/index.rst | 6 +-- providers/apache/kafka/provider.yaml | 3 +- providers/apache/kafka/pyproject.toml | 6 +-- .../providers/apache/kafka/__init__.py | 2 +- .../kylin/docs/.latest-doc-only-change.txt | 2 +- .../pig/docs/.latest-doc-only-change.txt | 2 +- providers/apache/spark/README.rst | 6 +-- providers/apache/spark/docs/changelog.rst | 11 +++++ providers/apache/spark/docs/index.rst | 6 +-- providers/apache/spark/provider.yaml | 3 +- providers/apache/spark/pyproject.toml | 6 +-- .../providers/apache/spark/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../apprise/docs/.latest-doc-only-change.txt | 2 +- .../arangodb/docs/.latest-doc-only-change.txt | 2 +- .../asana/docs/.latest-doc-only-change.txt | 2 +- .../jira/docs/.latest-doc-only-change.txt | 2 +- providers/celery/README.rst | 6 +-- providers/celery/docs/changelog.rst | 11 +++++ providers/celery/docs/index.rst | 6 +-- providers/celery/provider.yaml | 3 +- providers/celery/pyproject.toml | 6 +-- .../src/airflow/providers/celery/__init__.py | 2 +- .../cloudant/docs/.latest-doc-only-change.txt | 2 +- providers/cncf/kubernetes/README.rst | 10 ++--- providers/cncf/kubernetes/docs/changelog.rst | 23 ++++++++++ providers/cncf/kubernetes/docs/index.rst | 8 ++-- providers/cncf/kubernetes/provider.yaml | 3 +- providers/cncf/kubernetes/pyproject.toml | 8 ++-- .../providers/cncf/kubernetes/__init__.py | 2 +- providers/cohere/README.rst | 6 +-- providers/cohere/docs/changelog.rst | 13 ++++++ providers/cohere/docs/index.rst | 6 +-- providers/cohere/provider.yaml | 3 +- providers/cohere/pyproject.toml | 6 +-- .../src/airflow/providers/cohere/__init__.py | 2 +- providers/common/ai/docs/index.rst | 4 +- providers/common/ai/pyproject.toml | 4 +- providers/common/compat/README.rst | 6 +-- providers/common/compat/docs/changelog.rst | 13 ++++++ providers/common/compat/docs/index.rst | 6 +-- providers/common/compat/provider.yaml | 3 +- providers/common/compat/pyproject.toml | 6 +-- .../providers/common/compat/__init__.py | 2 +- .../io/docs/.latest-doc-only-change.txt | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- providers/common/sql/README.rst | 44 +++++++++++-------- providers/common/sql/docs/changelog.rst | 28 ++++++++++++ providers/common/sql/docs/index.rst | 8 ++-- providers/common/sql/provider.yaml | 3 +- providers/common/sql/pyproject.toml | 8 ++-- .../airflow/providers/common/sql/__init__.py | 2 +- providers/databricks/README.rst | 6 +-- providers/databricks/docs/changelog.rst | 21 +++++++++ providers/databricks/docs/index.rst | 6 +-- providers/databricks/provider.yaml | 3 +- providers/databricks/pyproject.toml | 6 +-- .../airflow/providers/databricks/__init__.py | 2 +- .../datadog/docs/.latest-doc-only-change.txt | 2 +- providers/dbt/cloud/README.rst | 6 +-- providers/dbt/cloud/docs/changelog.rst | 16 +++++++ providers/dbt/cloud/docs/index.rst | 6 +-- providers/dbt/cloud/provider.yaml | 3 +- providers/dbt/cloud/pyproject.toml | 6 +-- .../airflow/providers/dbt/cloud/__init__.py | 2 +- .../dingding/docs/.latest-doc-only-change.txt | 2 +- .../discord/docs/.latest-doc-only-change.txt | 2 +- providers/docker/README.rst | 15 ++----- providers/docker/docs/changelog.rst | 15 +++++++ providers/docker/docs/index.rst | 6 +-- providers/docker/provider.yaml | 3 +- providers/docker/pyproject.toml | 6 +-- .../src/airflow/providers/docker/__init__.py | 2 +- providers/edge3/README.rst | 6 +-- providers/edge3/docs/changelog.rst | 29 +++++++++++- providers/edge3/docs/index.rst | 6 +-- providers/edge3/provider.yaml | 3 +- providers/edge3/pyproject.toml | 6 +-- .../src/airflow/providers/edge3/__init__.py | 2 +- providers/fab/README.rst | 22 +++++----- providers/fab/docs/changelog.rst | 31 +++++++++++++ providers/fab/docs/index.rst | 6 +-- providers/fab/provider.yaml | 3 +- providers/fab/pyproject.toml | 6 +-- .../fab/src/airflow/providers/fab/__init__.py | 2 +- .../facebook/docs/.latest-doc-only-change.txt | 2 +- .../ftp/docs/.latest-doc-only-change.txt | 2 +- .../git/docs/.latest-doc-only-change.txt | 2 +- .../github/docs/.latest-doc-only-change.txt | 2 +- providers/google/README.rst | 13 +++--- providers/google/docs/changelog.rst | 42 ++++++++++++++++++ providers/google/docs/index.rst | 6 +-- providers/google/provider.yaml | 3 +- providers/google/pyproject.toml | 6 +-- .../src/airflow/providers/google/__init__.py | 2 +- .../grpc/docs/.latest-doc-only-change.txt | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../imap/docs/.latest-doc-only-change.txt | 2 +- .../influxdb/docs/.latest-doc-only-change.txt | 2 +- providers/jdbc/README.rst | 6 +-- providers/jdbc/docs/changelog.rst | 11 +++++ providers/jdbc/docs/index.rst | 6 +-- providers/jdbc/provider.yaml | 3 +- providers/jdbc/pyproject.toml | 6 +-- .../src/airflow/providers/jdbc/__init__.py | 2 +- providers/jenkins/README.rst | 6 +-- providers/jenkins/docs/changelog.rst | 13 ++++++ providers/jenkins/docs/index.rst | 6 +-- providers/jenkins/provider.yaml | 3 +- providers/jenkins/pyproject.toml | 6 +-- .../src/airflow/providers/jenkins/__init__.py | 2 +- providers/keycloak/README.rst | 6 +-- providers/keycloak/docs/changelog.rst | 19 ++++++++ providers/keycloak/docs/index.rst | 6 +-- providers/keycloak/provider.yaml | 3 +- providers/keycloak/pyproject.toml | 6 +-- .../airflow/providers/keycloak/__init__.py | 2 +- providers/microsoft/azure/README.rst | 6 +-- providers/microsoft/azure/docs/changelog.rst | 11 +++++ providers/microsoft/azure/docs/index.rst | 6 +-- providers/microsoft/azure/provider.yaml | 3 +- providers/microsoft/azure/pyproject.toml | 6 +-- .../providers/microsoft/azure/__init__.py | 2 +- .../psrp/docs/.latest-doc-only-change.txt | 2 +- providers/mongo/README.rst | 6 +-- providers/mongo/docs/changelog.rst | 13 ++++++ providers/mongo/docs/index.rst | 6 +-- providers/mongo/provider.yaml | 3 +- providers/mongo/pyproject.toml | 6 +-- .../src/airflow/providers/mongo/__init__.py | 2 +- .../neo4j/docs/.latest-doc-only-change.txt | 2 +- .../openai/docs/.latest-doc-only-change.txt | 2 +- .../openfaas/docs/.latest-doc-only-change.txt | 2 +- providers/openlineage/README.rst | 6 +-- providers/openlineage/docs/changelog.rst | 11 +++++ providers/openlineage/docs/index.rst | 6 +-- providers/openlineage/provider.yaml | 3 +- providers/openlineage/pyproject.toml | 6 +-- .../airflow/providers/openlineage/__init__.py | 2 +- .../opsgenie/docs/.latest-doc-only-change.txt | 2 +- providers/oracle/README.rst | 6 +-- providers/oracle/docs/changelog.rst | 11 +++++ providers/oracle/docs/index.rst | 6 +-- providers/oracle/provider.yaml | 3 +- providers/oracle/pyproject.toml | 6 +-- .../src/airflow/providers/oracle/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- .../pinecone/docs/.latest-doc-only-change.txt | 2 +- providers/postgres/README.rst | 6 +-- providers/postgres/docs/changelog.rst | 11 +++++ providers/postgres/docs/index.rst | 6 +-- providers/postgres/provider.yaml | 3 +- providers/postgres/pyproject.toml | 6 +-- .../airflow/providers/postgres/__init__.py | 2 +- .../qdrant/docs/.latest-doc-only-change.txt | 2 +- .../redis/docs/.latest-doc-only-change.txt | 2 +- providers/salesforce/README.rst | 6 +-- providers/salesforce/docs/changelog.rst | 13 ++++++ providers/salesforce/docs/index.rst | 6 +-- providers/salesforce/provider.yaml | 3 +- providers/salesforce/pyproject.toml | 6 +-- .../airflow/providers/salesforce/__init__.py | 2 +- providers/samba/README.rst | 6 +-- providers/samba/docs/changelog.rst | 13 ++++++ providers/samba/docs/index.rst | 6 +-- providers/samba/provider.yaml | 3 +- providers/samba/pyproject.toml | 6 +-- .../src/airflow/providers/samba/__init__.py | 2 +- .../segment/docs/.latest-doc-only-change.txt | 2 +- .../sendgrid/docs/.latest-doc-only-change.txt | 2 +- providers/sftp/README.rst | 8 ++-- providers/sftp/docs/changelog.rst | 14 ++++++ providers/sftp/docs/index.rst | 6 +-- providers/sftp/provider.yaml | 3 +- providers/sftp/pyproject.toml | 6 +-- .../src/airflow/providers/sftp/__init__.py | 2 +- .../docs/.latest-doc-only-change.txt | 2 +- providers/slack/README.rst | 6 +-- providers/slack/docs/changelog.rst | 11 +++++ providers/slack/docs/index.rst | 6 +-- providers/slack/provider.yaml | 3 +- providers/slack/pyproject.toml | 6 +-- .../src/airflow/providers/slack/__init__.py | 2 +- providers/smtp/README.rst | 6 +-- providers/smtp/docs/changelog.rst | 15 +++++++ providers/smtp/docs/index.rst | 6 +-- providers/smtp/provider.yaml | 3 +- providers/smtp/pyproject.toml | 6 +-- .../src/airflow/providers/smtp/__init__.py | 2 +- providers/snowflake/README.rst | 6 +-- providers/snowflake/docs/changelog.rst | 17 +++++++ providers/snowflake/docs/index.rst | 6 +-- providers/snowflake/provider.yaml | 3 +- providers/snowflake/pyproject.toml | 6 +-- .../airflow/providers/snowflake/__init__.py | 2 +- providers/ssh/README.rst | 8 ++-- providers/ssh/docs/changelog.rst | 14 ++++++ providers/ssh/docs/index.rst | 6 +-- providers/ssh/provider.yaml | 3 +- providers/ssh/pyproject.toml | 6 +-- .../ssh/src/airflow/providers/ssh/__init__.py | 2 +- providers/standard/README.rst | 8 ++-- providers/standard/docs/changelog.rst | 18 ++++++++ providers/standard/docs/index.rst | 8 ++-- providers/standard/provider.yaml | 3 +- providers/standard/pyproject.toml | 8 ++-- .../airflow/providers/standard/__init__.py | 2 +- .../tableau/docs/.latest-doc-only-change.txt | 2 +- .../telegram/docs/.latest-doc-only-change.txt | 2 +- .../weaviate/docs/.latest-doc-only-change.txt | 2 +- .../zendesk/docs/.latest-doc-only-change.txt | 2 +- 250 files changed, 1094 insertions(+), 492 deletions(-) diff --git a/providers/.last_release_date.txt b/providers/.last_release_date.txt index 642b2c4f033ba..f05b3f46dbc26 100644 --- a/providers/.last_release_date.txt +++ b/providers/.last_release_date.txt @@ -1 +1 @@ -2026-03-03 +2026-03-09 diff --git a/providers/airbyte/README.rst b/providers/airbyte/README.rst index 35f6b13a96c7c..86494264f4493 100644 --- a/providers/airbyte/README.rst +++ b/providers/airbyte/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-airbyte`` -Release: ``5.3.2`` +Release: ``5.3.3`` `Airbyte `__ @@ -36,7 +36,7 @@ This is a provider package for ``airbyte`` provider. All classes for this provid are in ``airflow.providers.airbyte`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.10.1`` +``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``airbyte-api`` ``>=0.52.0`` ``requests`` ``>=2.32.0`` ========================================== ================== @@ -79,4 +79,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/airbyte/docs/changelog.rst b/providers/airbyte/docs/changelog.rst index d5bf18e2e8815..67a246a7b9e94 100644 --- a/providers/airbyte/docs/changelog.rst +++ b/providers/airbyte/docs/changelog.rst @@ -27,6 +27,20 @@ Changelog --------- +5.3.3 +..... + +Misc +~~~~ + +* ``Migrate-airbyte-connection-UI-metadata-to-YAML (#62426)`` + + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + 5.3.2 ..... diff --git a/providers/airbyte/docs/index.rst b/providers/airbyte/docs/index.rst index f5673bfc9389e..a7316d3e2553f 100644 --- a/providers/airbyte/docs/index.rst +++ b/providers/airbyte/docs/index.rst @@ -76,7 +76,7 @@ apache-airflow-providers-airbyte package `Airbyte `__ -Release: 5.3.2 +Release: 5.3.3 Provider package ---------------- @@ -130,5 +130,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-airbyte 5.3.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-airbyte 5.3.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-airbyte 5.3.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-airbyte 5.3.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/airbyte/provider.yaml b/providers/airbyte/provider.yaml index 23a771548d547..b60aa43473fb0 100644 --- a/providers/airbyte/provider.yaml +++ b/providers/airbyte/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768333828 +source-date-epoch: 1773069973 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.3.3 - 5.3.2 - 5.3.1 - 5.3.0 diff --git a/providers/airbyte/pyproject.toml b/providers/airbyte/pyproject.toml index a29c777b40432..073cec5b5c2f9 100644 --- a/providers/airbyte/pyproject.toml +++ b/providers/airbyte/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-airbyte" -version = "5.3.2" +version = "5.3.3" description = "Provider package apache-airflow-providers-airbyte for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -99,8 +99,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-airbyte/5.3.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/airbyte/src/airflow/providers/airbyte/__init__.py b/providers/airbyte/src/airflow/providers/airbyte/__init__.py index dd3fea4574f00..88a79e907c44d 100644 --- a/providers/airbyte/src/airflow/providers/airbyte/__init__.py +++ b/providers/airbyte/src/airflow/providers/airbyte/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.3.2" +__version__ = "5.3.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/alibaba/README.rst b/providers/alibaba/README.rst index 341c166449a2b..9ad9742932708 100644 --- a/providers/alibaba/README.rst +++ b/providers/alibaba/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-alibaba`` -Release: ``3.3.4`` +Release: ``3.3.5`` Alibaba Cloud integration (including `Alibaba Cloud `__). @@ -36,7 +36,7 @@ This is a provider package for ``alibaba`` provider. All classes for this provid are in ``airflow.providers.alibaba`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -81,4 +81,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/alibaba/docs/changelog.rst b/providers/alibaba/docs/changelog.rst index 6e48150a7a514..90bfacab0d60d 100644 --- a/providers/alibaba/docs/changelog.rst +++ b/providers/alibaba/docs/changelog.rst @@ -26,6 +26,19 @@ Changelog --------- +3.3.5 +..... + +Misc +~~~~ + +* ``Migrate alibaba connection UI metadata to YAML (#62379)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 3.3.4 ..... diff --git a/providers/alibaba/docs/index.rst b/providers/alibaba/docs/index.rst index a200c77ac9027..70206ebb13807 100644 --- a/providers/alibaba/docs/index.rst +++ b/providers/alibaba/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-alibaba package Alibaba Cloud integration (including `Alibaba Cloud `__). -Release: 3.3.4 +Release: 3.3.5 Provider package ---------------- @@ -133,5 +133,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-alibaba 3.3.4 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-alibaba 3.3.4 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-alibaba 3.3.5 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-alibaba 3.3.5 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/alibaba/provider.yaml b/providers/alibaba/provider.yaml index 82fcbbd55a192..e02be8f9a676f 100644 --- a/providers/alibaba/provider.yaml +++ b/providers/alibaba/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769460342 +source-date-epoch: 1773070020 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.3.5 - 3.3.4 - 3.3.3 - 3.3.2 diff --git a/providers/alibaba/pyproject.toml b/providers/alibaba/pyproject.toml index 85adae95b83c8..dfb918523f45c 100644 --- a/providers/alibaba/pyproject.toml +++ b/providers/alibaba/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-alibaba" -version = "3.3.4" +version = "3.3.5" description = "Provider package apache-airflow-providers-alibaba for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -101,8 +101,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.4" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.4/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.5" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-alibaba/3.3.5/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/alibaba/src/airflow/providers/alibaba/__init__.py b/providers/alibaba/src/airflow/providers/alibaba/__init__.py index 16a862c1e9f9d..78f6afb499648 100644 --- a/providers/alibaba/src/airflow/providers/alibaba/__init__.py +++ b/providers/alibaba/src/airflow/providers/alibaba/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.3.4" +__version__ = "3.3.5" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/amazon/README.rst b/providers/amazon/README.rst index 65d2c3a88ee82..6fd0afb4e735b 100644 --- a/providers/amazon/README.rst +++ b/providers/amazon/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-amazon`` -Release: ``9.22.0`` +Release: ``9.23.0`` Amazon integration (including `Amazon Web Services (AWS) `__). @@ -36,7 +36,7 @@ This is a provider package for ``amazon`` provider. All classes for this provide are in ``airflow.providers.amazon`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -133,4 +133,4 @@ Extra Dependencies ==================== ======================================================================================================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/amazon/docs/changelog.rst b/providers/amazon/docs/changelog.rst index 4b559496f861b..884e600680697 100644 --- a/providers/amazon/docs/changelog.rst +++ b/providers/amazon/docs/changelog.rst @@ -26,6 +26,33 @@ Changelog --------- +9.23.0 +...... + +Features +~~~~~~~~ + +* ``Add 'SesEmailOperator' (#58312)`` +* ``Adding sftp_remote_host to S3 transfer Operators (#63147)`` + +Bug Fixes +~~~~~~~~~ + +* ``Fix CloudwatchTaskHandler not deleting local logs after streaming (#62985)`` +* ``Fix invalid RequestPayer usage in S3Hook.select_key() method (#63148)`` +* ``S3GetBucketTaggingOperator ignoring aws_conn_id parameter (#63137)`` +* ``Scope session token in cookie to base_url (#62771)`` +* ``S3DagBundle does not delete stale dag recursively (#63104)`` + +Misc +~~~~ + +* ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` +* ``Clarify to avoid bumping min version for sagemaker-studio (#62891)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 9.22.0 ...... diff --git a/providers/amazon/docs/index.rst b/providers/amazon/docs/index.rst index 8a05dcf951dd1..a8f73b3e58bda 100644 --- a/providers/amazon/docs/index.rst +++ b/providers/amazon/docs/index.rst @@ -87,7 +87,7 @@ apache-airflow-providers-amazon package Amazon integration (including `Amazon Web Services (AWS) `__). -Release: 9.22.0 +Release: 9.23.0 Provider package ---------------- @@ -168,5 +168,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-amazon 9.22.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-amazon 9.22.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-amazon 9.23.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-amazon 9.23.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml index dee23e4dd86ab..13903584c2c8f 100644 --- a/providers/amazon/provider.yaml +++ b/providers/amazon/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1770750957 +source-date-epoch: 1773070067 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 9.23.0 - 9.22.0 - 9.21.0 - 9.20.0 diff --git a/providers/amazon/pyproject.toml b/providers/amazon/pyproject.toml index 723be6c77d27a..20a9e3c46a406 100644 --- a/providers/amazon/pyproject.toml +++ b/providers/amazon/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-amazon" -version = "9.22.0" +version = "9.23.0" description = "Provider package apache-airflow-providers-amazon for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -218,8 +218,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.22.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.22.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.23.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-amazon/9.23.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/amazon/src/airflow/providers/amazon/__init__.py b/providers/amazon/src/airflow/providers/amazon/__init__.py index 899a09b548ed3..4bc0477d911a0 100644 --- a/providers/amazon/src/airflow/providers/amazon/__init__.py +++ b/providers/amazon/src/airflow/providers/amazon/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "9.22.0" +__version__ = "9.23.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/cassandra/docs/.latest-doc-only-change.txt b/providers/apache/cassandra/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/cassandra/docs/.latest-doc-only-change.txt +++ b/providers/apache/cassandra/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/flink/README.rst b/providers/apache/flink/README.rst index 93487aec60754..a8cba1f034954 100644 --- a/providers/apache/flink/README.rst +++ b/providers/apache/flink/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-flink`` -Release: ``1.8.2`` +Release: ``1.8.3`` `Apache Flink `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.flink`` provider. All classes for this p are in ``airflow.providers.apache.flink`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -50,14 +50,14 @@ The package supports the following python versions: 3.10,3.11,3.12,3.13 Requirements ------------ -============================================ ==================== +============================================ ================== PIP package Version required -============================================ ==================== +============================================ ================== ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-common-compat`` ``>=1.10.1`` -``cryptography`` ``>=41.0.0,<46.0.0`` +``cryptography`` ``>=44.0.3`` ``apache-airflow-providers-cncf-kubernetes`` ``>=5.1.0`` -============================================ ==================== +============================================ ================== Cross provider package dependencies ----------------------------------- @@ -80,4 +80,4 @@ Dependent package ====================================================================================================================== =================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/flink/docs/changelog.rst b/providers/apache/flink/docs/changelog.rst index da6f44d88e546..896f05b4099fd 100644 --- a/providers/apache/flink/docs/changelog.rst +++ b/providers/apache/flink/docs/changelog.rst @@ -26,6 +26,19 @@ Changelog --------- +1.8.3 +..... + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.8.2 ..... diff --git a/providers/apache/flink/docs/index.rst b/providers/apache/flink/docs/index.rst index 6424236d27ee0..183cc74012b79 100644 --- a/providers/apache/flink/docs/index.rst +++ b/providers/apache/flink/docs/index.rst @@ -68,7 +68,7 @@ apache-airflow-providers-apache-flink package `Apache Flink `__ -Release: 1.8.2 +Release: 1.8.3 Provider package ---------------- @@ -123,5 +123,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-flink 1.8.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-flink 1.8.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-flink 1.8.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-flink 1.8.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/flink/provider.yaml b/providers/apache/flink/provider.yaml index b49699e31c552..e86caff756023 100644 --- a/providers/apache/flink/provider.yaml +++ b/providers/apache/flink/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768334173 +source-date-epoch: 1773070115 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.8.3 - 1.8.2 - 1.8.1 - 1.8.0 diff --git a/providers/apache/flink/pyproject.toml b/providers/apache/flink/pyproject.toml index 0fdd1be5ab76d..f625f12bf5513 100644 --- a/providers/apache/flink/pyproject.toml +++ b/providers/apache/flink/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-flink" -version = "1.8.2" +version = "1.8.3" description = "Provider package apache-airflow-providers-apache-flink for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -104,8 +104,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-flink/1.8.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py b/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py index ab97fcf2e7e57..83a69242bbdc5 100644 --- a/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py +++ b/providers/apache/flink/src/airflow/providers/apache/flink/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.8.2" +__version__ = "1.8.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/hdfs/docs/.latest-doc-only-change.txt b/providers/apache/hdfs/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/hdfs/docs/.latest-doc-only-change.txt +++ b/providers/apache/hdfs/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/hive/README.rst b/providers/apache/hive/README.rst index b6543cf3881ce..3581efb0c2d92 100644 --- a/providers/apache/hive/README.rst +++ b/providers/apache/hive/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-hive`` -Release: ``9.3.0`` +Release: ``9.4.0`` `Apache Hive `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.hive`` provider. All classes for this pr are in ``airflow.providers.apache.hive`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -106,4 +106,4 @@ Extra Dependencies =================== ============================================================================================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/hive/docs/changelog.rst b/providers/apache/hive/docs/changelog.rst index de8369e579e8f..0d89cfc632f96 100644 --- a/providers/apache/hive/docs/changelog.rst +++ b/providers/apache/hive/docs/changelog.rst @@ -27,6 +27,23 @@ Changelog --------- +9.4.0 +..... + +Features +~~~~~~~~ + +* ``Make hive cli 'zooKeeperNamespace' and 'ssl' parameters configurable (#63193)`` + +Misc +~~~~ + +* ``Made sqlalchemy dependency optional for hive (#62329)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``change to owner airflow in example dag (#62957)`` + 9.3.0 ..... diff --git a/providers/apache/hive/docs/index.rst b/providers/apache/hive/docs/index.rst index 75a1c44249126..c22f505c54aef 100644 --- a/providers/apache/hive/docs/index.rst +++ b/providers/apache/hive/docs/index.rst @@ -79,7 +79,7 @@ apache-airflow-providers-apache-hive package `Apache Hive `__ -Release: 9.3.0 +Release: 9.4.0 Provider package ---------------- @@ -144,5 +144,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-hive 9.3.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-hive 9.3.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-hive 9.4.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-hive 9.4.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/hive/provider.yaml b/providers/apache/hive/provider.yaml index d650df5b4633f..cae016e6718e6 100644 --- a/providers/apache/hive/provider.yaml +++ b/providers/apache/hive/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064024 +source-date-epoch: 1773070139 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 9.4.0 - 9.3.0 - 9.2.5 - 9.2.4 diff --git a/providers/apache/hive/pyproject.toml b/providers/apache/hive/pyproject.toml index 96eb44061fd8a..d6aa77652c821 100644 --- a/providers/apache/hive/pyproject.toml +++ b/providers/apache/hive/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-hive" -version = "9.3.0" +version = "9.4.0" description = "Provider package apache-airflow-providers-apache-hive for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -142,8 +142,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.3.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.3.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.4.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-hive/9.4.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py b/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py index dff1b23e65016..2c53bc5cd8794 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "9.3.0" +__version__ = "9.4.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/iceberg/README.rst b/providers/apache/iceberg/README.rst index 9cb28ca56f445..8cd81148ea635 100644 --- a/providers/apache/iceberg/README.rst +++ b/providers/apache/iceberg/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-iceberg`` -Release: ``1.4.1`` +Release: ``2.0.0`` `Iceberg `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.iceberg`` provider. All classes for this are in ``airflow.providers.apache.iceberg`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -50,11 +50,13 @@ The package supports the following python versions: 3.10,3.11,3.12,3.13 Requirements ------------ -================== ================== -PIP package Version required -================== ================== -``apache-airflow`` ``>=2.11.0`` -================== ================== +========================================== ================== +PIP package Version required +========================================== ================== +``apache-airflow`` ``>=2.11.0`` +``apache-airflow-providers-common-compat`` ``>=1.8.0`` +``pyiceberg`` ``>=0.8.0`` +========================================== ================== Cross provider package dependencies ----------------------------------- @@ -75,14 +77,5 @@ Dependent package `apache-airflow-providers-common-compat `_ ``common.compat`` ================================================================================================================== ================= -Optional dependencies ----------------------- - -================= ========================================== -Extra Dependencies -================= ========================================== -``common.compat`` ``apache-airflow-providers-common-compat`` -================= ========================================== - The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/iceberg/docs/changelog.rst b/providers/apache/iceberg/docs/changelog.rst index b9622355bea20..dcc943cbc19af 100644 --- a/providers/apache/iceberg/docs/changelog.rst +++ b/providers/apache/iceberg/docs/changelog.rst @@ -26,6 +26,24 @@ Changelog --------- +2.0.0 +..... + +Breaking changes +~~~~~~~~~~~~~~~~ + +* ``Add catalog introspection to IcebergHook using pyiceberg (#62634)`` + +Features +~~~~~~~~ + +* ``Add Iceberg support to AnalyticsOperator (#62754)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.4.1 ..... diff --git a/providers/apache/impala/docs/.latest-doc-only-change.txt b/providers/apache/impala/docs/.latest-doc-only-change.txt index 3f35346f79b81..2c1ab461a9c8e 100644 --- a/providers/apache/impala/docs/.latest-doc-only-change.txt +++ b/providers/apache/impala/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -134348e1895ad54cfa4d3a75a78bafe872328b11 +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/apache/kafka/README.rst b/providers/apache/kafka/README.rst index 1e9f1ba715024..82b9ff86b2649 100644 --- a/providers/apache/kafka/README.rst +++ b/providers/apache/kafka/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-kafka`` -Release: ``1.12.0`` +Release: ``1.13.0`` `Apache Kafka `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.kafka`` provider. All classes for this p are in ``airflow.providers.apache.kafka`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -91,4 +91,4 @@ Extra Dependencies ==================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/kafka/docs/changelog.rst b/providers/apache/kafka/docs/changelog.rst index dd4863f2f39ed..1cd00ba30103f 100644 --- a/providers/apache/kafka/docs/changelog.rst +++ b/providers/apache/kafka/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +1.13.0 +...... + +Features +~~~~~~~~ + +* ``Add commit_offset option to AwaitMessageSensor and AwaitMessageTrigger (#62916)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.12.0 ...... diff --git a/providers/apache/kafka/docs/index.rst b/providers/apache/kafka/docs/index.rst index abd3d4f427266..57cecb4501252 100644 --- a/providers/apache/kafka/docs/index.rst +++ b/providers/apache/kafka/docs/index.rst @@ -83,7 +83,7 @@ apache-airflow-providers-apache-kafka package `Apache Kafka `__ -Release: 1.12.0 +Release: 1.13.0 Provider package ---------------- @@ -139,5 +139,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-kafka 1.12.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-kafka 1.12.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-kafka 1.13.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-kafka 1.13.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/kafka/provider.yaml b/providers/apache/kafka/provider.yaml index 2006d3c7a2d55..046c2871235e1 100644 --- a/providers/apache/kafka/provider.yaml +++ b/providers/apache/kafka/provider.yaml @@ -21,7 +21,7 @@ name: Apache Kafka state: ready lifecycle: production -source-date-epoch: 1770751158 +source-date-epoch: 1773070274 description: | `Apache Kafka `__ # Note that those versions are maintained by release manager - do not update them manually @@ -29,6 +29,7 @@ description: | # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.13.0 - 1.12.0 - 1.11.3 - 1.11.2 diff --git a/providers/apache/kafka/pyproject.toml b/providers/apache/kafka/pyproject.toml index 5b48d76b32d1b..20fd34ac1c4bf 100644 --- a/providers/apache/kafka/pyproject.toml +++ b/providers/apache/kafka/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-kafka" -version = "1.12.0" +version = "1.13.0" description = "Provider package apache-airflow-providers-apache-kafka for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -112,8 +112,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.12.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.12.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.13.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-kafka/1.13.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py b/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py index fb711ffcb5cd9..710939c37b643 100644 --- a/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py +++ b/providers/apache/kafka/src/airflow/providers/apache/kafka/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.12.0" +__version__ = "1.13.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/kylin/docs/.latest-doc-only-change.txt b/providers/apache/kylin/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/kylin/docs/.latest-doc-only-change.txt +++ b/providers/apache/kylin/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/pig/docs/.latest-doc-only-change.txt b/providers/apache/pig/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apache/pig/docs/.latest-doc-only-change.txt +++ b/providers/apache/pig/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/apache/spark/README.rst b/providers/apache/spark/README.rst index 80c8924c80d1e..b3b998df4e55b 100644 --- a/providers/apache/spark/README.rst +++ b/providers/apache/spark/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-apache-spark`` -Release: ``5.5.1`` +Release: ``5.6.0`` `Apache Spark `__ @@ -36,7 +36,7 @@ This is a provider package for ``apache.spark`` provider. All classes for this p are in ``airflow.providers.apache.spark`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -90,4 +90,4 @@ Extra Dependencies =================== =================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/apache/spark/docs/changelog.rst b/providers/apache/spark/docs/changelog.rst index aaede54e58595..0078f932415af 100644 --- a/providers/apache/spark/docs/changelog.rst +++ b/providers/apache/spark/docs/changelog.rst @@ -29,6 +29,17 @@ Changelog --------- +5.6.0 +..... + +Features +~~~~~~~~ + +* ``spark-pipelines operator (#61681)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 5.5.1 ..... diff --git a/providers/apache/spark/docs/index.rst b/providers/apache/spark/docs/index.rst index e097cbd3596a0..23a7c1ef407c8 100644 --- a/providers/apache/spark/docs/index.rst +++ b/providers/apache/spark/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-apache-spark package `Apache Spark `__ -Release: 5.5.1 +Release: 5.6.0 Provider package ---------------- @@ -132,5 +132,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-apache-spark 5.5.1 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-apache-spark 5.5.1 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-spark 5.6.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-apache-spark 5.6.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/apache/spark/provider.yaml b/providers/apache/spark/provider.yaml index 376cec1068380..33b212f73be33 100644 --- a/providers/apache/spark/provider.yaml +++ b/providers/apache/spark/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064082 +source-date-epoch: 1773070302 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.6.0 - 5.5.1 - 5.5.0 - 5.4.2 diff --git a/providers/apache/spark/pyproject.toml b/providers/apache/spark/pyproject.toml index 7412aa3a1b8fd..be89b0599746a 100644 --- a/providers/apache/spark/pyproject.toml +++ b/providers/apache/spark/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-apache-spark" -version = "5.5.1" +version = "5.6.0" description = "Provider package apache-airflow-providers-apache-spark for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -111,8 +111,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.5.1" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.5.1/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.6.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/5.6.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py b/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py index 1e2d5a44f64d6..7aec790b255e4 100644 --- a/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py +++ b/providers/apache/spark/src/airflow/providers/apache/spark/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.5.1" +__version__ = "5.6.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt b/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt +++ b/providers/apache/tinkerpop/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/apprise/docs/.latest-doc-only-change.txt b/providers/apprise/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/apprise/docs/.latest-doc-only-change.txt +++ b/providers/apprise/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/arangodb/docs/.latest-doc-only-change.txt b/providers/arangodb/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/arangodb/docs/.latest-doc-only-change.txt +++ b/providers/arangodb/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/asana/docs/.latest-doc-only-change.txt b/providers/asana/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/asana/docs/.latest-doc-only-change.txt +++ b/providers/asana/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/atlassian/jira/docs/.latest-doc-only-change.txt b/providers/atlassian/jira/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/atlassian/jira/docs/.latest-doc-only-change.txt +++ b/providers/atlassian/jira/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/celery/README.rst b/providers/celery/README.rst index a1af4e6a3dbf6..1b89f2b97d0e9 100644 --- a/providers/celery/README.rst +++ b/providers/celery/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-celery`` -Release: ``3.17.0`` +Release: ``3.17.1`` `Celery `__ @@ -36,7 +36,7 @@ This is a provider package for ``celery`` provider. All classes for this provide are in ``airflow.providers.celery`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -89,4 +89,4 @@ Extra Dependencies =================== =================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/celery/docs/changelog.rst b/providers/celery/docs/changelog.rst index 1c2db002068e5..aa094671b328f 100644 --- a/providers/celery/docs/changelog.rst +++ b/providers/celery/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +3.17.1 +...... + +Bug Fixes +~~~~~~~~~ + +* ``Ensure Celery tasks are registered at worker startup (main) (#63110)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 3.17.0 ...... diff --git a/providers/celery/docs/index.rst b/providers/celery/docs/index.rst index 3baa116abcf0c..d1659198a93f5 100644 --- a/providers/celery/docs/index.rst +++ b/providers/celery/docs/index.rst @@ -67,7 +67,7 @@ apache-airflow-providers-celery package `Celery `__ -Release: 3.17.0 +Release: 3.17.1 Provider package ---------------- @@ -122,5 +122,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-celery 3.17.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-celery 3.17.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-celery 3.17.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-celery 3.17.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/celery/provider.yaml b/providers/celery/provider.yaml index 71f9c663cca83..448d0fc369088 100644 --- a/providers/celery/provider.yaml +++ b/providers/celery/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064141 +source-date-epoch: 1773070341 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.17.1 - 3.17.0 - 3.16.0 - 3.15.2 diff --git a/providers/celery/pyproject.toml b/providers/celery/pyproject.toml index 7c9fed71892ba..7fdb1b219548a 100644 --- a/providers/celery/pyproject.toml +++ b/providers/celery/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-celery" -version = "3.17.0" +version = "3.17.1" description = "Provider package apache-airflow-providers-celery for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -111,8 +111,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-celery/3.17.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/celery/src/airflow/providers/celery/__init__.py b/providers/celery/src/airflow/providers/celery/__init__.py index 8060c68950004..5cdd33e57d956 100644 --- a/providers/celery/src/airflow/providers/celery/__init__.py +++ b/providers/celery/src/airflow/providers/celery/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.17.0" +__version__ = "3.17.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/cloudant/docs/.latest-doc-only-change.txt b/providers/cloudant/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/cloudant/docs/.latest-doc-only-change.txt +++ b/providers/cloudant/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/cncf/kubernetes/README.rst b/providers/cncf/kubernetes/README.rst index 1176aa2381df8..b58487246e738 100644 --- a/providers/cncf/kubernetes/README.rst +++ b/providers/cncf/kubernetes/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-cncf-kubernetes`` -Release: ``10.13.0`` +Release: ``10.14.0`` `Kubernetes `__ @@ -36,7 +36,7 @@ This is a provider package for ``cncf.kubernetes`` provider. All classes for thi are in ``airflow.providers.cncf.kubernetes`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -55,9 +55,9 @@ PIP package Version required ========================================== ==================== ``aiofiles`` ``>=23.2.0`` ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``asgiref`` ``>=3.5.2`` -``cryptography`` ``>=41.0.0,<46.0.0`` +``cryptography`` ``>=44.0.3`` ``kubernetes`` ``>=35.0.0,<36.0.0`` ``urllib3`` ``>=2.1.0,!=2.6.0`` ``kubernetes_asyncio`` ``>=32.0.0,<35.0.0`` @@ -83,4 +83,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/cncf/kubernetes/docs/changelog.rst b/providers/cncf/kubernetes/docs/changelog.rst index c9dcbd4169fe9..03353a1915577 100644 --- a/providers/cncf/kubernetes/docs/changelog.rst +++ b/providers/cncf/kubernetes/docs/changelog.rst @@ -32,6 +32,29 @@ Changelog Previously this would create a job that would never complete and always fail the task. Executing a task with ``parallelism = 0`` and ``wait_until_job_complete=True`` will now raise a validation error. +10.14.0 +....... + +Features +~~~~~~~~ + +* ``Add multi-team support for KubernetesExecutor (#61798)`` +* ``Executor Synchronous callback workload (#61153)`` + +Bug Fixes +~~~~~~~~~ + +* ``fixed an issue that caused a state mismatch (#63061)`` + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 10.13.0 ....... diff --git a/providers/cncf/kubernetes/docs/index.rst b/providers/cncf/kubernetes/docs/index.rst index 6fff7e0b93be4..0eb4071a4039c 100644 --- a/providers/cncf/kubernetes/docs/index.rst +++ b/providers/cncf/kubernetes/docs/index.rst @@ -88,7 +88,7 @@ apache-airflow-providers-cncf-kubernetes package `Kubernetes `__ -Release: 10.13.0 +Release: 10.14.0 Provider package ---------------- @@ -113,7 +113,7 @@ PIP package Version required ========================================== ==================== ``aiofiles`` ``>=23.2.0`` ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``asgiref`` ``>=3.5.2`` ``cryptography`` ``>=44.0.3`` ``kubernetes`` ``>=35.0.0,<36.0.0`` @@ -146,5 +146,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-cncf-kubernetes 10.13.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-cncf-kubernetes 10.13.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cncf-kubernetes 10.14.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cncf-kubernetes 10.14.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/cncf/kubernetes/provider.yaml b/providers/cncf/kubernetes/provider.yaml index 337c26fc982e7..4e5c48ea830d4 100644 --- a/providers/cncf/kubernetes/provider.yaml +++ b/providers/cncf/kubernetes/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064185 +source-date-epoch: 1773070394 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 10.14.0 - 10.13.0 - 10.12.4 - 10.12.3 diff --git a/providers/cncf/kubernetes/pyproject.toml b/providers/cncf/kubernetes/pyproject.toml index 5221158ec1f1e..f5a37c64be4c2 100644 --- a/providers/cncf/kubernetes/pyproject.toml +++ b/providers/cncf/kubernetes/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-cncf-kubernetes" -version = "10.13.0" +version = "10.14.0" description = "Provider package apache-airflow-providers-cncf-kubernetes for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -60,7 +60,7 @@ requires-python = ">=3.10" dependencies = [ "aiofiles>=23.2.0", "apache-airflow>=2.11.0", - "apache-airflow-providers-common-compat>=1.13.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", "asgiref>=3.5.2", # Cryptography could be upgraded to 46.0.5, but it does not have overlap with earlier versions # Of Airflow which were limited to <46.0.0 also earlier provider versions will not be compatible with newer @@ -119,8 +119,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.13.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.13.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.14.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.14.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py index 37682c4599048..5298ebcc5bf37 100644 --- a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py +++ b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "10.13.0" +__version__ = "10.14.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/cohere/README.rst b/providers/cohere/README.rst index 73f76b0c1fbaa..4d14a7355fa0d 100644 --- a/providers/cohere/README.rst +++ b/providers/cohere/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-cohere`` -Release: ``1.6.2`` +Release: ``1.6.3`` `Cohere `__ @@ -36,7 +36,7 @@ This is a provider package for ``cohere`` provider. All classes for this provide are in ``airflow.providers.cohere`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -79,4 +79,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/cohere/docs/changelog.rst b/providers/cohere/docs/changelog.rst index c9e10bdeaa1d9..c814133fa9840 100644 --- a/providers/cohere/docs/changelog.rst +++ b/providers/cohere/docs/changelog.rst @@ -20,6 +20,19 @@ Changelog --------- +1.6.3 +..... + +Misc +~~~~ + +* ``Migrate trino/cohere connection UI metadata to YAML (#62390)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 1.6.2 ..... diff --git a/providers/cohere/docs/index.rst b/providers/cohere/docs/index.rst index ef3151a4da1ff..84baeb772bb50 100644 --- a/providers/cohere/docs/index.rst +++ b/providers/cohere/docs/index.rst @@ -71,7 +71,7 @@ apache-airflow-providers-cohere package `Cohere `__ -Release: 1.6.2 +Release: 1.6.3 Provider package ---------------- @@ -125,5 +125,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-cohere 1.6.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-cohere 1.6.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cohere 1.6.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-cohere 1.6.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/cohere/provider.yaml b/providers/cohere/provider.yaml index 562b9275aa587..31a6a7827b94e 100644 --- a/providers/cohere/provider.yaml +++ b/providers/cohere/provider.yaml @@ -25,13 +25,14 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769460748 +source-date-epoch: 1773070423 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.6.3 - 1.6.2 - 1.6.1 - 1.6.0 diff --git a/providers/cohere/pyproject.toml b/providers/cohere/pyproject.toml index 30c10c38b8223..b311a5c8b4e56 100644 --- a/providers/cohere/pyproject.toml +++ b/providers/cohere/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-cohere" -version = "1.6.2" +version = "1.6.3" description = "Provider package apache-airflow-providers-cohere for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -99,8 +99,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cohere/1.6.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/cohere/src/airflow/providers/cohere/__init__.py b/providers/cohere/src/airflow/providers/cohere/__init__.py index bc3ae95d7a4be..02fb74a98e6d5 100644 --- a/providers/cohere/src/airflow/providers/cohere/__init__.py +++ b/providers/cohere/src/airflow/providers/cohere/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.6.2" +__version__ = "1.6.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/common/ai/docs/index.rst b/providers/common/ai/docs/index.rst index e94e3b13e6150..661effc430ac1 100644 --- a/providers/common/ai/docs/index.rst +++ b/providers/common/ai/docs/index.rst @@ -103,8 +103,8 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=3.0.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.1`` -``apache-airflow-providers-standard`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` +``apache-airflow-providers-standard`` ``>=1.12.1`` ``pydantic-ai-slim`` ``>=1.14.0`` ========================================== ================== diff --git a/providers/common/ai/pyproject.toml b/providers/common/ai/pyproject.toml index 19b5aa98292bc..507265c293f88 100644 --- a/providers/common/ai/pyproject.toml +++ b/providers/common/ai/pyproject.toml @@ -59,8 +59,8 @@ requires-python = ">=3.10" # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` dependencies = [ "apache-airflow>=3.0.0", - "apache-airflow-providers-common-compat>=1.13.1", # use next version - "apache-airflow-providers-standard>=1.12.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", + "apache-airflow-providers-standard>=1.12.1", "pydantic-ai-slim>=1.14.0", ] diff --git a/providers/common/compat/README.rst b/providers/common/compat/README.rst index ec9404b56f2c3..c91fcacd139dd 100644 --- a/providers/common/compat/README.rst +++ b/providers/common/compat/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-common-compat`` -Release: ``1.14.0`` +Release: ``1.14.1`` Common Compatibility Provider - providing compatibility code for previous Airflow versions @@ -36,7 +36,7 @@ This is a provider package for ``common.compat`` provider. All classes for this are in ``airflow.providers.common.compat`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -87,4 +87,4 @@ Extra Dependencies =============== ======================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/common/compat/docs/changelog.rst b/providers/common/compat/docs/changelog.rst index 421940d3263f1..7823ce6869249 100644 --- a/providers/common/compat/docs/changelog.rst +++ b/providers/common/compat/docs/changelog.rst @@ -25,6 +25,19 @@ Changelog --------- +1.14.1 +...... + +Misc +~~~~ + +* ``Consolidate 'SkipMixin' imports through 'common-compat' layer (#62776)`` +* ``Move SkipMixin and BranchMixIn to Task SDK (#62749)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 1.14.0 ...... diff --git a/providers/common/compat/docs/index.rst b/providers/common/compat/docs/index.rst index 8bd6c53af3304..a8052bf2eceb4 100644 --- a/providers/common/compat/docs/index.rst +++ b/providers/common/compat/docs/index.rst @@ -62,7 +62,7 @@ apache-airflow-providers-common-compat package Common Compatibility Provider - providing compatibility code for previous Airflow versions -Release: 1.14.0 +Release: 1.14.1 Provider package ---------------- @@ -114,5 +114,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-common-compat 1.14.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-common-compat 1.14.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-compat 1.14.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-compat 1.14.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/common/compat/provider.yaml b/providers/common/compat/provider.yaml index 746bcb603d415..fc21d3a094332 100644 --- a/providers/common/compat/provider.yaml +++ b/providers/common/compat/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064215 +source-date-epoch: 1773070447 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.14.1 - 1.14.0 - 1.13.1 - 1.13.0 diff --git a/providers/common/compat/pyproject.toml b/providers/common/compat/pyproject.toml index 46a7ffab16fb7..a4b0eaf10463c 100644 --- a/providers/common/compat/pyproject.toml +++ b/providers/common/compat/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-common-compat" -version = "1.14.0" +version = "1.14.1" description = "Provider package apache-airflow-providers-common-compat for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -107,8 +107,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-compat/1.14.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/common/compat/src/airflow/providers/common/compat/__init__.py b/providers/common/compat/src/airflow/providers/common/compat/__init__.py index 29bd8d0d75ed5..e312ca9ef3a03 100644 --- a/providers/common/compat/src/airflow/providers/common/compat/__init__.py +++ b/providers/common/compat/src/airflow/providers/common/compat/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.14.0" +__version__ = "1.14.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/common/io/docs/.latest-doc-only-change.txt b/providers/common/io/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/common/io/docs/.latest-doc-only-change.txt +++ b/providers/common/io/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/common/messaging/docs/.latest-doc-only-change.txt b/providers/common/messaging/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/common/messaging/docs/.latest-doc-only-change.txt +++ b/providers/common/messaging/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/common/sql/README.rst b/providers/common/sql/README.rst index 3f1bc0d73396e..5fe692b50d526 100644 --- a/providers/common/sql/README.rst +++ b/providers/common/sql/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-common-sql`` -Release: ``1.32.0`` +Release: ``1.33.0`` `Common SQL Provider `__ @@ -36,7 +36,7 @@ This is a provider package for ``common.sql`` provider. All classes for this pro are in ``airflow.providers.common.sql`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``sqlparse`` ``>=0.5.1`` ``more-itertools`` ``>=9.0.0`` ``methodtools`` ``>=0.4.7`` @@ -70,27 +70,33 @@ You can install such cross-provider dependencies when installing from PyPI. For .. code-block:: bash - pip install apache-airflow-providers-common-sql[common.compat] + pip install apache-airflow-providers-common-sql[amazon] -================================================================================================================== ================= -Dependent package Extra -================================================================================================================== ================= -`apache-airflow-providers-common-compat `_ ``common.compat`` -`apache-airflow-providers-openlineage `_ ``openlineage`` -================================================================================================================== ================= +==================================================================================================================== ================== +Dependent package Extra +==================================================================================================================== ================== +`apache-airflow-providers-amazon `_ ``amazon`` +`apache-airflow-providers-apache-iceberg `_ ``apache.iceberg`` +`apache-airflow-providers-common-compat `_ ``common.compat`` +`apache-airflow-providers-openlineage `_ ``openlineage`` +==================================================================================================================== ================== Optional dependencies ---------------------- -=============== ================================================================================================ -Extra Dependencies -=============== ================================================================================================ -``pandas`` ``pandas[sql-other]>=2.1.2; python_version <"3.13"``, ``pandas>=2.2.3; python_version >="3.13"`` -``openlineage`` ``apache-airflow-providers-openlineage`` -``polars`` ``polars>=1.26.0`` -``sqlalchemy`` ``sqlalchemy>=1.4.54`` -=============== ================================================================================================ +================== ================================================================================================ +Extra Dependencies +================== ================================================================================================ +``pandas`` ``pandas[sql-other]>=2.1.2; python_version <"3.13"``, ``pandas>=2.2.3; python_version >="3.13"`` +``openlineage`` ``apache-airflow-providers-openlineage`` +``polars`` ``polars>=1.26.0`` +``sqlalchemy`` ``sqlalchemy>=1.4.54`` +``amazon`` ``apache-airflow-providers-amazon`` +``datafusion`` ``datafusion>=50.0.0,<52.0.0`` +``pyiceberg-core`` ``pyiceberg-core>=0.8.0`` +``apache.iceberg`` ``apache-airflow-providers-apache-iceberg`` +================== ================================================================================================ The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/common/sql/docs/changelog.rst b/providers/common/sql/docs/changelog.rst index 590dfac85a00c..9a4c0d4867d2e 100644 --- a/providers/common/sql/docs/changelog.rst +++ b/providers/common/sql/docs/changelog.rst @@ -25,6 +25,34 @@ Changelog --------- +1.33.0 +...... + +Features +~~~~~~~~ + +* ``Add Iceberg support to AnalyticsOperator (#62754)`` +* ``Add @task.analytics Decorator (#62648)`` +* ``Add ObjectStorage support to LLMSQLQueryOperator via DataFusion (#62640)`` +* ``Add 'LLMSQLQueryOperator' and '@task.llm_sql' to common.ai provider (#62599)`` +* ``AIP-99: Add AnalyticsOperator (#62232)`` + +Bug Fixes +~~~~~~~~~ + +* ``Cache DbApiHook.inspector to avoid creating N engines (#62594)`` + +Misc +~~~~ + +* ``Consolidate 'SkipMixin' imports through 'common-compat' layer (#62776)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Fix removal of '__str__' method from Datafusion Format enums (#62830)`` + * ``Explicitly set extra for connections in generic transfer tests (#62581)`` + 1.32.0 ...... diff --git a/providers/common/sql/docs/index.rst b/providers/common/sql/docs/index.rst index d78f310a2f03c..ada01d754b4dd 100644 --- a/providers/common/sql/docs/index.rst +++ b/providers/common/sql/docs/index.rst @@ -79,7 +79,7 @@ apache-airflow-providers-common-sql package `Common SQL Provider `__ -Release: 1.32.0 +Release: 1.33.0 Provider package ---------------- @@ -103,7 +103,7 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ``sqlparse`` ``>=0.5.1`` ``more-itertools`` ``>=9.0.0`` ``methodtools`` ``>=0.4.7`` @@ -137,5 +137,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-common-sql 1.32.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-common-sql 1.32.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-sql 1.33.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-common-sql 1.33.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/common/sql/provider.yaml b/providers/common/sql/provider.yaml index 8afdd4bfc5830..2cf0b2f294b1a 100644 --- a/providers/common/sql/provider.yaml +++ b/providers/common/sql/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1771577228 +source-date-epoch: 1773070489 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.33.0 - 1.32.0 - 1.31.0 - 1.30.4 diff --git a/providers/common/sql/pyproject.toml b/providers/common/sql/pyproject.toml index 7780a7fe94598..a08c1ac3fd2db 100644 --- a/providers/common/sql/pyproject.toml +++ b/providers/common/sql/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-common-sql" -version = "1.32.0" +version = "1.33.0" description = "Provider package apache-airflow-providers-common-sql for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -59,7 +59,7 @@ requires-python = ">=3.10" # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` dependencies = [ "apache-airflow>=2.11.0", - "apache-airflow-providers-common-compat>=1.12.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", "sqlparse>=0.5.1", "more-itertools>=9.0.0", # The methodtools dependency is necessary since the introduction of dialects: @@ -146,8 +146,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.32.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.32.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.33.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/1.33.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/common/sql/src/airflow/providers/common/sql/__init__.py b/providers/common/sql/src/airflow/providers/common/sql/__init__.py index ba4f11945fdb2..3d5342ad3cd3c 100644 --- a/providers/common/sql/src/airflow/providers/common/sql/__init__.py +++ b/providers/common/sql/src/airflow/providers/common/sql/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.32.0" +__version__ = "1.33.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/databricks/README.rst b/providers/databricks/README.rst index d424c1be9fd23..ca640c098a6a1 100644 --- a/providers/databricks/README.rst +++ b/providers/databricks/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-databricks`` -Release: ``7.10.0`` +Release: ``7.11.0`` `Databricks `__ @@ -36,7 +36,7 @@ This is a provider package for ``databricks`` provider. All classes for this pro are in ``airflow.providers.databricks`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -105,4 +105,4 @@ Extra Dependencies ================== ================================================================ The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/databricks/docs/changelog.rst b/providers/databricks/docs/changelog.rst index b0aaa70e0ad39..5734d4a8970d2 100644 --- a/providers/databricks/docs/changelog.rst +++ b/providers/databricks/docs/changelog.rst @@ -26,6 +26,27 @@ Changelog --------- +7.11.0 +...... + +Features +~~~~~~~~ + +* ``Add validation for table_name and expression_list in DatabricksCopyIntoOperator (#62499)`` + +Bug Fixes +~~~~~~~~~ + +* ``Raise ValueError instead of KeyError when cancel_previous_runs=True and no job identifier is provided (#62393)`` + +Misc +~~~~ + +* ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 7.10.0 ...... diff --git a/providers/databricks/docs/index.rst b/providers/databricks/docs/index.rst index 2b9618bf1b70a..164b453dc7c85 100644 --- a/providers/databricks/docs/index.rst +++ b/providers/databricks/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-databricks package `Databricks `__ -Release: 7.10.0 +Release: 7.11.0 Provider package ---------------- @@ -142,5 +142,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-databricks 7.10.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-databricks 7.10.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-databricks 7.11.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-databricks 7.11.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/databricks/provider.yaml b/providers/databricks/provider.yaml index ec83c0ae1c642..41ab271bcbcd1 100644 --- a/providers/databricks/provider.yaml +++ b/providers/databricks/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064288 +source-date-epoch: 1773070523 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 7.11.0 - 7.10.0 - 7.9.1 - 7.9.0 diff --git a/providers/databricks/pyproject.toml b/providers/databricks/pyproject.toml index 756384686f210..a755ba6b5803e 100644 --- a/providers/databricks/pyproject.toml +++ b/providers/databricks/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-databricks" -version = "7.10.0" +version = "7.11.0" description = "Provider package apache-airflow-providers-databricks for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -146,8 +146,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.10.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.10.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.11.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-databricks/7.11.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/databricks/src/airflow/providers/databricks/__init__.py b/providers/databricks/src/airflow/providers/databricks/__init__.py index 9a2831d24eac3..856c275706dda 100644 --- a/providers/databricks/src/airflow/providers/databricks/__init__.py +++ b/providers/databricks/src/airflow/providers/databricks/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "7.10.0" +__version__ = "7.11.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/datadog/docs/.latest-doc-only-change.txt b/providers/datadog/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/datadog/docs/.latest-doc-only-change.txt +++ b/providers/datadog/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/dbt/cloud/README.rst b/providers/dbt/cloud/README.rst index a31b4ec52d6c3..631b7768770ca 100644 --- a/providers/dbt/cloud/README.rst +++ b/providers/dbt/cloud/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-dbt-cloud`` -Release: ``4.6.5`` +Release: ``4.7.0`` `dbt Cloud `__ @@ -36,7 +36,7 @@ This is a provider package for ``dbt.cloud`` provider. All classes for this prov are in ``airflow.providers.dbt.cloud`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -92,4 +92,4 @@ Extra Dependencies =============== =============================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/dbt/cloud/docs/changelog.rst b/providers/dbt/cloud/docs/changelog.rst index 478363753a433..a39ded73990c4 100644 --- a/providers/dbt/cloud/docs/changelog.rst +++ b/providers/dbt/cloud/docs/changelog.rst @@ -28,6 +28,22 @@ Changelog --------- +4.7.0 +..... + +Features +~~~~~~~~ + +* ``Add sync and async helpers to resolve the dbt Cloud account ID from the (#61757)`` + +Bug Fixes +~~~~~~~~~ + +* ``Raise on unexpected terminal dbt Cloud job run states (#61300)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 4.6.5 ..... diff --git a/providers/dbt/cloud/docs/index.rst b/providers/dbt/cloud/docs/index.rst index ae05429382d53..0da8131f10942 100644 --- a/providers/dbt/cloud/docs/index.rst +++ b/providers/dbt/cloud/docs/index.rst @@ -81,7 +81,7 @@ apache-airflow-providers-dbt-cloud package `dbt Cloud `__ -Release: 4.6.5 +Release: 4.7.0 Provider package ---------------- @@ -139,5 +139,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-dbt-cloud 4.6.5 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-dbt-cloud 4.6.5 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-dbt-cloud 4.7.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-dbt-cloud 4.7.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/dbt/cloud/provider.yaml b/providers/dbt/cloud/provider.yaml index 95f557b083b3d..a27c7dc1107ea 100644 --- a/providers/dbt/cloud/provider.yaml +++ b/providers/dbt/cloud/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064316 +source-date-epoch: 1773070569 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.7.0 - 4.6.5 - 4.6.4 - 4.6.3 diff --git a/providers/dbt/cloud/pyproject.toml b/providers/dbt/cloud/pyproject.toml index ed10b75468e85..78fc7a1569357 100644 --- a/providers/dbt/cloud/pyproject.toml +++ b/providers/dbt/cloud/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-dbt-cloud" -version = "4.6.5" +version = "4.7.0" description = "Provider package apache-airflow-providers-dbt-cloud for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -111,8 +111,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.6.5" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.6.5/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.7.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-dbt-cloud/4.7.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py b/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py index 66ff805704e6e..4abc0f34ccafc 100644 --- a/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py +++ b/providers/dbt/cloud/src/airflow/providers/dbt/cloud/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.6.5" +__version__ = "4.7.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/dingding/docs/.latest-doc-only-change.txt b/providers/dingding/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/dingding/docs/.latest-doc-only-change.txt +++ b/providers/dingding/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/discord/docs/.latest-doc-only-change.txt b/providers/discord/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/discord/docs/.latest-doc-only-change.txt +++ b/providers/discord/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/docker/README.rst b/providers/docker/README.rst index 130b4de4b6844..ad6e4a9b4ebe9 100644 --- a/providers/docker/README.rst +++ b/providers/docker/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-docker`` -Release: ``4.5.2`` +Release: ``4.5.3`` `Docker `__ @@ -36,7 +36,7 @@ This is a provider package for ``docker`` provider. All classes for this provide are in ``airflow.providers.docker`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,14 +78,5 @@ Dependent package `apache-airflow-providers-common-compat `_ ``common.compat`` ================================================================================================================== ================= -Optional dependencies ----------------------- - -================= ========================================== -Extra Dependencies -================= ========================================== -``common.compat`` ``apache-airflow-providers-common-compat`` -================= ========================================== - The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/docker/docs/changelog.rst b/providers/docker/docs/changelog.rst index 5ddaa9a2d3ef1..8e46ac38bcd22 100644 --- a/providers/docker/docs/changelog.rst +++ b/providers/docker/docs/changelog.rst @@ -28,6 +28,21 @@ Changelog --------- +4.5.3 +..... + +Bug Fixes +~~~~~~~~~ + +* ``Switch from surrogateescape to replace to handle utf-8 error (#62632)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 2] Migrate connection UI metadata to YAML for more providers (#62109)`` + * ``CI: Upgrade important CI environment (#61417)`` + 4.5.2 ..... diff --git a/providers/docker/docs/index.rst b/providers/docker/docs/index.rst index f0d6420fe2452..9865c8ebbc5e4 100644 --- a/providers/docker/docs/index.rst +++ b/providers/docker/docs/index.rst @@ -70,7 +70,7 @@ apache-airflow-providers-docker package `Docker `__ -Release: 4.5.2 +Release: 4.5.3 Provider package ---------------- @@ -124,5 +124,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-docker 4.5.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-docker 4.5.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-docker 4.5.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-docker 4.5.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/docker/provider.yaml b/providers/docker/provider.yaml index 6824c551cd0f8..7018ce1cdf7d9 100644 --- a/providers/docker/provider.yaml +++ b/providers/docker/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768334813 +source-date-epoch: 1773070597 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.5.3 - 4.5.2 - 4.5.1 - 4.5.0 diff --git a/providers/docker/pyproject.toml b/providers/docker/pyproject.toml index 7bd84e70afa28..187c51df8c737 100644 --- a/providers/docker/pyproject.toml +++ b/providers/docker/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-docker" -version = "4.5.2" +version = "4.5.3" description = "Provider package apache-airflow-providers-docker for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -99,8 +99,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-docker/4.5.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/docker/src/airflow/providers/docker/__init__.py b/providers/docker/src/airflow/providers/docker/__init__.py index fa8f18b48a41a..5f83b16e276a5 100644 --- a/providers/docker/src/airflow/providers/docker/__init__.py +++ b/providers/docker/src/airflow/providers/docker/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.5.2" +__version__ = "4.5.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/edge3/README.rst b/providers/edge3/README.rst index 5fd13d3e00124..9f6ff32c6364b 100644 --- a/providers/edge3/README.rst +++ b/providers/edge3/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-edge3`` -Release: ``3.1.0`` +Release: ``3.2.0`` Handle edge workers on remote sites via HTTP(s) connection and orchestrates work over distributed sites. @@ -48,7 +48,7 @@ This is a provider package for ``edge3`` provider. All classes for this provider are in ``airflow.providers.edge3`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -93,4 +93,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/edge3/docs/changelog.rst b/providers/edge3/docs/changelog.rst index f36f983745adf..db23c37380edb 100644 --- a/providers/edge3/docs/changelog.rst +++ b/providers/edge3/docs/changelog.rst @@ -27,6 +27,31 @@ Changelog --------- +3.2.0 +..... + +Features +~~~~~~~~ + +* ``Add real-time concurrency control for edge workers via UI (#63142)`` + +Bug Fixes +~~~~~~~~~ + +* ``Centralized runtime control of Edge Worker concurrency in distributed deployments (#62896)`` +* ``Fix _execution_api_server_url() reading edge.api_url when execution_api_server_url is already set (#63192)`` + +Doc-only +~~~~~~~~ + +* ``docs(edge3): add set-worker-concurrency command to deployment guide (#63083)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``chore(deps): bump the edge-ui-package-updates group across 1 directory with 6 updates (#63070)`` + * ``Upgrade 'tar' (#62939)`` + * ``Update dependencies for TS code (#62678)`` + 3.1.0 ..... @@ -255,14 +280,14 @@ Misc .. warning:: The React Plugin integration in this release is incompatible with Airflow 3.1.0 - It is recommended to use apache-airflow>=3.1.1 + It is recommended to use apache-airflow>=3.2.0 Bug Fixes ~~~~~~~~~ * ``Fix Link to Dag in Plugin (#55642)`` * ``Bugfix/support Subpath w/o Execution API Url (#57372)`` -* ``Adjust authentication token after UI changes in Airflow 3.1.1 (#57370)`` +* ``Adjust authentication token after UI changes in Airflow 3.2.0 (#57370)`` Misc ~~~~ diff --git a/providers/edge3/docs/index.rst b/providers/edge3/docs/index.rst index 18a39ad68ecaf..6f79fa617f7cc 100644 --- a/providers/edge3/docs/index.rst +++ b/providers/edge3/docs/index.rst @@ -90,7 +90,7 @@ Additional REST API endpoints are provided to distribute tasks and manage the ed are provided by the API server. -Release: 3.1.0 +Release: 3.2.0 Provider package ---------------- @@ -146,5 +146,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-edge3 3.1.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-edge3 3.1.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-edge3 3.2.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-edge3 3.2.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/edge3/provider.yaml b/providers/edge3/provider.yaml index 9f9d87cfd1865..1e5d4386cab9f 100644 --- a/providers/edge3/provider.yaml +++ b/providers/edge3/provider.yaml @@ -34,7 +34,7 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064422 +source-date-epoch: 1773070647 build-system: hatchling # Note that those versions are maintained by release manager - do not update them manually @@ -42,6 +42,7 @@ build-system: hatchling # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.2.0 - 3.1.0 - 3.0.2 - 3.0.1 diff --git a/providers/edge3/pyproject.toml b/providers/edge3/pyproject.toml index eb9b4a6e136ae..f1209231b804e 100644 --- a/providers/edge3/pyproject.toml +++ b/providers/edge3/pyproject.toml @@ -32,7 +32,7 @@ build-backend = "hatchling.build" [project] name = "apache-airflow-providers-edge3" -version = "3.1.0" +version = "3.2.0" description = "Provider package apache-airflow-providers-edge3 for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -108,8 +108,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.1.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.1.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.2.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-edge3/3.2.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/edge3/src/airflow/providers/edge3/__init__.py b/providers/edge3/src/airflow/providers/edge3/__init__.py index 534146709056d..0b8f09bb4764d 100644 --- a/providers/edge3/src/airflow/providers/edge3/__init__.py +++ b/providers/edge3/src/airflow/providers/edge3/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.1.0" +__version__ = "3.2.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "3.0.0" diff --git a/providers/fab/README.rst b/providers/fab/README.rst index 14dbbbeed71a4..83445661c5994 100644 --- a/providers/fab/README.rst +++ b/providers/fab/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-fab`` -Release: ``3.4.0`` +Release: ``3.5.0`` `Flask App Builder `__ @@ -36,7 +36,7 @@ This is a provider package for ``fab`` provider. All classes for this provider p are in ``airflow.providers.fab`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -45,14 +45,14 @@ You can install this package on top of an existing Airflow installation (see ``R for the minimum Airflow version supported) via ``pip install apache-airflow-providers-fab`` -The package supports the following python versions: 3.10,3.11,3.12 +The package supports the following python versions: 3.10,3.11,3.12,3.13 Requirements ------------ -========================================== ========================================== +========================================== ================== PIP package Version required -========================================== ========================================== +========================================== ================== ``apache-airflow`` ``>=3.0.2`` ``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``blinker`` ``>=1.6.2`` @@ -68,7 +68,7 @@ PIP package Version required ``wtforms`` ``>=3.0,<4`` ``cachetools`` ``>=6.0`` ``flask_limiter`` ``>3,!=3.13,<4`` -========================================== ========================================== +========================================== ================== Cross provider package dependencies ----------------------------------- @@ -92,11 +92,11 @@ Dependent package Optional dependencies ---------------------- -============ ============================================ +============ =================== Extra Dependencies -============ ============================================ -``kerberos`` ``kerberos>=1.3.0; python_version < '3.13'`` -============ ============================================ +============ =================== +``kerberos`` ``kerberos>=1.3.0`` +============ =================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/fab/docs/changelog.rst b/providers/fab/docs/changelog.rst index 1407236bc456e..c18c1ab6fa388 100644 --- a/providers/fab/docs/changelog.rst +++ b/providers/fab/docs/changelog.rst @@ -20,6 +20,37 @@ Changelog --------- +3.5.0 +..... + +Features +~~~~~~~~ + +* ``Replace connexion with FastAPI for FAB provider (#62664)`` + +Bug Fixes +~~~~~~~~~ + +* ``Add missing HTTP timeout to FAB JWKS fetching (#63058)`` +* ``Fix race condition in auth manager initialization (#62431)`` +* ``Fix/FabAuthManager race condition on startup with multiple workers (#62737)`` +* ``Scope session token in cookie to base_url (#62771)`` + +Misc +~~~~ + +* ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``chore(deps-dev): bump the fab-ui-package-updates group across 1 directory with 3 updates (#63067)`` + * ``chore(deps-dev): bump copy-webpack-plugin (#63004)`` + * ``Upgrade 'sgvo' (#62941)`` + * ``chore(deps-dev): bump the fab-ui-package-updates group across 1 directory with 3 updates (#62719)`` + * ``Update dependencies for TS code in Fab Provider (#62679)`` + * ``CI: Upgrade important CI environment (#62610)`` + * ``Fix all build-system/requires including transitive dependencies (#62570)`` + 3.4.0 ..... diff --git a/providers/fab/docs/index.rst b/providers/fab/docs/index.rst index bb7137d489a88..d0a4de316fa8d 100644 --- a/providers/fab/docs/index.rst +++ b/providers/fab/docs/index.rst @@ -83,7 +83,7 @@ apache-airflow-providers-fab package `Flask App Builder `__ -Release: 3.4.0 +Release: 3.5.0 Provider package ---------------- @@ -148,5 +148,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-fab 3.4.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-fab 3.4.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-fab 3.5.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-fab 3.5.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/fab/provider.yaml b/providers/fab/provider.yaml index e4dcef5520564..d1be6d2d9a051 100644 --- a/providers/fab/provider.yaml +++ b/providers/fab/provider.yaml @@ -29,7 +29,7 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064573 +source-date-epoch: 1773070724 build-system: hatchling # Note that those versions are maintained by release manager - do not update them manually @@ -37,6 +37,7 @@ build-system: hatchling # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 3.5.0 - 3.4.0 - 3.3.0 - 3.2.0 diff --git a/providers/fab/pyproject.toml b/providers/fab/pyproject.toml index 8041fa77e6350..11a4bada9c063 100644 --- a/providers/fab/pyproject.toml +++ b/providers/fab/pyproject.toml @@ -32,7 +32,7 @@ build-backend = "hatchling.build" [project] name = "apache-airflow-providers-fab" -version = "3.4.0" +version = "3.5.0" description = "Provider package apache-airflow-providers-fab for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -139,8 +139,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.4.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.4.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.5.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-fab/3.5.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/fab/src/airflow/providers/fab/__init__.py b/providers/fab/src/airflow/providers/fab/__init__.py index 7acecac38ad07..d075015de3802 100644 --- a/providers/fab/src/airflow/providers/fab/__init__.py +++ b/providers/fab/src/airflow/providers/fab/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "3.4.0" +__version__ = "3.5.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "3.0.2" diff --git a/providers/facebook/docs/.latest-doc-only-change.txt b/providers/facebook/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/facebook/docs/.latest-doc-only-change.txt +++ b/providers/facebook/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/ftp/docs/.latest-doc-only-change.txt b/providers/ftp/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/ftp/docs/.latest-doc-only-change.txt +++ b/providers/ftp/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/git/docs/.latest-doc-only-change.txt b/providers/git/docs/.latest-doc-only-change.txt index 18968b027f51a..2c1ab461a9c8e 100644 --- a/providers/git/docs/.latest-doc-only-change.txt +++ b/providers/git/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -a4a51a02db2994e4ea94b83887739dc79a4d11d9 +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/github/docs/.latest-doc-only-change.txt b/providers/github/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/github/docs/.latest-doc-only-change.txt +++ b/providers/github/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/google/README.rst b/providers/google/README.rst index cc9b9082ee093..4acfce9e2bec9 100644 --- a/providers/google/README.rst +++ b/providers/google/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-google`` -Release: ``20.0.0`` +Release: ``21.0.0`` Google services including: @@ -43,7 +43,7 @@ This is a provider package for ``google`` provider. All classes for this provide are in ``airflow.providers.google`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -177,10 +177,9 @@ Dependent package Optional dependencies ---------------------- -==================== ================================================================ +==================== ==================================================== Extra Dependencies -==================== ================================================================ -``apache.beam`` ``apache-airflow-providers-apache-beam>=6.2.2`` +==================== ==================================================== ``cncf.kubernetes`` ``apache-airflow-providers-cncf-kubernetes>=10.1.0`` ``fab`` ``apache-airflow-providers-fab>=2.0.0`` ``leveldb`` ``plyvel>=1.5.1; python_version < '3.13'`` @@ -201,7 +200,7 @@ Extra Dependencies ``http`` ``apache-airflow-providers-http`` ``standard`` ``apache-airflow-providers-standard`` ``common.messaging`` ``apache-airflow-providers-common-messaging>=2.0.0`` -==================== ================================================================ +==================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/google/docs/changelog.rst b/providers/google/docs/changelog.rst index 7257e0fbd3937..d91d76192296c 100644 --- a/providers/google/docs/changelog.rst +++ b/providers/google/docs/changelog.rst @@ -67,6 +67,48 @@ Changelog * Remove ``CloudDataCatalogHook`` use ``airflow.providers.google.cloud.hooks.dataplex.DataplexHook`` instead * Remove ``airflow.providers.google.cloud.hooks.vertex_ai.generative_model.ExperimentRunHook`` use ``airflow.providers.google.cloud.hooks.vertex_ai.experiment_service.ExperimentRunHook`` instead +21.0.0 +...... + +Breaking changes +~~~~~~~~~~~~~~~~ + +* ``Delete google provider deprecated items sheduled for Jan 2026 (#62802)`` + +Features +~~~~~~~~ + +* ``Add drift detection and optional recreation to ComputeEngineInsertInstanceOperator (#61830)`` +* ``Return destination GCS URIs from ADLSToGCSOperator (#61463)`` +* ``Return GCS URIs from GoogleAdsToGcsOperator (#61334)`` +* ``Add bounded best-effort cluster deletion when PermissionDenied occurs after cluster creation has been initiated in non-deferrable mode. Deletion is triggered with wait_to_complete=False and retried on FailedPrecondition until cleanup_timeout_seconds is reached, and the original exception is always re-raised. Add unit tests covering cleanup initiation, retry behavior, and error propagation. (#62302)`` +* ``Add ClusterType field for Zero-Scale cluster support (#62207)`` + +Bug Fixes +~~~~~~~~~ + +* ``fix DataprocSubmitTrigger deferred tasks stuck forever (#62082)`` + +Misc +~~~~ + +* ``Upgrade version of Campaign Manager API to v5 (#62510)`` +* ``Add .json template_ext to BigQueryCreateTableOperator (#62058)`` + +Doc-only +~~~~~~~~ + +* ``Add known issue notice for version 19.5.0 (#61927)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Add exception test for GenAICountTokensOperator (#61391)`` + * ``Remove dependency limitations related to FAB's py3.13 incompatibility (#62924)`` + * ``Fix mypy issues from trinodb 0.337.0 (#62998)`` + * ``Replace connexion with FastAPI for FAB provider (#62664)`` + * ``Update provider's compatibility matrix with 2.11.1 (#62295)`` + * ``Suspend Apache Beam Provider due to grpcio limitation (#61926)`` + 20.0.0 ...... diff --git a/providers/google/docs/index.rst b/providers/google/docs/index.rst index e7da96e24af38..8cc8cca4a36e4 100644 --- a/providers/google/docs/index.rst +++ b/providers/google/docs/index.rst @@ -89,7 +89,7 @@ Google services including: - `Google Workspace `__ (formerly Google Suite) -Release: 20.0.0 +Release: 21.0.0 Provider package ---------------- @@ -232,5 +232,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-google 20.0.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-google 20.0.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-google 21.0.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-google 21.0.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/google/provider.yaml b/providers/google/provider.yaml index 4f0d1d90c81c9..c12c3e80d2993 100644 --- a/providers/google/provider.yaml +++ b/providers/google/provider.yaml @@ -30,12 +30,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1770752544 +source-date-epoch: 1773070870 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 21.0.0 - 20.0.0 - 19.5.0 - 19.4.0 diff --git a/providers/google/pyproject.toml b/providers/google/pyproject.toml index 125348db2bb05..3159b4ce712e2 100644 --- a/providers/google/pyproject.toml +++ b/providers/google/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-google" -version = "20.0.0" +version = "21.0.0" description = "Provider package apache-airflow-providers-google for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -263,8 +263,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-google/20.0.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-google/20.0.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-google/21.0.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-google/21.0.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/google/src/airflow/providers/google/__init__.py b/providers/google/src/airflow/providers/google/__init__.py index a976e00da2610..b698e55c63bd7 100644 --- a/providers/google/src/airflow/providers/google/__init__.py +++ b/providers/google/src/airflow/providers/google/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "20.0.0" +__version__ = "21.0.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/grpc/docs/.latest-doc-only-change.txt b/providers/grpc/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/grpc/docs/.latest-doc-only-change.txt +++ b/providers/grpc/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/hashicorp/docs/.latest-doc-only-change.txt b/providers/hashicorp/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/hashicorp/docs/.latest-doc-only-change.txt +++ b/providers/hashicorp/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/imap/docs/.latest-doc-only-change.txt b/providers/imap/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/imap/docs/.latest-doc-only-change.txt +++ b/providers/imap/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/influxdb/docs/.latest-doc-only-change.txt b/providers/influxdb/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/influxdb/docs/.latest-doc-only-change.txt +++ b/providers/influxdb/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/jdbc/README.rst b/providers/jdbc/README.rst index 8a0640c60d00a..8862f95a76ff1 100644 --- a/providers/jdbc/README.rst +++ b/providers/jdbc/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-jdbc`` -Release: ``5.4.0`` +Release: ``5.4.1`` `Java Database Connectivity (JDBC) `__ @@ -36,7 +36,7 @@ This is a provider package for ``jdbc`` provider. All classes for this provider are in ``airflow.providers.jdbc`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -80,4 +80,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/jdbc/docs/changelog.rst b/providers/jdbc/docs/changelog.rst index 15d82afa088be..c8e981bb17251 100644 --- a/providers/jdbc/docs/changelog.rst +++ b/providers/jdbc/docs/changelog.rst @@ -26,6 +26,17 @@ Changelog --------- +5.4.1 +..... + +Misc +~~~~ + +* `` Migrate JDBC connection UI metadata to YAML (#62427)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 5.4.0 ..... diff --git a/providers/jdbc/docs/index.rst b/providers/jdbc/docs/index.rst index da78458f75bc5..85ffe5c0b1cd8 100644 --- a/providers/jdbc/docs/index.rst +++ b/providers/jdbc/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-jdbc package `Java Database Connectivity (JDBC) `__ -Release: 5.4.0 +Release: 5.4.1 Provider package ---------------- @@ -133,5 +133,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-jdbc 5.4.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-jdbc 5.4.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jdbc 5.4.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jdbc 5.4.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/jdbc/provider.yaml b/providers/jdbc/provider.yaml index 8786059249ffb..c89933f3bd860 100644 --- a/providers/jdbc/provider.yaml +++ b/providers/jdbc/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772064974 +source-date-epoch: 1773070907 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.4.1 - 5.4.0 - 5.3.2 - 5.3.1 diff --git a/providers/jdbc/pyproject.toml b/providers/jdbc/pyproject.toml index fea3e735f6616..3217d510a91f7 100644 --- a/providers/jdbc/pyproject.toml +++ b/providers/jdbc/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-jdbc" -version = "5.4.0" +version = "5.4.1" description = "Provider package apache-airflow-providers-jdbc for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -100,8 +100,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jdbc/5.4.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/jdbc/src/airflow/providers/jdbc/__init__.py b/providers/jdbc/src/airflow/providers/jdbc/__init__.py index fe42a853a0c86..3b3bce1367917 100644 --- a/providers/jdbc/src/airflow/providers/jdbc/__init__.py +++ b/providers/jdbc/src/airflow/providers/jdbc/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.4.0" +__version__ = "5.4.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/jenkins/README.rst b/providers/jenkins/README.rst index 0af8749ab9b88..07a0ec767ec97 100644 --- a/providers/jenkins/README.rst +++ b/providers/jenkins/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-jenkins`` -Release: ``4.2.2`` +Release: ``4.2.3`` `Jenkins `__ @@ -36,7 +36,7 @@ This is a provider package for ``jenkins`` provider. All classes for this provid are in ``airflow.providers.jenkins`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,4 +78,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/jenkins/docs/changelog.rst b/providers/jenkins/docs/changelog.rst index 537b62068f89a..ddebd985bca67 100644 --- a/providers/jenkins/docs/changelog.rst +++ b/providers/jenkins/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +4.2.3 +..... + +Misc +~~~~ + +* ``Migrate Jenkins connection UI metadata to YAML (#62432)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 4.2.2 ..... diff --git a/providers/jenkins/docs/index.rst b/providers/jenkins/docs/index.rst index 4795e02ec8134..16bb23054a39e 100644 --- a/providers/jenkins/docs/index.rst +++ b/providers/jenkins/docs/index.rst @@ -76,7 +76,7 @@ apache-airflow-providers-jenkins package `Jenkins `__ -Release: 4.2.2 +Release: 4.2.3 Provider package ---------------- @@ -129,5 +129,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-jenkins 4.2.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-jenkins 4.2.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jenkins 4.2.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-jenkins 4.2.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/jenkins/provider.yaml b/providers/jenkins/provider.yaml index 6c6f08dba394b..402e5b93c5152 100644 --- a/providers/jenkins/provider.yaml +++ b/providers/jenkins/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335202 +source-date-epoch: 1773070923 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.2.3 - 4.2.2 - 4.2.1 - 4.2.0 diff --git a/providers/jenkins/pyproject.toml b/providers/jenkins/pyproject.toml index 18db1ad0ea024..e3dd747fa6a64 100644 --- a/providers/jenkins/pyproject.toml +++ b/providers/jenkins/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-jenkins" -version = "4.2.2" +version = "4.2.3" description = "Provider package apache-airflow-providers-jenkins for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -98,8 +98,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-jenkins/4.2.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/jenkins/src/airflow/providers/jenkins/__init__.py b/providers/jenkins/src/airflow/providers/jenkins/__init__.py index 256291edc2068..8a8614d24362c 100644 --- a/providers/jenkins/src/airflow/providers/jenkins/__init__.py +++ b/providers/jenkins/src/airflow/providers/jenkins/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.2.2" +__version__ = "4.2.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/keycloak/README.rst b/providers/keycloak/README.rst index c32c27458d2f2..d0bc895a88a42 100644 --- a/providers/keycloak/README.rst +++ b/providers/keycloak/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-keycloak`` -Release: ``0.5.3`` +Release: ``0.6.0`` ``Keycloak Provider`` @@ -36,7 +36,7 @@ This is a provider package for ``keycloak`` provider. All classes for this provi are in ``airflow.providers.keycloak`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,4 +78,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/keycloak/docs/changelog.rst b/providers/keycloak/docs/changelog.rst index b405ae7170aaf..529a81ffbe9d7 100644 --- a/providers/keycloak/docs/changelog.rst +++ b/providers/keycloak/docs/changelog.rst @@ -25,6 +25,25 @@ Changelog --------- +0.6.0 +..... + +Features +~~~~~~~~ + +* ``Add method to retrieve teams from Keycloak as resources (#62715)`` + +Bug Fixes +~~~~~~~~~ + +* ``Check 'id_token' format before redirecting in Keycloak auth manager (#62813)`` +* ``Do not get 'logout_callback_url' from request in keycloak auth manager (#62795)`` +* ``Scope session token in cookie to base_url (#62771)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Add Apache Airflow Provider Registry (#62261)`` + 0.5.3 ..... diff --git a/providers/keycloak/docs/index.rst b/providers/keycloak/docs/index.rst index 93143d0c18eac..704a84a388b15 100644 --- a/providers/keycloak/docs/index.rst +++ b/providers/keycloak/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-keycloak package ``Keycloak Provider`` -Release: 0.5.3 +Release: 0.6.0 Provider package ---------------- @@ -131,5 +131,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-keycloak 0.5.3 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-keycloak 0.5.3 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-keycloak 0.6.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-keycloak 0.6.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/keycloak/provider.yaml b/providers/keycloak/provider.yaml index ababda03f1588..081421d74e28b 100644 --- a/providers/keycloak/provider.yaml +++ b/providers/keycloak/provider.yaml @@ -23,9 +23,10 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065004 +source-date-epoch: 1773070964 # note that those versions are maintained by release manager - do not update them manually versions: + - 0.6.0 - 0.5.3 - 0.5.2 - 0.5.1 diff --git a/providers/keycloak/pyproject.toml b/providers/keycloak/pyproject.toml index b52d63dfe8d9b..4e666b5ac6c0e 100644 --- a/providers/keycloak/pyproject.toml +++ b/providers/keycloak/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-keycloak" -version = "0.5.3" +version = "0.6.0" description = "Provider package apache-airflow-providers-keycloak for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -98,8 +98,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.5.3" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.5.3/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.6.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-keycloak/0.6.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/keycloak/src/airflow/providers/keycloak/__init__.py b/providers/keycloak/src/airflow/providers/keycloak/__init__.py index 3656d64cbd016..9ef43bea7aaae 100644 --- a/providers/keycloak/src/airflow/providers/keycloak/__init__.py +++ b/providers/keycloak/src/airflow/providers/keycloak/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "0.5.3" +__version__ = "0.6.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "3.0.0" diff --git a/providers/microsoft/azure/README.rst b/providers/microsoft/azure/README.rst index ed9497bcd46ea..05b49eaebb204 100644 --- a/providers/microsoft/azure/README.rst +++ b/providers/microsoft/azure/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-microsoft-azure`` -Release: ``13.0.0`` +Release: ``13.0.1`` `Microsoft Azure `__ @@ -36,7 +36,7 @@ This is a provider package for ``microsoft.azure`` provider. All classes for thi are in ``airflow.providers.microsoft.azure`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -121,4 +121,4 @@ Extra Dependencies ==================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/microsoft/azure/docs/changelog.rst b/providers/microsoft/azure/docs/changelog.rst index e1c2f81a29fae..14fc206ed0f0b 100644 --- a/providers/microsoft/azure/docs/changelog.rst +++ b/providers/microsoft/azure/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +13.0.1 +...... + +Bug Fixes +~~~~~~~~~ + +* ``Fix PowerBIDatasetRefreshOperator to properly respect wait_for_completion flag (#62842)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 13.0.0 ...... diff --git a/providers/microsoft/azure/docs/index.rst b/providers/microsoft/azure/docs/index.rst index 6aa0bfe4483fe..8ba0c07f63738 100644 --- a/providers/microsoft/azure/docs/index.rst +++ b/providers/microsoft/azure/docs/index.rst @@ -84,7 +84,7 @@ apache-airflow-providers-microsoft-azure package `Microsoft Azure `__ -Release: 13.0.0 +Release: 13.0.1 Provider package ---------------- @@ -168,5 +168,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-microsoft-azure 13.0.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-microsoft-azure 13.0.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-microsoft-azure 13.0.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-microsoft-azure 13.0.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/microsoft/azure/provider.yaml b/providers/microsoft/azure/provider.yaml index c49e000bcb7b0..8b89a7cbd0a0e 100644 --- a/providers/microsoft/azure/provider.yaml +++ b/providers/microsoft/azure/provider.yaml @@ -21,12 +21,13 @@ description: | `Microsoft Azure `__ state: ready lifecycle: production -source-date-epoch: 1770752842 +source-date-epoch: 1773070980 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 13.0.1 - 13.0.0 - 12.10.3 - 12.10.2 diff --git a/providers/microsoft/azure/pyproject.toml b/providers/microsoft/azure/pyproject.toml index 00ca93d806339..3cc4edf2c5376 100644 --- a/providers/microsoft/azure/pyproject.toml +++ b/providers/microsoft/azure/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-microsoft-azure" -version = "13.0.0" +version = "13.0.1" description = "Provider package apache-airflow-providers-microsoft-azure for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -152,8 +152,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-microsoft-azure/13.0.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py index 8e3e291443a6e..84eafe5fb7b0d 100644 --- a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py +++ b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "13.0.0" +__version__ = "13.0.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/microsoft/psrp/docs/.latest-doc-only-change.txt b/providers/microsoft/psrp/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/microsoft/psrp/docs/.latest-doc-only-change.txt +++ b/providers/microsoft/psrp/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/mongo/README.rst b/providers/mongo/README.rst index d0bea0d7b5654..2ab0bacd2d871 100644 --- a/providers/mongo/README.rst +++ b/providers/mongo/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-mongo`` -Release: ``5.3.2`` +Release: ``5.3.3`` `MongoDB `__ @@ -36,7 +36,7 @@ This is a provider package for ``mongo`` provider. All classes for this provider are in ``airflow.providers.mongo`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -79,4 +79,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/mongo/docs/changelog.rst b/providers/mongo/docs/changelog.rst index e39d290fbf399..b680a04154d01 100644 --- a/providers/mongo/docs/changelog.rst +++ b/providers/mongo/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +5.3.3 +..... + +Misc +~~~~ + +* ``Migrate mongo connection UI metadata to YAML (#62444)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 5.3.2 ..... diff --git a/providers/mongo/docs/index.rst b/providers/mongo/docs/index.rst index eb704b6129d13..d7f192eb5a53c 100644 --- a/providers/mongo/docs/index.rst +++ b/providers/mongo/docs/index.rst @@ -62,7 +62,7 @@ apache-airflow-providers-mongo package `MongoDB `__ -Release: 5.3.2 +Release: 5.3.3 Provider package ---------------- @@ -116,5 +116,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-mongo 5.3.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-mongo 5.3.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-mongo 5.3.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-mongo 5.3.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/mongo/provider.yaml b/providers/mongo/provider.yaml index adb1c5de2d666..6082719a7c4ca 100644 --- a/providers/mongo/provider.yaml +++ b/providers/mongo/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335322 +source-date-epoch: 1773071003 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.3.3 - 5.3.2 - 5.3.1 - 5.3.0 diff --git a/providers/mongo/pyproject.toml b/providers/mongo/pyproject.toml index f870868558703..1949f0f7a6002 100644 --- a/providers/mongo/pyproject.toml +++ b/providers/mongo/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-mongo" -version = "5.3.2" +version = "5.3.3" description = "Provider package apache-airflow-providers-mongo for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -100,8 +100,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-mongo/5.3.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/mongo/src/airflow/providers/mongo/__init__.py b/providers/mongo/src/airflow/providers/mongo/__init__.py index b6342b81b360e..7b5e05a66a6a4 100644 --- a/providers/mongo/src/airflow/providers/mongo/__init__.py +++ b/providers/mongo/src/airflow/providers/mongo/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.3.2" +__version__ = "5.3.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/neo4j/docs/.latest-doc-only-change.txt b/providers/neo4j/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/neo4j/docs/.latest-doc-only-change.txt +++ b/providers/neo4j/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/openai/docs/.latest-doc-only-change.txt b/providers/openai/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/openai/docs/.latest-doc-only-change.txt +++ b/providers/openai/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/openfaas/docs/.latest-doc-only-change.txt b/providers/openfaas/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/openfaas/docs/.latest-doc-only-change.txt +++ b/providers/openfaas/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/openlineage/README.rst b/providers/openlineage/README.rst index 51fd4f7839b54..c599ca655d9c4 100644 --- a/providers/openlineage/README.rst +++ b/providers/openlineage/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-openlineage`` -Release: ``2.11.0`` +Release: ``2.12.0`` `OpenLineage `__ is an open framework for data lineage collection. @@ -37,7 +37,7 @@ This is a provider package for ``openlineage`` provider. All classes for this pr are in ``airflow.providers.openlineage`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -92,4 +92,4 @@ Extra Dependencies ============== ====================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/openlineage/docs/changelog.rst b/providers/openlineage/docs/changelog.rst index cc078d4561025..d3421615124aa 100644 --- a/providers/openlineage/docs/changelog.rst +++ b/providers/openlineage/docs/changelog.rst @@ -26,6 +26,17 @@ Changelog --------- +2.12.0 +...... + +Features +~~~~~~~~ + +* ``Add DagRun note to OL events (#62347)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 2.11.0 ...... diff --git a/providers/openlineage/docs/index.rst b/providers/openlineage/docs/index.rst index c82fec5863a4f..eb02224d38175 100644 --- a/providers/openlineage/docs/index.rst +++ b/providers/openlineage/docs/index.rst @@ -83,7 +83,7 @@ apache-airflow-providers-openlineage package At its core it is an extensible specification that systems can use to interoperate with lineage metadata. -Release: 2.11.0 +Release: 2.12.0 Provider package ---------------- @@ -140,5 +140,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-openlineage 2.11.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-openlineage 2.11.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-openlineage 2.12.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-openlineage 2.12.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/openlineage/provider.yaml b/providers/openlineage/provider.yaml index f0c28868c56ae..42bd12f391922 100644 --- a/providers/openlineage/provider.yaml +++ b/providers/openlineage/provider.yaml @@ -24,12 +24,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065120 +source-date-epoch: 1773071035 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 2.12.0 - 2.11.0 - 2.10.2 - 2.10.1 diff --git a/providers/openlineage/pyproject.toml b/providers/openlineage/pyproject.toml index 101e190d0b98a..890dc5fc37766 100644 --- a/providers/openlineage/pyproject.toml +++ b/providers/openlineage/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-openlineage" -version = "2.11.0" +version = "2.12.0" description = "Provider package apache-airflow-providers-openlineage for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -114,8 +114,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.11.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.11.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.12.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-openlineage/2.12.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/openlineage/src/airflow/providers/openlineage/__init__.py b/providers/openlineage/src/airflow/providers/openlineage/__init__.py index d27aa3d9dab0f..17d9e85bd0859 100644 --- a/providers/openlineage/src/airflow/providers/openlineage/__init__.py +++ b/providers/openlineage/src/airflow/providers/openlineage/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "2.11.0" +__version__ = "2.12.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/opsgenie/docs/.latest-doc-only-change.txt b/providers/opsgenie/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/opsgenie/docs/.latest-doc-only-change.txt +++ b/providers/opsgenie/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/oracle/README.rst b/providers/oracle/README.rst index 2ed04a9bb3814..67377de8cfd57 100644 --- a/providers/oracle/README.rst +++ b/providers/oracle/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-oracle`` -Release: ``4.5.0`` +Release: ``4.5.1`` `Oracle `__ @@ -36,7 +36,7 @@ This is a provider package for ``oracle`` provider. All classes for this provide are in ``airflow.providers.oracle`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -91,4 +91,4 @@ Extra Dependencies =============== ======================================================================================================================================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/oracle/docs/changelog.rst b/providers/oracle/docs/changelog.rst index 7c2fc4aa413f7..018fb29ef9ef8 100644 --- a/providers/oracle/docs/changelog.rst +++ b/providers/oracle/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +4.5.1 +..... + +Bug Fixes +~~~~~~~~~ + +* ``Use conn.schema as service_name fallback in OracleHook (#62895)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 4.5.0 ..... diff --git a/providers/oracle/docs/index.rst b/providers/oracle/docs/index.rst index 300ade40662cd..6be1b0738b382 100644 --- a/providers/oracle/docs/index.rst +++ b/providers/oracle/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-oracle package `Oracle `__ -Release: 4.5.0 +Release: 4.5.1 Provider package ---------------- @@ -133,5 +133,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-oracle 4.5.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-oracle 4.5.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-oracle 4.5.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-oracle 4.5.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/oracle/provider.yaml b/providers/oracle/provider.yaml index b159bbca15e5d..49d3553037487 100644 --- a/providers/oracle/provider.yaml +++ b/providers/oracle/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065135 +source-date-epoch: 1773100961 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.5.1 - 4.5.0 - 4.4.0 - 4.3.1 diff --git a/providers/oracle/pyproject.toml b/providers/oracle/pyproject.toml index 9ee108484ef08..82f1ea783b025 100644 --- a/providers/oracle/pyproject.toml +++ b/providers/oracle/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-oracle" -version = "4.5.0" +version = "4.5.1" description = "Provider package apache-airflow-providers-oracle for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -118,8 +118,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-oracle/4.5.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/oracle/src/airflow/providers/oracle/__init__.py b/providers/oracle/src/airflow/providers/oracle/__init__.py index 802d6e799e1b8..38b2bbae07723 100644 --- a/providers/oracle/src/airflow/providers/oracle/__init__.py +++ b/providers/oracle/src/airflow/providers/oracle/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.5.0" +__version__ = "4.5.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/pagerduty/docs/.latest-doc-only-change.txt b/providers/pagerduty/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/pagerduty/docs/.latest-doc-only-change.txt +++ b/providers/pagerduty/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/papermill/docs/.latest-doc-only-change.txt b/providers/papermill/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/papermill/docs/.latest-doc-only-change.txt +++ b/providers/papermill/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/pinecone/docs/.latest-doc-only-change.txt b/providers/pinecone/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/pinecone/docs/.latest-doc-only-change.txt +++ b/providers/pinecone/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/postgres/README.rst b/providers/postgres/README.rst index bcdf94557aa3c..fb21e54b43ba7 100644 --- a/providers/postgres/README.rst +++ b/providers/postgres/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-postgres`` -Release: ``6.6.0`` +Release: ``6.6.1`` `PostgreSQL `__ @@ -36,7 +36,7 @@ This is a provider package for ``postgres`` provider. All classes for this provi are in ``airflow.providers.postgres`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -100,4 +100,4 @@ Extra Dependencies =================== ===================================================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/postgres/docs/changelog.rst b/providers/postgres/docs/changelog.rst index cf290e5d81343..bd2d034db3d90 100644 --- a/providers/postgres/docs/changelog.rst +++ b/providers/postgres/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +6.6.1 +..... + +Misc +~~~~ + +* ``Migrate postgres connection UI metadata to YAML (#62445)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 6.6.0 ..... diff --git a/providers/postgres/docs/index.rst b/providers/postgres/docs/index.rst index e81d4f5c0bc7b..df69d2cef59df 100644 --- a/providers/postgres/docs/index.rst +++ b/providers/postgres/docs/index.rst @@ -78,7 +78,7 @@ apache-airflow-providers-postgres package `PostgreSQL `__ -Release: 6.6.0 +Release: 6.6.1 Provider package ---------------- @@ -138,5 +138,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-postgres 6.6.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-postgres 6.6.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-postgres 6.6.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-postgres 6.6.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/postgres/provider.yaml b/providers/postgres/provider.yaml index 9535a91baddec..23d5801ac4bd7 100644 --- a/providers/postgres/provider.yaml +++ b/providers/postgres/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065167 +source-date-epoch: 1773071067 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 6.6.1 - 6.6.0 - 6.5.4 - 6.5.3 diff --git a/providers/postgres/pyproject.toml b/providers/postgres/pyproject.toml index f24d724696c26..922aa70c68db2 100644 --- a/providers/postgres/pyproject.toml +++ b/providers/postgres/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-postgres" -version = "6.6.0" +version = "6.6.1" description = "Provider package apache-airflow-providers-postgres for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -134,8 +134,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/6.6.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/postgres/src/airflow/providers/postgres/__init__.py b/providers/postgres/src/airflow/providers/postgres/__init__.py index 17ec5e58942c3..848db5ea9a905 100644 --- a/providers/postgres/src/airflow/providers/postgres/__init__.py +++ b/providers/postgres/src/airflow/providers/postgres/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "6.6.0" +__version__ = "6.6.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/qdrant/docs/.latest-doc-only-change.txt b/providers/qdrant/docs/.latest-doc-only-change.txt index 3f35346f79b81..2c1ab461a9c8e 100644 --- a/providers/qdrant/docs/.latest-doc-only-change.txt +++ b/providers/qdrant/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -134348e1895ad54cfa4d3a75a78bafe872328b11 +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/redis/docs/.latest-doc-only-change.txt b/providers/redis/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/redis/docs/.latest-doc-only-change.txt +++ b/providers/redis/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/salesforce/README.rst b/providers/salesforce/README.rst index 04a7caa51e42e..dbc673594f779 100644 --- a/providers/salesforce/README.rst +++ b/providers/salesforce/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-salesforce`` -Release: ``5.12.2`` +Release: ``5.12.3`` `Salesforce `__ @@ -36,7 +36,7 @@ This is a provider package for ``salesforce`` provider. All classes for this pro are in ``airflow.providers.salesforce`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -80,4 +80,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/salesforce/docs/changelog.rst b/providers/salesforce/docs/changelog.rst index 4f3d446c8de32..9221f3a338f4c 100644 --- a/providers/salesforce/docs/changelog.rst +++ b/providers/salesforce/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +5.12.3 +...... + +Misc +~~~~ + +* ``Migrate salesforce connection UI metadata to YAML (#62446)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 5.12.2 ...... diff --git a/providers/salesforce/docs/index.rst b/providers/salesforce/docs/index.rst index ec768b2e531b6..f4a4edad10a02 100644 --- a/providers/salesforce/docs/index.rst +++ b/providers/salesforce/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-salesforce package `Salesforce `__ -Release: 5.12.2 +Release: 5.12.3 Provider package ---------------- @@ -132,5 +132,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-salesforce 5.12.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-salesforce 5.12.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-salesforce 5.12.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-salesforce 5.12.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/salesforce/provider.yaml b/providers/salesforce/provider.yaml index 06cfbcdfc49b1..e1b1035bf9a9a 100644 --- a/providers/salesforce/provider.yaml +++ b/providers/salesforce/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335553 +source-date-epoch: 1773071089 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.12.3 - 5.12.2 - 5.12.1 - 5.12.0 diff --git a/providers/salesforce/pyproject.toml b/providers/salesforce/pyproject.toml index 576de240bc6f8..8af3ace68e2a1 100644 --- a/providers/salesforce/pyproject.toml +++ b/providers/salesforce/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-salesforce" -version = "5.12.2" +version = "5.12.3" description = "Provider package apache-airflow-providers-salesforce for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -100,8 +100,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-salesforce/5.12.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/salesforce/src/airflow/providers/salesforce/__init__.py b/providers/salesforce/src/airflow/providers/salesforce/__init__.py index f7d156a4fee8b..7ae0165b0585f 100644 --- a/providers/salesforce/src/airflow/providers/salesforce/__init__.py +++ b/providers/salesforce/src/airflow/providers/salesforce/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.12.2" +__version__ = "5.12.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/samba/README.rst b/providers/samba/README.rst index e1f79b9216588..ef8b08092174b 100644 --- a/providers/samba/README.rst +++ b/providers/samba/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-samba`` -Release: ``4.12.2`` +Release: ``4.12.3`` `Samba `__ @@ -36,7 +36,7 @@ This is a provider package for ``samba`` provider. All classes for this provider are in ``airflow.providers.samba`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -88,4 +88,4 @@ Extra Dependencies ========== =================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/samba/docs/changelog.rst b/providers/samba/docs/changelog.rst index 61242761b44cb..4a2216edb8cca 100644 --- a/providers/samba/docs/changelog.rst +++ b/providers/samba/docs/changelog.rst @@ -27,6 +27,19 @@ Changelog --------- +4.12.3 +...... + +Misc +~~~~ + +* ``Migrate samba connection UI metadata to YAML (#62514)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + 4.12.2 ...... diff --git a/providers/samba/docs/index.rst b/providers/samba/docs/index.rst index c9f7dffd7b3cb..8e18adc109d60 100644 --- a/providers/samba/docs/index.rst +++ b/providers/samba/docs/index.rst @@ -76,7 +76,7 @@ apache-airflow-providers-samba package `Samba `__ -Release: 4.12.2 +Release: 4.12.3 Provider package ---------------- @@ -130,5 +130,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-samba 4.12.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-samba 4.12.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-samba 4.12.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-samba 4.12.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/samba/provider.yaml b/providers/samba/provider.yaml index ba87fc8e5b58e..0e11b63ad6f38 100644 --- a/providers/samba/provider.yaml +++ b/providers/samba/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335561 +source-date-epoch: 1773071107 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.12.3 - 4.12.2 - 4.12.1 - 4.12.0 diff --git a/providers/samba/pyproject.toml b/providers/samba/pyproject.toml index b8d9178d2bdcb..0cb09047b7b06 100644 --- a/providers/samba/pyproject.toml +++ b/providers/samba/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-samba" -version = "4.12.2" +version = "4.12.3" description = "Provider package apache-airflow-providers-samba for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -106,8 +106,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-samba/4.12.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/samba/src/airflow/providers/samba/__init__.py b/providers/samba/src/airflow/providers/samba/__init__.py index 6b4ed70730f88..f1c28bff822ed 100644 --- a/providers/samba/src/airflow/providers/samba/__init__.py +++ b/providers/samba/src/airflow/providers/samba/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.12.2" +__version__ = "4.12.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/segment/docs/.latest-doc-only-change.txt b/providers/segment/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/segment/docs/.latest-doc-only-change.txt +++ b/providers/segment/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/sendgrid/docs/.latest-doc-only-change.txt b/providers/sendgrid/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/sendgrid/docs/.latest-doc-only-change.txt +++ b/providers/sendgrid/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/sftp/README.rst b/providers/sftp/README.rst index 867e2bbdbf170..060fef7ae53e3 100644 --- a/providers/sftp/README.rst +++ b/providers/sftp/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-sftp`` -Release: ``5.7.0`` +Release: ``5.7.1`` `SSH File Transfer Protocol (SFTP) `__ @@ -36,7 +36,7 @@ This is a provider package for ``sftp`` provider. All classes for this provider are in ``airflow.providers.sftp`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -56,7 +56,7 @@ PIP package Version required ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-ssh`` ``>=4.0.0`` ``apache-airflow-providers-common-compat`` ``>=1.12.0`` -``paramiko`` ``>=2.9.0,<4.0.0`` +``paramiko`` ``>=3.4.0,<4.0.0`` ``asyncssh`` ``>=2.12.0`` ========================================== ================== @@ -92,4 +92,4 @@ Extra Dependencies =============== ======================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/sftp/docs/changelog.rst b/providers/sftp/docs/changelog.rst index 3ea2f4a97f700..3f21e158373d8 100644 --- a/providers/sftp/docs/changelog.rst +++ b/providers/sftp/docs/changelog.rst @@ -27,6 +27,20 @@ Changelog --------- +5.7.1 +..... + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 2] Migrate connection UI metadata to YAML for more providers (#62109)`` + 5.7.0 ..... diff --git a/providers/sftp/docs/index.rst b/providers/sftp/docs/index.rst index b8e79b2aab2dc..e84e67bd263b6 100644 --- a/providers/sftp/docs/index.rst +++ b/providers/sftp/docs/index.rst @@ -77,7 +77,7 @@ apache-airflow-providers-sftp package `SSH File Transfer Protocol (SFTP) `__ -Release: 5.7.0 +Release: 5.7.1 Provider package ---------------- @@ -134,5 +134,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-sftp 5.7.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-sftp 5.7.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-sftp 5.7.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-sftp 5.7.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/sftp/provider.yaml b/providers/sftp/provider.yaml index b763b8c49ce78..f9082754991cf 100644 --- a/providers/sftp/provider.yaml +++ b/providers/sftp/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769461567 +source-date-epoch: 1773071130 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 5.7.1 - 5.7.0 - 5.6.0 - 5.5.1 diff --git a/providers/sftp/pyproject.toml b/providers/sftp/pyproject.toml index 0322cf9ae41ce..0b5094b8a2e2e 100644 --- a/providers/sftp/pyproject.toml +++ b/providers/sftp/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-sftp" -version = "5.7.0" +version = "5.7.1" description = "Provider package apache-airflow-providers-sftp for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -113,8 +113,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.7.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/sftp/src/airflow/providers/sftp/__init__.py b/providers/sftp/src/airflow/providers/sftp/__init__.py index 12dd3477cefa5..650766498cacc 100644 --- a/providers/sftp/src/airflow/providers/sftp/__init__.py +++ b/providers/sftp/src/airflow/providers/sftp/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.7.0" +__version__ = "5.7.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/singularity/docs/.latest-doc-only-change.txt b/providers/singularity/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/singularity/docs/.latest-doc-only-change.txt +++ b/providers/singularity/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/slack/README.rst b/providers/slack/README.rst index f1d7982968043..adfc818821949 100644 --- a/providers/slack/README.rst +++ b/providers/slack/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-slack`` -Release: ``9.7.0`` +Release: ``9.8.0`` `Slack `__ services integration including: @@ -39,7 +39,7 @@ This is a provider package for ``slack`` provider. All classes for this provider are in ``airflow.providers.slack`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -84,4 +84,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/slack/docs/changelog.rst b/providers/slack/docs/changelog.rst index 15ebd8e518742..1bdef66d5fb05 100644 --- a/providers/slack/docs/changelog.rst +++ b/providers/slack/docs/changelog.rst @@ -27,6 +27,17 @@ Changelog --------- +9.8.0 +..... + +Features +~~~~~~~~ + +* ``Add thread_ts parameter to Slack operators for thread replies (#62289)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 9.7.0 ..... diff --git a/providers/slack/docs/index.rst b/providers/slack/docs/index.rst index 19d9fdc5e344b..38095c90e9975 100644 --- a/providers/slack/docs/index.rst +++ b/providers/slack/docs/index.rst @@ -81,7 +81,7 @@ apache-airflow-providers-slack package - `Slack Incoming Webhook `__ -Release: 9.7.0 +Release: 9.8.0 Provider package ---------------- @@ -137,5 +137,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-slack 9.7.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-slack 9.7.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-slack 9.8.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-slack 9.8.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/slack/provider.yaml b/providers/slack/provider.yaml index c68232222619c..0f477e12658ec 100644 --- a/providers/slack/provider.yaml +++ b/providers/slack/provider.yaml @@ -26,12 +26,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065264 +source-date-epoch: 1773071150 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 9.8.0 - 9.7.0 - 9.6.2 - 9.6.1 diff --git a/providers/slack/pyproject.toml b/providers/slack/pyproject.toml index 3b33b5b327991..3092429744fda 100644 --- a/providers/slack/pyproject.toml +++ b/providers/slack/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-slack" -version = "9.7.0" +version = "9.8.0" description = "Provider package apache-airflow-providers-slack for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -104,8 +104,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.7.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.7.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.8.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-slack/9.8.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/slack/src/airflow/providers/slack/__init__.py b/providers/slack/src/airflow/providers/slack/__init__.py index 76faae8051e5d..ca7c532255cb0 100644 --- a/providers/slack/src/airflow/providers/slack/__init__.py +++ b/providers/slack/src/airflow/providers/slack/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "9.7.0" +__version__ = "9.8.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/smtp/README.rst b/providers/smtp/README.rst index d200725496af0..b8f30b46a40e9 100644 --- a/providers/smtp/README.rst +++ b/providers/smtp/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-smtp`` -Release: ``2.4.2`` +Release: ``2.4.3`` `Simple Mail Transfer Protocol (SMTP) `__ @@ -36,7 +36,7 @@ This is a provider package for ``smtp`` provider. All classes for this provider are in ``airflow.providers.smtp`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -78,4 +78,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/smtp/docs/changelog.rst b/providers/smtp/docs/changelog.rst index f9ea95f367dff..eb3f28d0f8a53 100644 --- a/providers/smtp/docs/changelog.rst +++ b/providers/smtp/docs/changelog.rst @@ -28,6 +28,21 @@ Changelog --------- +2.4.3 +..... + +Bug Fixes +~~~~~~~~~ + +* ``Fix OAuth2 XOAUTH2 auth and EHLO after STARTTLS in SmtpHook (#62879)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 3] Migrate connection UI metadata to YAML for more providers (#62165)`` + * ``YAML first discovery for connection form metadata (#60410)`` + 2.4.2 ..... diff --git a/providers/smtp/docs/index.rst b/providers/smtp/docs/index.rst index 46136d870726a..cf471ea0b698b 100644 --- a/providers/smtp/docs/index.rst +++ b/providers/smtp/docs/index.rst @@ -64,7 +64,7 @@ apache-airflow-providers-smtp package `Simple Mail Transfer Protocol (SMTP) `__ -Release: 2.4.2 +Release: 2.4.3 Provider package ---------------- @@ -117,5 +117,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-smtp 2.4.2 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-smtp 2.4.2 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-smtp 2.4.3 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-smtp 2.4.3 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/smtp/provider.yaml b/providers/smtp/provider.yaml index 52dc0c5ce88ce..6ace7528fc96e 100644 --- a/providers/smtp/provider.yaml +++ b/providers/smtp/provider.yaml @@ -24,12 +24,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1768335608 +source-date-epoch: 1773071172 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 2.4.3 - 2.4.2 - 2.4.1 - 2.4.0 diff --git a/providers/smtp/pyproject.toml b/providers/smtp/pyproject.toml index 1faab697ef3b0..d8dbc9532d712 100644 --- a/providers/smtp/pyproject.toml +++ b/providers/smtp/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-smtp" -version = "2.4.2" +version = "2.4.3" description = "Provider package apache-airflow-providers-smtp for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -98,8 +98,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.2" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.2/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.3" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-smtp/2.4.3/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/smtp/src/airflow/providers/smtp/__init__.py b/providers/smtp/src/airflow/providers/smtp/__init__.py index c5143d55da10d..585fd3c708794 100644 --- a/providers/smtp/src/airflow/providers/smtp/__init__.py +++ b/providers/smtp/src/airflow/providers/smtp/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "2.4.2" +__version__ = "2.4.3" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/snowflake/README.rst b/providers/snowflake/README.rst index 258daff7d417c..5f5e6442906e2 100644 --- a/providers/snowflake/README.rst +++ b/providers/snowflake/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-snowflake`` -Release: ``6.10.0`` +Release: ``6.11.0`` `Snowflake `__ @@ -36,7 +36,7 @@ This is a provider package for ``snowflake`` provider. All classes for this prov are in ``airflow.providers.snowflake`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -100,4 +100,4 @@ Extra Dependencies =================== ==================================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/snowflake/docs/changelog.rst b/providers/snowflake/docs/changelog.rst index 80bb41ef89197..5919c7d833ecf 100644 --- a/providers/snowflake/docs/changelog.rst +++ b/providers/snowflake/docs/changelog.rst @@ -27,6 +27,23 @@ Changelog --------- +6.11.0 +...... + +Features +~~~~~~~~ + +* ``Allow SnowflakeHook + SnowflakeSqlApiHook 'private_key_content' to use raw key in addition to base64 encoding (#62378)`` + +Misc +~~~~ + +* ``Centralize OAuth grant_type validation in SnowflakeHook (#61969)`` +* ``Lazy load 'snowflake' imports in Snowflake provider. (#62365)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 6.10.0 ...... diff --git a/providers/snowflake/docs/index.rst b/providers/snowflake/docs/index.rst index 4114a92fdb873..e6bff2210729d 100644 --- a/providers/snowflake/docs/index.rst +++ b/providers/snowflake/docs/index.rst @@ -79,7 +79,7 @@ apache-airflow-providers-snowflake package `Snowflake `__ -Release: 6.10.0 +Release: 6.11.0 Provider package ---------------- @@ -144,5 +144,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-snowflake 6.10.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-snowflake 6.10.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-snowflake 6.11.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-snowflake 6.11.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/snowflake/provider.yaml b/providers/snowflake/provider.yaml index 8c60aefecc859..3b466b0e3388e 100644 --- a/providers/snowflake/provider.yaml +++ b/providers/snowflake/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1772065306 +source-date-epoch: 1773071191 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 6.11.0 - 6.10.0 - 6.9.1 - 6.9.0 diff --git a/providers/snowflake/pyproject.toml b/providers/snowflake/pyproject.toml index 442fb8008098b..167941cc564b9 100644 --- a/providers/snowflake/pyproject.toml +++ b/providers/snowflake/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-snowflake" -version = "6.10.0" +version = "6.11.0" description = "Provider package apache-airflow-providers-snowflake for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -124,8 +124,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.10.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.10.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.11.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-snowflake/6.11.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/snowflake/src/airflow/providers/snowflake/__init__.py b/providers/snowflake/src/airflow/providers/snowflake/__init__.py index 8fc3dd06e4795..700cb20e6f9f3 100644 --- a/providers/snowflake/src/airflow/providers/snowflake/__init__.py +++ b/providers/snowflake/src/airflow/providers/snowflake/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "6.10.0" +__version__ = "6.11.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/ssh/README.rst b/providers/ssh/README.rst index 019843f53b4a3..734636e872ae2 100644 --- a/providers/ssh/README.rst +++ b/providers/ssh/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-ssh`` -Release: ``4.3.1`` +Release: ``4.3.2`` `Secure Shell (SSH) `__ @@ -36,7 +36,7 @@ This is a provider package for ``ssh`` provider. All classes for this provider p are in ``airflow.providers.ssh`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -56,7 +56,7 @@ PIP package Version required ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``asyncssh`` ``>=2.12.0`` -``paramiko`` ``>=2.9.0,<4.0.0`` +``paramiko`` ``>=3.4.0,<4.0.0`` ``sshtunnel`` ``>=0.3.2`` ========================================== ================== @@ -80,4 +80,4 @@ Dependent package ================================================================================================================== ================= The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/ssh/docs/changelog.rst b/providers/ssh/docs/changelog.rst index a2f5d4b6d4faa..60740e88b3f03 100644 --- a/providers/ssh/docs/changelog.rst +++ b/providers/ssh/docs/changelog.rst @@ -27,6 +27,20 @@ Changelog --------- +4.3.2 +..... + +Misc +~~~~ + +* ``Bump minimum cryptography to 44.0.3 and paramiko to 3.4.0 (#62723)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Prepare documentation for next release of providers (2026-02-24) (#62495)`` + * ``Add 'lifecycle' field to provider.yaml schema and all providers per AIP-95 (#62190)`` + * ``[Part 2] Migrate connection UI metadata to YAML for more providers (#62109)`` + 4.3.1 ..... diff --git a/providers/ssh/docs/index.rst b/providers/ssh/docs/index.rst index 1081e2c748080..1b8517b630683 100644 --- a/providers/ssh/docs/index.rst +++ b/providers/ssh/docs/index.rst @@ -69,7 +69,7 @@ apache-airflow-providers-ssh package `Secure Shell (SSH) `__ -Release: 4.3.1 +Release: 4.3.2 Provider package ---------------- @@ -124,5 +124,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-ssh 4.3.1 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-ssh 4.3.1 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ssh 4.3.2 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ssh 4.3.2 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/ssh/provider.yaml b/providers/ssh/provider.yaml index 8971c35d2d0f6..6424cd6a267a1 100644 --- a/providers/ssh/provider.yaml +++ b/providers/ssh/provider.yaml @@ -23,12 +23,13 @@ description: | state: ready lifecycle: production -source-date-epoch: 1769537303 +source-date-epoch: 1773071210 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 4.3.2 - 4.3.1 - 4.3.0 - 4.2.1 diff --git a/providers/ssh/pyproject.toml b/providers/ssh/pyproject.toml index 40966736a34ed..753ca8e48171a 100644 --- a/providers/ssh/pyproject.toml +++ b/providers/ssh/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-ssh" -version = "4.3.1" +version = "4.3.2" description = "Provider package apache-airflow-providers-ssh for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -101,8 +101,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.1" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.1/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.2" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ssh/4.3.2/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/ssh/src/airflow/providers/ssh/__init__.py b/providers/ssh/src/airflow/providers/ssh/__init__.py index 0dc13683e2b2a..e9f8f6fb4e894 100644 --- a/providers/ssh/src/airflow/providers/ssh/__init__.py +++ b/providers/ssh/src/airflow/providers/ssh/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "4.3.1" +__version__ = "4.3.2" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/standard/README.rst b/providers/standard/README.rst index e934d97042206..5c61621ffd9ae 100644 --- a/providers/standard/README.rst +++ b/providers/standard/README.rst @@ -23,7 +23,7 @@ Package ``apache-airflow-providers-standard`` -Release: ``1.12.0`` +Release: ``1.12.1`` Airflow Standard Provider @@ -36,7 +36,7 @@ This is a provider package for ``standard`` provider. All classes for this provi are in ``airflow.providers.standard`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ========================================== ================== Cross provider package dependencies @@ -87,4 +87,4 @@ Extra Dependencies =============== ======================================== The changelog for the provider package can be found in the -`changelog `_. +`changelog `_. diff --git a/providers/standard/docs/changelog.rst b/providers/standard/docs/changelog.rst index 598903059329c..77e8173ee2552 100644 --- a/providers/standard/docs/changelog.rst +++ b/providers/standard/docs/changelog.rst @@ -35,6 +35,24 @@ Changelog --------- +1.12.1 +...... + +Bug Fixes +~~~~~~~~~ + +* ``Fix PythonVirtualenvOperator cannot run with pendulum<3 (#62604)`` + +Misc +~~~~ + +* ``Consolidate 'SkipMixin' imports through 'common-compat' layer (#62776)`` +* ``Move SkipMixin and BranchMixIn to Task SDK (#62749)`` +* ``Move determine_kwargs and KeywordParameters to SDK DecoratedOperator (#62746)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + 1.12.0 ...... diff --git a/providers/standard/docs/index.rst b/providers/standard/docs/index.rst index b482ca7a5f46a..9cf45e0868489 100644 --- a/providers/standard/docs/index.rst +++ b/providers/standard/docs/index.rst @@ -66,7 +66,7 @@ apache-airflow-providers-standard package Airflow Standard Provider -Release: 1.12.0 +Release: 1.12.1 Provider package ---------------- @@ -90,7 +90,7 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.13.0`` +``apache-airflow-providers-common-compat`` ``>=1.14.1`` ========================================== ================== Cross provider package dependencies @@ -119,5 +119,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-standard 1.12.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-standard 1.12.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-standard 1.12.1 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-standard 1.12.1 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/standard/provider.yaml b/providers/standard/provider.yaml index b815bab96aa9f..91e2498118a46 100644 --- a/providers/standard/provider.yaml +++ b/providers/standard/provider.yaml @@ -22,12 +22,13 @@ description: | Airflow Standard Provider state: ready lifecycle: production -source-date-epoch: 1772065352 +source-date-epoch: 1773071240 # Note that those versions are maintained by release manager - do not update them manually # with the exception of case where other provider in sources has >= new provider version. # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 1.12.1 - 1.12.0 - 1.11.1 - 1.11.0 diff --git a/providers/standard/pyproject.toml b/providers/standard/pyproject.toml index d6d9df1972e70..0b20194009ebd 100644 --- a/providers/standard/pyproject.toml +++ b/providers/standard/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-standard" -version = "1.12.0" +version = "1.12.1" description = "Provider package apache-airflow-providers-standard for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -59,7 +59,7 @@ requires-python = ">=3.10" # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` dependencies = [ "apache-airflow>=2.11.0", - "apache-airflow-providers-common-compat>=1.13.0", # use next version + "apache-airflow-providers-common-compat>=1.14.1", ] # The optional dependencies should be modified in place in the generated file @@ -106,8 +106,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.1" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-standard/1.12.1/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/standard/src/airflow/providers/standard/__init__.py b/providers/standard/src/airflow/providers/standard/__init__.py index 9a041985f2332..609daa225234a 100644 --- a/providers/standard/src/airflow/providers/standard/__init__.py +++ b/providers/standard/src/airflow/providers/standard/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "1.12.0" +__version__ = "1.12.1" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0" diff --git a/providers/tableau/docs/.latest-doc-only-change.txt b/providers/tableau/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/tableau/docs/.latest-doc-only-change.txt +++ b/providers/tableau/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/telegram/docs/.latest-doc-only-change.txt b/providers/telegram/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/telegram/docs/.latest-doc-only-change.txt +++ b/providers/telegram/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 diff --git a/providers/weaviate/docs/.latest-doc-only-change.txt b/providers/weaviate/docs/.latest-doc-only-change.txt index 33caaeb056916..2c1ab461a9c8e 100644 --- a/providers/weaviate/docs/.latest-doc-only-change.txt +++ b/providers/weaviate/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +da9caffdbbeab1917e1cec5726e50af5f14a5206 diff --git a/providers/zendesk/docs/.latest-doc-only-change.txt b/providers/zendesk/docs/.latest-doc-only-change.txt index 33caaeb056916..4314cebe1711b 100644 --- a/providers/zendesk/docs/.latest-doc-only-change.txt +++ b/providers/zendesk/docs/.latest-doc-only-change.txt @@ -1 +1 @@ -e9fc6bccbedbff536bc9fcdd09001267a226420e +a1ddf31098a8388d392b338ed29e8925b0e77b69 From 554b1d167203f738bac0441542eae5232496d309 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 00:48:20 +0000 Subject: [PATCH 027/595] Clean up stale pagefind index files from S3 during registry builds (#63234) --- .github/workflows/registry-build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/registry-build.yml b/.github/workflows/registry-build.yml index 094820c539f8b..0d35d02417b22 100644 --- a/.github/workflows/registry-build.yml +++ b/.github/workflows/registry-build.yml @@ -239,7 +239,14 @@ jobs: S3_BUCKET: ${{ steps.destination.outputs.bucket }} run: | aws s3 sync registry/_site/ "${S3_BUCKET}" \ - --cache-control "${REGISTRY_CACHE_CONTROL}" + --cache-control "${REGISTRY_CACHE_CONTROL}" \ + --exclude "pagefind/*" + # Pagefind generates content-hashed filenames (e.g. en_181da6f.pf_index). + # Each rebuild produces new hashes, so --delete is needed to remove stale + # index files. This is separate from the main sync which intentionally + # omits --delete to preserve files written by other steps (publish-versions). + aws s3 sync registry/_site/pagefind/ "${S3_BUCKET}pagefind/" \ + --cache-control "${REGISTRY_CACHE_CONTROL}" --delete - name: "Publish version metadata" env: From 2ab6f9490dfbc205d1176d3d5dc1204d206f5397 Mon Sep 17 00:00:00 2001 From: Xiaodong DENG Date: Mon, 9 Mar 2026 17:57:08 -0700 Subject: [PATCH 028/595] Fix undefined variable in install_from_external_spec error message (#63233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error message in install_from_external_spec() referenced ${INSTALLATION_METHOD} which does not exist — the correct variable is ${AIRFLOW_INSTALLATION_METHOD}. With set -u active, hitting this error path would crash with an "unbound variable" error instead of printing the intended user-friendly message. The typo was introduced in a1717a652b and carried forward into the inlined copies in both Dockerfiles. --- Dockerfile | 2 +- Dockerfile.ci | 2 +- scripts/docker/install_airflow_when_building_images.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 251b589564d43..aa3f1fba405bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1201,7 +1201,7 @@ function install_from_external_spec() { installation_command_flags="apache-airflow[${AIRFLOW_EXTRAS}]${AIRFLOW_VERSION_SPECIFICATION}" else echo - echo "${COLOR_RED}The '${INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" + echo "${COLOR_RED}The '${AIRFLOW_INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" echo echo "${COLOR_YELLOW}Supported methods are ('.', 'apache-airflow')${COLOR_RESET}" echo diff --git a/Dockerfile.ci b/Dockerfile.ci index 5b0e6f7eaefc0..55229964f2412 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -936,7 +936,7 @@ function install_from_external_spec() { installation_command_flags="apache-airflow[${AIRFLOW_EXTRAS}]${AIRFLOW_VERSION_SPECIFICATION}" else echo - echo "${COLOR_RED}The '${INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" + echo "${COLOR_RED}The '${AIRFLOW_INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" echo echo "${COLOR_YELLOW}Supported methods are ('.', 'apache-airflow')${COLOR_RESET}" echo diff --git a/scripts/docker/install_airflow_when_building_images.sh b/scripts/docker/install_airflow_when_building_images.sh index 5341a1cfc331b..31b2c4062d4e7 100644 --- a/scripts/docker/install_airflow_when_building_images.sh +++ b/scripts/docker/install_airflow_when_building_images.sh @@ -148,7 +148,7 @@ function install_from_external_spec() { installation_command_flags="apache-airflow[${AIRFLOW_EXTRAS}]${AIRFLOW_VERSION_SPECIFICATION}" else echo - echo "${COLOR_RED}The '${INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" + echo "${COLOR_RED}The '${AIRFLOW_INSTALLATION_METHOD}' installation method is not supported${COLOR_RESET}" echo echo "${COLOR_YELLOW}Supported methods are ('.', 'apache-airflow')${COLOR_RESET}" echo From 36918513f1b603eb1a768c97f4f82980062ee22a Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 01:06:15 +0000 Subject: [PATCH 029/595] Fix codespell failures on main (#63236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit by-passes → bypasses, by-passing → bypassing, pre-selected → preselected --- airflow-core/docs/tutorial/hitl.rst | 2 +- .../airflow/api_fastapi/execution_api/routes/task_instances.py | 2 +- .../unit/api_fastapi/execution_api/versions/head/test_xcoms.py | 2 +- go-sdk/pkg/api/client.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/airflow-core/docs/tutorial/hitl.rst b/airflow-core/docs/tutorial/hitl.rst index ab5c6ed0175b2..3a5e35c6f7918 100644 --- a/airflow-core/docs/tutorial/hitl.rst +++ b/airflow-core/docs/tutorial/hitl.rst @@ -159,7 +159,7 @@ The method ``HITLOperator.generate_link_to_ui_from_context`` can be used to gene - ``context`` – automatically passed to ``notify`` by the notifier - ``base_url`` – (optional) the base URL of the Airflow UI; if not provided, ``api.base_url`` in the configuration will be used -- ``options`` – (optional) pre-selected options for the UI page +- ``options`` – (optional) preselected options for the UI page - ``params_inputs`` – (optional) pre-loaded inputs for the UI page This makes it easy to include actionable links in notifications or logs. diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py index f22d7c125853d..53e3bbb2a9f9c 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py @@ -133,7 +133,7 @@ def ti_run( TI.hostname, TI.unixname, TI.pid, - # This selects the raw JSON value, by-passing the deserialization -- we want that to happen on the + # This selects the raw JSON value, bypassing the deserialization -- we want that to happen on the # client column("next_kwargs", JSON), ) diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py index f805971bf5207..554c2ad2c8437 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py @@ -394,7 +394,7 @@ def test_xcom_round_trip(self, client, create_task_instance, session, orig_value Test that deserialization works when XCom values are stored directly in the DB with API Server. This tests the case where the XCom value is stored from the Task API where the value is serialized - via Client SDK into JSON object and passed via the API Server to the DB. It by-passes + via Client SDK into JSON object and passed via the API Server to the DB. It bypasses the XComModel.serialize_value and stores valid Python JSON compatible objects to DB. This test is to ensure that the deserialization works correctly in this case as well as diff --git a/go-sdk/pkg/api/client.go b/go-sdk/pkg/api/client.go index f8abbf99cbc18..fd7b77fd3954c 100644 --- a/go-sdk/pkg/api/client.go +++ b/go-sdk/pkg/api/client.go @@ -61,7 +61,7 @@ func (c *Client) WithBearerToken(token string) (ClientInterface, error) { // We don't use SetAuthToken/SetAuthScheme, as that produces a (valid, but annoying) warning about using Auth // over HTTP: "Using sensitive credentials in HTTP mode is not secure." It's a time-limited-token though, so we - // can reasonably ignore that here and setting the header directly by-passes that + // can reasonably ignore that here and setting the header directly bypasses that rc.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) opts := []ClientOption{ From 5609361e40d031f97a3f32b2b55f22e1182a7d2b Mon Sep 17 00:00:00 2001 From: "D. Ferruzzi" Date: Mon, 9 Mar 2026 18:22:34 -0700 Subject: [PATCH 030/595] Docs updates for Deadlines for 3.2 (#62494) * Docs updates for Deadlines for 3.2 --- .../logging-monitoring/callbacks.rst | 10 ++ airflow-core/docs/howto/deadline-alerts.rst | 150 ++++++++++++++++-- .../newsfragments/61153.significant.rst | 19 +++ 3 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 airflow-core/newsfragments/61153.significant.rst diff --git a/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst b/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst index 25838377e5c25..9cbd3b2976bb3 100644 --- a/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst +++ b/airflow-core/docs/administration-and-deployment/logging-monitoring/callbacks.rst @@ -163,3 +163,13 @@ Here's an example of using a custom notifier: For a list of community-managed Notifiers, see :doc:`apache-airflow-providers:core-extensions/notifications`. For more information on writing a custom Notifier, see the :doc:`Notifiers <../../howto/notifications>` how-to page. + +Deadline Alert Callbacks +^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to the Dag/task lifecycle callbacks above, Airflow supports **Deadline Alert** callbacks which +trigger when a Dag run exceeds a configured time threshold. Deadline Alert callbacks use +:class:`~airflow.sdk.AsyncCallback` (runs in the Triggerer) or :class:`~airflow.sdk.SyncCallback` +(runs in the executor) and are configured on the Dag via the ``deadline`` parameter. + +For full details, see :doc:`/howto/deadline-alerts`. diff --git a/airflow-core/docs/howto/deadline-alerts.rst b/airflow-core/docs/howto/deadline-alerts.rst index 643e17fc185fb..1ed9750bf4e2e 100644 --- a/airflow-core/docs/howto/deadline-alerts.rst +++ b/airflow-core/docs/howto/deadline-alerts.rst @@ -21,13 +21,15 @@ Deadline Alerts .. warning:: Deadline Alerts are new in Airflow 3.1 and should be considered experimental. The feature may be - subject to changes in 3.2 without warning based on user feedback. + subject to changes in future versions without warning based on user feedback. |experimental| Deadline Alerts allow you to set time thresholds for your Dag runs and automatically respond when those -thresholds are exceeded. You can set up Deadline Alerts by choosing a built-in reference point, setting -an interval, and defining a response using either Airflow's Notifiers or a custom callback function. +thresholds are exceeded. You configure Deadline Alerts by choosing a reference point, setting an interval, +and defining a callback to execute if the deadline is missed. A reference may be one of the built-in +DeadlineReference options such as when the dagrun is queued or any custom method that returns a timestamp. +The callback can either be one of Airflow's Notifiers or a custom callback function. Migrating from SLA ------------------ @@ -57,8 +59,7 @@ Below is an example Dag implementation. If the Dag has not finished 15 minutes a .. code-block:: python from datetime import datetime, timedelta - from airflow import DAG - from airflow.sdk import AsyncCallback, DeadlineAlert, DeadlineReference + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference from airflow.providers.slack.notifications.slack_webhook import SlackWebhookNotifier from airflow.providers.standard.operators.empty import EmptyOperator @@ -196,18 +197,19 @@ Using Callbacks --------------- When a deadline is exceeded, the callback's callable is executed with the specified kwargs. You can use an -existing :doc:`Notifier ` or create a custom callable. A callback must be an -:class:`~airflow.sdk.AsyncCallback`, with support coming soon for :class:`~airflow.sdk.SyncCallback`. +existing :doc:`Notifier ` or create a custom callable. A callback must be either an +:class:`~airflow.sdk.AsyncCallback`, or a :class:`~airflow.sdk.SyncCallback` (SyncCallback support added in 3.2). Using Built-in Notifiers ^^^^^^^^^^^^^^^^^^^^^^^^ -Here's an example using the Slack Notifier if the Dag run has not finished within 30 minutes of it being queued: +Here's an example using the Slack Notifier with an **asynchronous callback** if the Dag run has not finished +within 30 minutes of it being queued. The callback runs in the Triggerer: .. code-block:: python with DAG( - dag_id="slack_deadline_alert", + dag_id="slack_deadline_alert_async", deadline=DeadlineAlert( reference=DeadlineReference.DAGRUN_QUEUED_AT, interval=timedelta(minutes=30), @@ -221,13 +223,33 @@ Here's an example using the Slack Notifier if the Dag run has not finished withi ): EmptyOperator(task_id="example_task") +Here's the same example using a **synchronous callback**. The callback runs in the executor: + +.. code-block:: python + + with DAG( + dag_id="slack_deadline_alert_sync", + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=30), + callback=SyncCallback( + SlackWebhookNotifier, + kwargs={ + "text": "🚨 Dag {{ dag_run.dag_id }} missed deadline at {{ deadline.deadline_time }}. DagRun: {{ dag_run }}" + }, + ), + ), + ): + EmptyOperator(task_id="example_task") + Creating Custom Callbacks ^^^^^^^^^^^^^^^^^^^^^^^^^ You can create custom callables for more complex handling. If ``kwargs`` are specified in the ``Callback``, they are passed to the callback function. **Asynchronous callbacks** must be defined somewhere in the -Triggerer's system path. +Triggerer's system path. **Synchronous callbacks** must be importable on the worker where they will be executed. + .. note:: Regarding Async Custom Deadline callbacks: @@ -237,6 +259,11 @@ Triggerer's system path. Nested callables are not currently supported. * The Triggerer will need to be restarted when a callback is added or changed in order to reload the file. +.. note:: + Regarding Synchronous callbacks: + + * Sync callbacks are sent to the executor and treated just like a Dag task with top priority. + .. note:: **Airflow ``context``:** When a deadline is missed, Airflow automatically provides a ``context`` kwarg into the callback containing information about the Dag run and the deadline. To receive it, @@ -245,9 +272,60 @@ Triggerer's system path. the callable accepts. The ``context`` keyword is reserved and cannot be used in the ``kwargs`` parameter of a ``Callback``; attempting to do so will raise a ``ValueError`` at DAG parse time. + +A **custom synchronous callback** might look like this: + +1. Place this method in your plugins folder (e.g. ``$AIRFLOW_HOME/plugins/deadline_callbacks.py``): + +.. code-block:: python + + def custom_sync_callback(**kwargs): + """Handle deadline violation with custom logic.""" + context = kwargs.get("context", {}) + print(f"Deadline exceeded for Dag {context.get('dag_run', {}).get('dag_id')}!") + print(f"Context: {context}") + print(f"Alert type: {kwargs.get('alert_type')}") + # Additional custom handling here + +2. Place this in a Dag file: + +.. code-block:: python + + from datetime import timedelta + + from deadline_callbacks import custom_sync_callback + + from airflow.providers.standard.operators.empty import EmptyOperator + from airflow.sdk import DAG, DeadlineAlert, DeadlineReference, SyncCallback + + with DAG( + dag_id="custom_sync_deadline_alert", + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=15), + callback=SyncCallback( + custom_sync_callback, + kwargs={"alert_type": "time_exceeded"}, + ), + ), + ): + EmptyOperator(task_id="example_task") + +.. tip:: + ``SyncCallback`` accepts an optional ``executor`` parameter to target a specific executor. + If not specified, the default executor is used. + + .. code-block:: python + + SyncCallback( + my_callback, + kwargs={"msg": "deadline missed"}, + executor="celery_executor", + ) + A **custom asynchronous callback** might look like this: -1. Place this method in ``/files/plugins/deadline_callbacks.py``: +1. Place this method in your plugins folder (e.g. ``$AIRFLOW_HOME/plugins/deadline_callbacks.py``): .. code-block:: python @@ -268,9 +346,8 @@ A **custom asynchronous callback** might look like this: from deadline_callbacks import custom_async_callback - from airflow import DAG from airflow.providers.standard.operators.empty import EmptyOperator - from airflow.sdk import AsyncCallback, DeadlineAlert, DeadlineReference + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference with DAG( dag_id="custom_deadline_alert", @@ -302,7 +379,7 @@ A deadline's trigger time is calculated by adding the ``interval`` to the dateti the ``reference``. For ``FIXED_DATETIME`` references, negative intervals can be particularly useful to trigger the callback *before* the reference time. -For example: +In the following examples, ``notify_team`` is either a SyncCallback or AsyncCallback defined elsewhere: .. code-block:: python @@ -386,8 +463,7 @@ Once registered [see notes below], use your custom references in Dag definitions .. code-block:: python from datetime import timedelta - from airflow import DAG - from airflow.sdk import AsyncCallback, DeadlineAlert, DeadlineReference + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference with DAG( dag_id="custom_reference_example", @@ -400,6 +476,48 @@ Once registered [see notes below], use your custom references in Dag definitions # Your tasks here ... +Multiple Deadline Alerts +^^^^^^^^^^^^^^^^^^^^^^^^ + +A Dag can have multiple Deadline Alerts. Pass a list to the ``deadline`` parameter instead of a single +``DeadlineAlert``. Each alert in the list is evaluated independently, and each may use any combination +of reference points and callback types (sync or async). + +.. code-block:: python + + from datetime import timedelta + from airflow.sdk import AsyncCallback, DAG, DeadlineAlert, DeadlineReference, SyncCallback + from airflow.providers.slack.notifications.slack_webhook import SlackWebhookNotifier + from airflow.providers.standard.operators.empty import EmptyOperator + + with DAG( + dag_id="multiple_deadline_alerts", + deadline=[ + # First alert: warn via Slack (async) if not done 30 min after queuing + DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=30), + callback=AsyncCallback( + SlackWebhookNotifier, + kwargs={"text": "⚠️ Dag {{ dag_run.dag_id }} is approaching its deadline."}, + ), + ), + # Second alert: escalate via custom sync callback if not done 60 min after queuing + DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(minutes=60), + callback=SyncCallback( + "my_plugins.escalation.escalate_to_oncall", + kwargs={"severity": "high"}, + ), + ), + ], + ): + EmptyOperator(task_id="example_task") + +This pattern is useful for creating tiered alerting strategies — for example, a warning notification +followed by a more urgent escalation if the Dag is still running. + **Important Notes:** * **Timezone Awareness**: Always return timezone-aware datetime objects. diff --git a/airflow-core/newsfragments/61153.significant.rst b/airflow-core/newsfragments/61153.significant.rst new file mode 100644 index 0000000000000..51f4727c240ae --- /dev/null +++ b/airflow-core/newsfragments/61153.significant.rst @@ -0,0 +1,19 @@ +Add synchronous callback support (``SyncCallback``) for Deadline Alerts + +Deadline Alerts now support synchronous callbacks via ``SyncCallback`` in addition to the existing +asynchronous ``AsyncCallback``. Synchronous callbacks are executed by the executor (rather than +the triggerer), and can optionally target a specific executor via the ``executor`` parameter. + +A DAG can also define multiple Deadline Alerts by passing a list to the ``deadline`` parameter, +and each alert can use either callback type. + +* Types of change + + * [ ] Dag changes + * [ ] Config changes + * [ ] API changes + * [ ] CLI changes + * [x] Behaviour changes + * [ ] Plugin changes + * [ ] Dependency changes + * [ ] Code interface changes From f98c39756680d0ffc49ee347ea389c52013458ca Mon Sep 17 00:00:00 2001 From: Jed Cunningham <66968678+jedcunningham@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:53:55 -0600 Subject: [PATCH 031/595] Fix typo in listener warning (#63238) --- airflow-core/docs/administration-and-deployment/listeners.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/docs/administration-and-deployment/listeners.rst b/airflow-core/docs/administration-and-deployment/listeners.rst index 170c55de1c576..70dde0b7fd2af 100644 --- a/airflow-core/docs/administration-and-deployment/listeners.rst +++ b/airflow-core/docs/administration-and-deployment/listeners.rst @@ -24,7 +24,7 @@ You can write listeners to enable Airflow to notify you when events happen. .. warning:: Listeners are an advanced feature of Airflow. They are not isolated from the Airflow components they run in, and - can slow down or in come cases take down your Airflow instance. As such, extra care should be taken when writing listeners. + can slow down or in some cases take down your Airflow instance. As such, extra care should be taken when writing listeners. Airflow supports notifications for the following events: From 3649636e700c2b57bb0ff747753f7ca043706ac6 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 03:58:29 +0000 Subject: [PATCH 032/595] Add Matomo analytics to the provider registry (#63239) The Airflow landing pages and Sphinx docs both track page views via Apache's Matomo instance (analytics.apache.org, site ID 13). The registry was missing this. Add the same snippet to the base layout template so all registry pages are tracked under the same property. Cookies are disabled (privacy-first) and the script loads async. --- registry/src/_includes/base.njk | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/registry/src/_includes/base.njk b/registry/src/_includes/base.njk index d2092ebd38097..02906819bb4b4 100644 --- a/registry/src/_includes/base.njk +++ b/registry/src/_includes/base.njk @@ -34,6 +34,20 @@ + + From df111d6a11eb37fb0c69315b08f4aa99b7e3307a Mon Sep 17 00:00:00 2001 From: "Jason(Zhe-You) Liu" <68415893+jason810496@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:13:20 +0800 Subject: [PATCH 033/595] Ensure all the PR links in auto-triage are clickable (#63241) --- .../airflow_breeze/commands/pr_commands.py | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/dev/breeze/src/airflow_breeze/commands/pr_commands.py b/dev/breeze/src/airflow_breeze/commands/pr_commands.py index 04cba80fd8d84..288dbb7e7c0f6 100644 --- a/dev/breeze/src/airflow_breeze/commands/pr_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/pr_commands.py @@ -945,6 +945,11 @@ def _compute_default_action( return action, f"{reason} — suggesting {action_label}" +def _pr_link(pr: PRData) -> str: + """Return a Rich-markup clickable link for a PR: [link=url]#number[/link].""" + return f"[link={pr.url}]#{pr.number}[/link]" + + def _display_pr_info_panels(pr: PRData, author_profile: dict | None): """Display PR info and author panels (shared by flagged-PR and workflow-approval flows).""" console = get_console() @@ -1098,10 +1103,10 @@ def _close_suspicious_prs( node_id = pr_info["node_id"] if _close_pr(token, node_id): - get_console().print(f" [success]PR #{pr_num} closed.[/]") + get_console().print(f" [success]PR [link={pr_info['url']}]#{pr_num}[/link] closed.[/]") closed += 1 else: - get_console().print(f" [error]Failed to close PR #{pr_num}.[/]") + get_console().print(f" [error]Failed to close PR [link={pr_info['url']}]#{pr_num}[/link].[/]") continue _add_label(token, github_repository, node_id, _SUSPICIOUS_CHANGES_LABEL) @@ -1322,7 +1327,7 @@ def auto_triage( overall = "[green]OK[/]" pr_table.add_row( - f"[link={pr.url}]#{pr.number}[/link]", + _pr_link(pr), pr.title[:50], pr.author_login, pr.author_association.lower(), @@ -1345,22 +1350,18 @@ def auto_triage( total_skipped_collaborator += 1 if verbose: get_console().print( - f" [dim]Skipping PR [link={pr.url}]#{pr.number}[/link] by " + f" [dim]Skipping PR {_pr_link(pr)} by " f"{pr.author_association.lower()} {pr.author_login}[/]" ) elif _is_bot_account(pr.author_login): total_skipped_bot += 1 if verbose: - get_console().print( - f" [dim]Skipping PR [link={pr.url}]#{pr.number}[/link] — " - f"bot account {pr.author_login}[/]" - ) + get_console().print(f" [dim]Skipping PR {_pr_link(pr)} — bot account {pr.author_login}[/]") elif _READY_FOR_REVIEW_LABEL in pr.labels: total_skipped_accepted += 1 if verbose: get_console().print( - f" [dim]Skipping PR [link={pr.url}]#{pr.number}[/link] — " - f"already has '{_READY_FOR_REVIEW_LABEL}' label[/]" + f" [dim]Skipping PR {_pr_link(pr)} — already has '{_READY_FOR_REVIEW_LABEL}' label[/]" ) else: candidate_prs.append(pr) @@ -1394,7 +1395,7 @@ def auto_triage( for pr in candidate_prs: if pr.checks_state == "FAILURE" and not pr.failed_checks and pr.head_sha: get_console().print( - f" [dim]Fetching full check details for PR #{pr.number} " + f" [dim]Fetching full check details for PR {_pr_link(pr)} " f"(failures beyond first 100 checks)...[/]" ) pr.failed_checks = _fetch_failed_checks(token, github_repository, pr.head_sha) @@ -1474,9 +1475,7 @@ def auto_triage( total_llm_errors += 1 continue if not assessment.should_flag: - get_console().print( - f" [success]PR [link={pr.url}]#{pr.number}[/link] passes quality check.[/]" - ) + get_console().print(f" [success]PR {_pr_link(pr)} passes quality check.[/]") continue assessments[pr.number] = assessment @@ -1549,7 +1548,9 @@ def auto_triage( continue action = prompt_triage_action( - f"Action for PR #{pr.number}?", default=default_action, forced_answer=answer_triage + f"Action for PR {_pr_link(pr)}?", + default=default_action, + forced_answer=answer_triage, ) if action == TriageAction.QUIT: @@ -1558,60 +1559,60 @@ def auto_triage( break if action == TriageAction.SKIP: - get_console().print(f" [info]Skipping PR #{pr.number} — no action taken.[/]") + get_console().print(f" [info]Skipping PR {_pr_link(pr)} — no action taken.[/]") total_skipped_action += 1 continue if action == TriageAction.READY: get_console().print( - f" [info]Marking PR #{pr.number} as ready — adding '{_READY_FOR_REVIEW_LABEL}' label.[/]" + f" [info]Marking PR {_pr_link(pr)} as ready — adding '{_READY_FOR_REVIEW_LABEL}' label.[/]" ) if _add_label(token, github_repository, pr.node_id, _READY_FOR_REVIEW_LABEL): get_console().print( - f" [success]Label '{_READY_FOR_REVIEW_LABEL}' added to PR #{pr.number}.[/]" + f" [success]Label '{_READY_FOR_REVIEW_LABEL}' added to PR {_pr_link(pr)}.[/]" ) total_ready += 1 else: - get_console().print(f" [warning]Failed to add label to PR #{pr.number}.[/]") + get_console().print(f" [warning]Failed to add label to PR {_pr_link(pr)}.[/]") continue if action == TriageAction.DRAFT: - get_console().print(f" Converting PR #{pr.number} to draft...") + get_console().print(f" Converting PR {_pr_link(pr)} to draft...") if _convert_pr_to_draft(token, pr.node_id): - get_console().print(f" [success]PR #{pr.number} converted to draft.[/]") + get_console().print(f" [success]PR {_pr_link(pr)} converted to draft.[/]") else: - get_console().print(f" [error]Failed to convert PR #{pr.number} to draft.[/]") + get_console().print(f" [error]Failed to convert PR {_pr_link(pr)} to draft.[/]") continue - get_console().print(f" Posting comment on PR #{pr.number}...") + get_console().print(f" Posting comment on PR {_pr_link(pr)}...") if _post_comment(token, pr.node_id, comment): - get_console().print(f" [success]Comment posted on PR #{pr.number}.[/]") + get_console().print(f" [success]Comment posted on PR {_pr_link(pr)}.[/]") total_converted += 1 else: - get_console().print(f" [error]Failed to post comment on PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to post comment on PR {_pr_link(pr)}.[/]") continue if action == TriageAction.CLOSE: - get_console().print(f" Closing PR #{pr.number}...") + get_console().print(f" Closing PR {_pr_link(pr)}...") if _close_pr(token, pr.node_id): - get_console().print(f" [success]PR #{pr.number} closed.[/]") + get_console().print(f" [success]PR {_pr_link(pr)} closed.[/]") else: - get_console().print(f" [error]Failed to close PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to close PR {_pr_link(pr)}.[/]") continue if _add_label(token, github_repository, pr.node_id, _CLOSED_QUALITY_LABEL): get_console().print( - f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR #{pr.number}.[/]" + f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR {_pr_link(pr)}.[/]" ) else: - get_console().print(f" [warning]Failed to add label to PR #{pr.number}.[/]") + get_console().print(f" [warning]Failed to add label to PR {_pr_link(pr)}.[/]") - get_console().print(f" Posting comment on PR #{pr.number}...") + get_console().print(f" Posting comment on PR {_pr_link(pr)}...") if _post_comment(token, pr.node_id, close_comment): - get_console().print(f" [success]Comment posted on PR #{pr.number}.[/]") + get_console().print(f" [success]Comment posted on PR {_pr_link(pr)}.[/]") total_closed += 1 else: - get_console().print(f" [error]Failed to post comment on PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to post comment on PR {_pr_link(pr)}.[/]") # Phase 6: Present NOT_RUN PRs for workflow approval total_workflows_approved = 0 @@ -1642,7 +1643,7 @@ def auto_triage( continue action = prompt_triage_action( - f"Action for PR #{pr.number}?", + f"Action for PR {_pr_link(pr)}?", default=TriageAction.CLOSE, forced_answer=answer_triage, ) @@ -1651,27 +1652,27 @@ def auto_triage( quit_early = True break if action == TriageAction.SKIP: - get_console().print(f" [info]Skipping PR #{pr.number} — no action taken.[/]") + get_console().print(f" [info]Skipping PR {_pr_link(pr)} — no action taken.[/]") continue if action == TriageAction.CLOSE: - get_console().print(f" Closing PR #{pr.number}...") + get_console().print(f" Closing PR {_pr_link(pr)}...") if _close_pr(token, pr.node_id): - get_console().print(f" [success]PR #{pr.number} closed.[/]") + get_console().print(f" [success]PR {_pr_link(pr)} closed.[/]") else: - get_console().print(f" [error]Failed to close PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to close PR {_pr_link(pr)}.[/]") continue if _add_label(token, github_repository, pr.node_id, _CLOSED_QUALITY_LABEL): get_console().print( - f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR #{pr.number}.[/]" + f" [success]Label '{_CLOSED_QUALITY_LABEL}' added to PR {_pr_link(pr)}.[/]" ) else: - get_console().print(f" [warning]Failed to add label to PR #{pr.number}.[/]") - get_console().print(f" Posting comment on PR #{pr.number}...") + get_console().print(f" [warning]Failed to add label to PR {_pr_link(pr)}.[/]") + get_console().print(f" Posting comment on PR {_pr_link(pr)}...") if _post_comment(token, pr.node_id, close_comment): - get_console().print(f" [success]Comment posted on PR #{pr.number}.[/]") + get_console().print(f" [success]Comment posted on PR {_pr_link(pr)}.[/]") total_closed += 1 else: - get_console().print(f" [error]Failed to post comment on PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to post comment on PR {_pr_link(pr)}.[/]") continue # For DRAFT or READY, fall through to normal workflow approval # (approve workflows first, then triage later) @@ -1682,13 +1683,13 @@ def auto_triage( if not pending_runs: get_console().print( - f" [dim]No pending workflow runs found for PR #{pr.number}. " + f" [dim]No pending workflow runs found for PR {_pr_link(pr)}. " f"Workflows may need to be triggered manually.[/]" ) continue answer = user_confirm( - f"Review diff for PR #{pr.number} before approving workflows?", + f"Review diff for PR {_pr_link(pr)} before approving workflows?", forced_answer=answer_triage, ) if answer == Answer.QUIT: @@ -1696,10 +1697,10 @@ def auto_triage( quit_early = True break if answer == Answer.NO: - get_console().print(f" [info]Skipping workflow approval for PR #{pr.number}.[/]") + get_console().print(f" [info]Skipping workflow approval for PR {_pr_link(pr)}.[/]") continue - get_console().print(f" Fetching diff for PR #{pr.number}...") + get_console().print(f" Fetching diff for PR {_pr_link(pr)}...") diff_text = _fetch_pr_diff(token, github_repository, pr.number) if diff_text: from rich.syntax import Syntax @@ -1707,18 +1708,18 @@ def auto_triage( get_console().print( Panel( Syntax(diff_text, "diff", theme="monokai", word_wrap=True), - title=f"Diff for PR #{pr.number}", + title=f"Diff for PR {_pr_link(pr)}", border_style="bright_cyan", ) ) else: get_console().print( - f" [warning]Could not fetch diff for PR #{pr.number}. " + f" [warning]Could not fetch diff for PR {_pr_link(pr)}. " f"Review manually at: {pr.url}/files[/]" ) answer = user_confirm( - f"No suspicious changes found in PR #{pr.number}? " + f"No suspicious changes found in PR {_pr_link(pr)}? " f"Approve {len(pending_runs)} workflow {'runs' if len(pending_runs) != 1 else 'run'}?", forced_answer=answer_triage, ) @@ -1728,7 +1729,7 @@ def auto_triage( break if answer == Answer.NO: get_console().print( - f"\n [bold red]Suspicious changes detected in PR #{pr.number} by {pr.author_login}.[/]" + f"\n [bold red]Suspicious changes detected in PR {_pr_link(pr)} by {pr.author_login}.[/]" ) get_console().print(f" Fetching all open PRs by {pr.author_login}...") author_prs = _fetch_author_open_prs(token, github_repository, pr.author_login) @@ -1774,11 +1775,12 @@ def auto_triage( if approved: get_console().print( f" [success]Approved {approved}/{len(pending_runs)} workflow " - f"{'runs' if len(pending_runs) != 1 else 'run'} for PR #{pr.number}.[/]" + f"{'runs' if len(pending_runs) != 1 else 'run'} for PR " + f"{_pr_link(pr)}.[/]" ) total_workflows_approved += 1 else: - get_console().print(f" [error]Failed to approve workflow runs for PR #{pr.number}.[/]") + get_console().print(f" [error]Failed to approve workflow runs for PR {_pr_link(pr)}.[/]") # Summary get_console().print() From 396f8186dc7e46cf4b615350574e8dc77542a50f Mon Sep 17 00:00:00 2001 From: yuseok89 Date: Tue, 10 Mar 2026 20:52:56 +0900 Subject: [PATCH 034/595] Fix RenderedJsonField not displaying in table cells (#63245) --- .../ui/src/components/RenderedJsonField.tsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx index 634fecd600725..5a9095172efeb 100644 --- a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx +++ b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Flex, type FlexProps } from "@chakra-ui/react"; +import { Box, Flex, type FlexProps } from "@chakra-ui/react"; import Editor, { type OnMount } from "@monaco-editor/react"; import { useCallback } from "react"; @@ -46,30 +46,34 @@ const RenderedJsonField = ({ collapsed = false, content, enableClipboard = true, ); return ( - - + + + + + + {enableClipboard ? ( From 78a40ef5f3dbcfd4857237295a2e91699f1733b0 Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Tue, 10 Mar 2026 14:16:21 +0100 Subject: [PATCH 035/595] Upgrade UI core dependencies (#63252) * chore(deps): bump the core-ui-package-updates group across 1 directory with 32 updates Bumps the core-ui-package-updates group with 32 updates in the /airflow-core/src/airflow/ui directory: | Package | From | To | | --- | --- | --- | | [@chakra-ui/react](https://github.com/chakra-ui/chakra-ui/tree/HEAD/packages/react) | `3.20.0` | `3.34.0` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.12` | `5.90.21` | | [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) | `3.13.12` | `3.13.19` | | [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react) | `12.10.0` | `12.10.1` | | [anser](https://github.com/IonicaBizau/anser) | `2.3.3` | `2.3.5` | | [axios](https://github.com/axios/axios) | `1.13.5` | `1.13.6` | | [elkjs](https://github.com/kieler/elkjs) | `0.11.0` | `0.11.1` | | [i18next](https://github.com/i18next/i18next) | `25.7.1` | `25.8.13` | | [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) | `8.2.0` | `8.2.1` | | [react-chartjs-2](https://github.com/reactchartjs/react-chartjs-2) | `5.3.0` | `5.3.1` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.56.2` | `7.71.2` | | [react-icons](https://github.com/react-icons/react-icons) | `5.5.0` | `5.6.0` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.12.0` | `7.13.1` | | [use-debounce](https://github.com/xnimorz/use-debounce) | `10.0.4` | `10.1.0` | | [yaml](https://github.com/eemeli/yaml) | `2.8.0` | `2.8.2` | | [zustand](https://github.com/pmndrs/zustand) | `5.0.4` | `5.0.11` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.57.0` | `1.58.2` | | [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) | `5.91.2` | `5.91.4` | | [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.0` | `16.3.2` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.14` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.49.0` | `8.56.1` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.49.0` | `8.56.1` | | [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.49.0` | `8.56.1` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.1.2` | `5.1.4` | | [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) | `4.2.2` | `4.2.3` | | [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) | `5.5.4` | `5.5.5` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.4.24` | `0.5.2` | | [happy-dom](https://github.com/capricorn86/happy-dom) | `20.0.11` | `20.8.3` | | [msw](https://github.com/mswjs/msw) | `2.12.4` | `2.12.10` | | [prettier](https://github.com/prettier/prettier) | `3.7.4` | `3.8.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.48.1` | `8.56.1` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.2.6` | `7.3.1` | Updates `@chakra-ui/react` from 3.20.0 to 3.34.0 - [Release notes](https://github.com/chakra-ui/chakra-ui/releases) - [Changelog](https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/chakra-ui/chakra-ui/commits/@chakra-ui/react@3.34.0/packages/react) Updates `@tanstack/react-query` from 5.90.12 to 5.90.21 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.21/packages/react-query) Updates `@tanstack/react-virtual` from 3.13.12 to 3.13.19 - [Release notes](https://github.com/TanStack/virtual/releases) - [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md) - [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.13.19/packages/react-virtual) Updates `@xyflow/react` from 12.10.0 to 12.10.1 - [Release notes](https://github.com/xyflow/xyflow/releases) - [Changelog](https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/xyflow/xyflow/commits/@xyflow/react@12.10.1/packages/react) Updates `anser` from 2.3.3 to 2.3.5 - [Release notes](https://github.com/IonicaBizau/anser/releases) - [Commits](https://github.com/IonicaBizau/anser/compare/2.3.3...2.3.5) Updates `axios` from 1.13.5 to 1.13.6 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.13.6) Updates `elkjs` from 0.11.0 to 0.11.1 - [Release notes](https://github.com/kieler/elkjs/releases) - [Commits](https://github.com/kieler/elkjs/compare/0.11.0...0.11.1) Updates `i18next` from 25.7.1 to 25.8.13 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.7.1...v25.8.13) Updates `i18next-browser-languagedetector` from 8.2.0 to 8.2.1 - [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.2.0...v8.2.1) Updates `react-chartjs-2` from 5.3.0 to 5.3.1 - [Release notes](https://github.com/reactchartjs/react-chartjs-2/releases) - [Changelog](https://github.com/reactchartjs/react-chartjs-2/blob/master/CHANGELOG.md) - [Commits](https://github.com/reactchartjs/react-chartjs-2/compare/v5.3.0...v5.3.1) Updates `react-hook-form` from 7.56.2 to 7.71.2 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.56.2...v7.71.2) Updates `react-icons` from 5.5.0 to 5.6.0 - [Release notes](https://github.com/react-icons/react-icons/releases) - [Commits](https://github.com/react-icons/react-icons/compare/v5.5.0...v5.6.0) Updates `react-router-dom` from 7.12.0 to 7.13.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.13.1/packages/react-router-dom) Updates `use-debounce` from 10.0.4 to 10.1.0 - [Release notes](https://github.com/xnimorz/use-debounce/releases) - [Changelog](https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md) - [Commits](https://github.com/xnimorz/use-debounce/commits) Updates `yaml` from 2.8.0 to 2.8.2 - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v2.8.0...v2.8.2) Updates `zustand` from 5.0.4 to 5.0.11 - [Release notes](https://github.com/pmndrs/zustand/releases) - [Commits](https://github.com/pmndrs/zustand/compare/v5.0.4...v5.0.11) Updates `@playwright/test` from 1.57.0 to 1.58.2 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.57.0...v1.58.2) Updates `@tanstack/eslint-plugin-query` from 5.91.2 to 5.91.4 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/eslint-plugin-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/eslint-plugin-query@5.91.4/packages/eslint-plugin-query) Updates `@testing-library/react` from 16.3.0 to 16.3.2 - [Release notes](https://github.com/testing-library/react-testing-library/releases) - [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/react-testing-library/compare/v16.3.0...v16.3.2) Updates `@types/react` from 19.2.7 to 19.2.14 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@typescript-eslint/eslint-plugin` from 8.49.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.49.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/parser) Updates `@typescript-eslint/utils` from 8.49.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/utils) Updates `@vitejs/plugin-react` from 5.1.2 to 5.1.4 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.1.4/packages/plugin-react) Updates `@vitejs/plugin-react-swc` from 4.2.2 to 4.2.3 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react-swc@4.2.3/packages/plugin-react-swc) Updates `eslint-plugin-prettier` from 5.5.4 to 5.5.5 - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.4...v5.5.5) Updates `eslint-plugin-react-refresh` from 0.4.24 to 0.5.2 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.24...v0.5.2) Updates `happy-dom` from 20.0.11 to 20.8.3 - [Release notes](https://github.com/capricorn86/happy-dom/releases) - [Commits](https://github.com/capricorn86/happy-dom/compare/v20.0.11...v20.8.3) Updates `msw` from 2.12.4 to 2.12.10 - [Release notes](https://github.com/mswjs/msw/releases) - [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md) - [Commits](https://github.com/mswjs/msw/compare/v2.12.4...v2.12.10) Updates `prettier` from 3.7.4 to 3.8.1 - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.7.4...3.8.1) Updates `typescript-eslint` from 8.48.1 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/typescript-eslint) Updates `vite` from 7.2.6 to 7.3.1 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite) --- updated-dependencies: - dependency-name: "@chakra-ui/react" dependency-version: 3.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@tanstack/react-query" dependency-version: 5.90.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@tanstack/react-virtual" dependency-version: 3.13.19 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@xyflow/react" dependency-version: 12.10.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: anser dependency-version: 2.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: axios dependency-version: 1.13.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: elkjs dependency-version: 0.11.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: i18next dependency-version: 25.8.13 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: i18next-browser-languagedetector dependency-version: 8.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: react-chartjs-2 dependency-version: 5.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: react-hook-form dependency-version: 7.71.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: react-icons dependency-version: 5.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: react-router-dom dependency-version: 7.13.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: use-debounce dependency-version: 10.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: yaml dependency-version: 2.8.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: zustand dependency-version: 5.0.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@playwright/test" dependency-version: 1.58.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@tanstack/eslint-plugin-query" dependency-version: 5.91.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@testing-library/react" dependency-version: 16.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@types/react" dependency-version: 19.2.14 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@typescript-eslint/parser" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@typescript-eslint/utils" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: "@vitejs/plugin-react" dependency-version: 5.1.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: "@vitejs/plugin-react-swc" dependency-version: 4.2.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: eslint-plugin-prettier dependency-version: 5.5.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: eslint-plugin-react-refresh dependency-version: 0.5.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: happy-dom dependency-version: 20.8.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: msw dependency-version: 2.12.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: core-ui-package-updates - dependency-name: prettier dependency-version: 3.8.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: typescript-eslint dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates - dependency-name: vite dependency-version: 7.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: core-ui-package-updates ... Signed-off-by: dependabot[bot] * Fix CI --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- airflow-core/src/airflow/ui/package.json | 64 +- airflow-core/src/airflow/ui/pnpm-lock.yaml | 3070 +++++++++-------- .../ui/src/components/AnsiRenderer.tsx | 1 + .../ui/src/components/DataTable/types.ts | 2 +- .../ui/src/components/renderStructuredLog.tsx | 1 + .../src/pages/TaskInstance/Logs/Logs.test.tsx | 2 +- .../TaskInstance/Logs/TaskLogContent.tsx | 2 +- .../src/airflow/ui/src/queries/useLogs.tsx | 1 + .../src/airflow/ui/src/utils/slots.tsx | 2 + 9 files changed, 1660 insertions(+), 1485 deletions(-) diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 1c61c86b4352b..97c6685793ea3 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -26,67 +26,67 @@ }, "dependencies": { "@chakra-ui/anatomy": "^2.3.4", - "@chakra-ui/react": "^3.20.0", + "@chakra-ui/react": "^3.34.0", "@emotion/react": "^11.14.0", "@lezer/highlight": "^1.2.3", "@guanmingchiu/sqlparser-ts": "^0.61.1", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.90.11", + "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", + "@tanstack/react-virtual": "^3.13.19", "@visx/group": "^3.12.0", "@visx/shape": "^3.12.0", - "@xyflow/react": "^12.10.0", - "anser": "^2.3.3", - "axios": "^1.13.5", + "@xyflow/react": "^12.10.1", + "anser": "^2.3.5", + "axios": "^1.13.6", "chakra-react-select": "^6.1.1", "chart.js": "^4.5.1", "chartjs-adapter-dayjs-4": "^1.0.4", "chartjs-plugin-annotation": "^3.1.0", "dayjs": "^1.11.19", - "elkjs": "^0.11.0", + "elkjs": "^0.11.1", "html-to-image": "^1.11.13", - "i18next": "^25.6.3", - "i18next-browser-languagedetector": "^8.2.0", + "i18next": "^25.8.14", + "i18next-browser-languagedetector": "^8.2.1", "i18next-http-backend": "^3.0.2", "next-themes": "^0.4.6", "react": "^19.2.4", - "react-chartjs-2": "^5.3.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.4", - "react-hook-form": "^7.56.1", + "react-hook-form": "^7.71.2", "react-hotkeys-hook": "^4.6.1", "react-i18next": "^15.5.1", - "react-icons": "^5.5.0", + "react-icons": "^5.6.0", "react-innertext": "^1.1.5", "react-markdown": "^9.1.0", "react-resizable-panels": "^3.0.6", - "react-router-dom": "^7.12.0", + "react-router-dom": "^7.13.1", "react-syntax-highlighter": "^15.6.1", "remark-gfm": "^4.0.1", - "use-debounce": "^10.0.4", + "use-debounce": "^10.1.0", "usehooks-ts": "^3.1.1", - "yaml": "^2.6.1", - "zustand": "^5.0.4" + "yaml": "^2.8.2", + "zustand": "^5.0.11" }, "devDependencies": { "@7nohe/openapi-react-query-codegen": "^1.6.2", "@eslint/compat": "^1.2.9", "@eslint/js": "^9.39.1", - "@playwright/test": "^1.57.0", + "@playwright/test": "^1.58.2", "@stylistic/eslint-plugin": "^2.13.0", - "@tanstack/eslint-plugin-query": "^5.91.2", + "@tanstack/eslint-plugin-query": "^5.91.4", "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.0", + "@testing-library/react": "^16.3.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^24.10.1", - "@types/react": "^19.2.7", + "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", - "@typescript-eslint/eslint-plugin": "^8.49.0", - "@typescript-eslint/parser": "^8.49.0", - "@typescript-eslint/utils": "^8.49.0", - "@vitejs/plugin-react": "^5.1.2", - "@vitejs/plugin-react-swc": "^4.0.1", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/utils": "^8.56.1", + "@vitejs/plugin-react": "^5.1.4", + "@vitejs/plugin-react-swc": "^4.2.3", "@vitest/coverage-v8": "^3.2.4", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.1", @@ -95,21 +95,21 @@ "eslint-plugin-jsonc": "^2.21.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-perfectionist": "^4.12.3", - "eslint-plugin-prettier": "^5.2.6", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.20", + "eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-unicorn": "^55.0.0", "globals": "^15.15.0", - "happy-dom": "^20.0.11", + "happy-dom": "^20.8.3", "jsonc-eslint-parser": "^2.4.0", - "msw": "^2.12.4", + "msw": "^2.12.10", "openapi-merge-cli": "^1.3.2", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "ts-morph": "^27.0.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.1.11", + "typescript-eslint": "^8.56.1", + "vite": "^7.3.1", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^3.2.4", "web-worker": "^1.5.0" diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index ab7277e16e60e..4a698017fa63c 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -21,11 +21,11 @@ importers: specifier: ^2.3.4 version: 2.3.4 '@chakra-ui/react': - specifier: ^3.20.0 - version: 3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.34.0 + version: 3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.7)(react@19.2.4) + version: 11.14.0(@types/react@19.2.14)(react@19.2.4) '@guanmingchiu/sqlparser-ts': specifier: ^0.61.1 version: 0.61.1 @@ -36,14 +36,14 @@ importers: specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.53.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.90.11 - version: 5.90.12(react@19.2.4) + specifier: ^5.90.21 + version: 5.90.21(react@19.2.4) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-virtual': - specifier: ^3.13.12 - version: 3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.13.19 + version: 3.13.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@visx/group': specifier: ^3.12.0 version: 3.12.0(react@19.2.4) @@ -51,17 +51,17 @@ importers: specifier: ^3.12.0 version: 3.12.0(react@19.2.4) '@xyflow/react': - specifier: ^12.10.0 - version: 12.10.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^12.10.1 + version: 12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) anser: - specifier: ^2.3.3 - version: 2.3.3 + specifier: ^2.3.5 + version: 2.3.5 axios: - specifier: ^1.13.5 - version: 1.13.5 + specifier: ^1.13.6 + version: 1.13.6 chakra-react-select: specifier: ^6.1.1 - version: 6.1.1(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.7)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 6.1.1(@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) chart.js: specifier: ^4.5.1 version: 4.5.1 @@ -75,17 +75,17 @@ importers: specifier: ^1.11.19 version: 1.11.19 elkjs: - specifier: ^0.11.0 - version: 0.11.0 + specifier: ^0.11.1 + version: 0.11.1 html-to-image: specifier: ^1.11.13 version: 1.11.13 i18next: - specifier: ^25.6.3 - version: 25.7.1(typescript@5.9.3) + specifier: ^25.8.14 + version: 25.8.14(typescript@5.9.3) i18next-browser-languagedetector: - specifier: ^8.2.0 - version: 8.2.0 + specifier: ^8.2.1 + version: 8.2.1 i18next-http-backend: specifier: ^3.0.2 version: 3.0.2 @@ -96,35 +96,35 @@ importers: specifier: ^19.2.4 version: 19.2.4 react-chartjs-2: - specifier: ^5.3.0 - version: 5.3.0(chart.js@4.5.1)(react@19.2.4) + specifier: ^5.3.1 + version: 5.3.1(chart.js@4.5.1)(react@19.2.4) react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) react-hook-form: - specifier: ^7.56.1 - version: 7.56.2(react@19.2.4) + specifier: ^7.71.2 + version: 7.71.2(react@19.2.4) react-hotkeys-hook: specifier: ^4.6.1 version: 4.6.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-i18next: specifier: ^15.5.1 - version: 15.5.1(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 15.5.1(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) react-icons: - specifier: ^5.5.0 - version: 5.5.0(react@19.2.4) + specifier: ^5.6.0 + version: 5.6.0(react@19.2.4) react-innertext: specifier: ^1.1.5 - version: 1.1.5(@types/react@19.2.7)(react@19.2.4) + version: 1.1.5(@types/react@19.2.14)(react@19.2.4) react-markdown: specifier: ^9.1.0 - version: 9.1.0(@types/react@19.2.7)(react@19.2.4) + version: 9.1.0(@types/react@19.2.14)(react@19.2.4) react-resizable-panels: specifier: ^3.0.6 version: 3.0.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-router-dom: - specifier: ^7.12.0 - version: 7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^7.13.1 + version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-syntax-highlighter: specifier: ^15.6.1 version: 15.6.1(react@19.2.4) @@ -132,17 +132,17 @@ importers: specifier: ^4.0.1 version: 4.0.1 use-debounce: - specifier: ^10.0.4 - version: 10.0.4(react@19.2.4) + specifier: ^10.1.0 + version: 10.1.0(react@19.2.4) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.2.4) yaml: - specifier: ^2.6.1 - version: 2.8.0 + specifier: ^2.8.2 + version: 2.8.2 zustand: - specifier: ^5.0.4 - version: 5.0.4(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@7nohe/openapi-react-query-codegen': specifier: ^1.6.2 @@ -154,53 +154,53 @@ importers: specifier: ^9.39.1 version: 9.39.1 '@playwright/test': - specifier: ^1.57.0 - version: 1.57.0 + specifier: ^1.58.2 + version: 1.58.2 '@stylistic/eslint-plugin': specifier: ^2.13.0 version: 2.13.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@tanstack/eslint-plugin-query': - specifier: ^5.91.2 - version: 5.91.2(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^5.91.4 + version: 5.91.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@trivago/prettier-plugin-sort-imports': specifier: ^4.3.0 - version: 4.3.0(prettier@3.7.4) + version: 4.3.0(prettier@3.8.1) '@types/node': specifier: ^24.10.1 version: 24.10.3 '@types/react': - specifier: ^19.2.7 - version: 19.2.7 + specifier: ^19.2.14 + version: 19.2.14 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.7) + version: 19.2.3(@types/react@19.2.14) '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 '@typescript-eslint/eslint-plugin': - specifier: ^8.49.0 - version: 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.49.0 - version: 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/utils': - specifier: ^8.49.0 - version: 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) '@vitejs/plugin-react': - specifier: ^5.1.2 - version: 5.1.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + specifier: ^5.1.4 + version: 5.1.4(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) '@vitejs/plugin-react-swc': - specifier: ^4.0.1 - version: 4.2.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + specifier: ^4.2.3 + version: 4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -223,8 +223,8 @@ importers: specifier: ^4.12.3 version: 4.15.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-prettier: - specifier: ^5.2.6 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.7.4) + specifier: ^5.5.5 + version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.8.1) eslint-plugin-react: specifier: ^7.37.5 version: 7.37.5(eslint@9.39.1(jiti@1.21.7)) @@ -232,8 +232,8 @@ importers: specifier: ^7.0.1 version: 7.0.1(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-react-refresh: - specifier: ^0.4.20 - version: 0.4.24(eslint@9.39.1(jiti@1.21.7)) + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-unicorn: specifier: ^55.0.0 version: 55.0.0(eslint@9.39.1(jiti@1.21.7)) @@ -241,20 +241,20 @@ importers: specifier: ^15.15.0 version: 15.15.0 happy-dom: - specifier: ^20.0.11 - version: 20.0.11 + specifier: ^20.8.3 + version: 20.8.3 jsonc-eslint-parser: specifier: ^2.4.0 version: 2.4.1 msw: - specifier: ^2.12.4 - version: 2.12.4(@types/node@24.10.3)(typescript@5.9.3) + specifier: ^2.12.10 + version: 2.12.10(@types/node@24.10.3)(typescript@5.9.3) openapi-merge-cli: specifier: ^1.3.2 version: 1.3.2 prettier: - specifier: ^3.7.4 - version: 3.7.4 + specifier: ^3.8.1 + version: 3.8.1 ts-morph: specifier: ^27.0.2 version: 27.0.2 @@ -262,17 +262,17 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.48.1 - version: 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) vite: - specifier: ^7.1.11 - version: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + specifier: ^7.3.1 + version: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + version: 3.5.2(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2) web-worker: specifier: ^1.5.0 version: 1.5.0 @@ -300,8 +300,8 @@ packages: resolution: {integrity: sha512-9K6xOqeevacvweLGik6LnZCb1fBtCOSIWQs8d096XGeqoLKC33UVMGz9+77Gw44KvbH4pKcQPWo4ZpxkXYj05w==} engines: {node: '>= 16'} - '@ark-ui/react@5.12.0': - resolution: {integrity: sha512-UV89EqyESZoyr6rtvrbFJn/FejpswhvRVcfK44dZDU6h6UY8CxfR/6Ayvrq9UtFdD0dEawqwWrXS22l8Y05Nnw==} + '@ark-ui/react@5.34.1': + resolution: {integrity: sha512-RJlXCvsHzbK9LVxUVtaSD5pyF1PL8IUR1rHHkf0H0Sa397l6kOFE4EH7MCSj3pDumj2NsmKDVeVgfkfG0KCuEw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -322,10 +322,18 @@ packages: resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.28.5': resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.17.7': resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} engines: {node: '>=6.9.0'} @@ -338,10 +346,18 @@ packages: resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + '@babel/helper-environment-visitor@7.24.7': resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} @@ -366,14 +382,24 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.3': resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} '@babel/helper-split-export-declaration@7.24.7': @@ -408,6 +434,10 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.26.10': resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} engines: {node: '>=6.0.0'} @@ -418,6 +448,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -434,30 +469,30 @@ packages: resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.26.9': - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.23.2': - resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.26.10': - resolution: {integrity: sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==} + '@babel/traverse@7.23.2': + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} engines: {node: '>=6.9.0'} '@babel/traverse@7.28.5': resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.17.0': resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} engines: {node: '>=6.9.0'} @@ -470,6 +505,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -477,8 +516,8 @@ packages: '@chakra-ui/anatomy@2.3.4': resolution: {integrity: sha512-fFIYN7L276gw0Q7/ikMMlZxP7mvnjRaWJ7f3Jsf9VtDOi6eAYIBRrhQe6+SZ0PGmoOkRaBc7gSE5oeIbgFFyrw==} - '@chakra-ui/react@3.20.0': - resolution: {integrity: sha512-zHYQAUqrT2pZZ/Xi+sskRC/An9q4ZelLPJkFHdobftTYkcFo1FtkMbBO0AEBZhb/6mZGyfw3JLflSawkuR++uQ==} + '@chakra-ui/react@3.34.0': + resolution: {integrity: sha512-VLhpVwv5IVxhwajO10KnS1VQT4hDqQMQP/A796Ya+uVu8AdoSX+5HHyTLTkYIeXIDMe0xLqJfov04OBKbBchJA==} peerDependencies: '@emotion/react': '>=11' react: '>=18' @@ -493,8 +532,8 @@ packages: '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@emotion/is-prop-valid@1.3.1': - resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} @@ -528,158 +567,158 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -696,10 +735,20 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@1.2.9': resolution: {integrity: sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -740,9 +789,18 @@ packages: '@floating-ui/core@1.7.1': resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/dom@1.7.1': resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} @@ -777,8 +835,12 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} - '@inquirer/confirm@5.1.8': - resolution: {integrity: sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -786,8 +848,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.1.9': - resolution: {integrity: sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -795,12 +857,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.11': - resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/type@3.0.5': - resolution: {integrity: sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -808,11 +870,11 @@ packages: '@types/node': optional: true - '@internationalized/date@3.8.1': - resolution: {integrity: sha512-PgVE6B6eIZtzf9Gu5HvJxRK3ufUFz9DhspELuhW/N0GuMGMTLvPQNRkHP2hTuP9lblOk+f+1xi96sPiPXANXAA==} + '@internationalized/date@3.11.0': + resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} - '@internationalized/number@3.6.2': - resolution: {integrity: sha512-E5QTOlMg9wo5OrKdHD6edo1JJlIoOsylh0+mbf0evi1tHJwMZfJSaBpGtnJV9N7w3jeiioox9EG/EWRWPh82vg==} + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -882,8 +944,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@mswjs/interceptors@0.40.0': - resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} engines: {node: '>=18'} '@open-draft/deferred-promise@2.2.0': @@ -895,8 +957,8 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@pandacss/is-valid-prop@0.53.6': - resolution: {integrity: sha512-TgWBQmz/5j/oAMjavqJAjQh1o+yxhYspKvepXPn4lFhAN3yBhilrw9HliAkvpUr0sB2CkJ2BYMpFXbAJYEocsA==} + '@pandacss/is-valid-prop@1.9.0': + resolution: {integrity: sha512-AZvpXWGyjbHc8TC+YVloQ31Z2c4j2xMvYj6UfVxuZdB5w4c9+4N8wy5R7I/XswNh8e4cfUlkvsEGDXjhJRgypw==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -906,16 +968,20 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.57.0': - resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} hasBin: true - '@rolldown/pluginutils@1.0.0-beta.47': - resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rolldown/pluginutils@1.0.0-beta.53': - resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} @@ -1048,68 +1114,68 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@swc/core-darwin-arm64@1.13.5': - resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} + '@swc/core-darwin-arm64@1.15.18': + resolution: {integrity: sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.13.5': - resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==} + '@swc/core-darwin-x64@1.15.18': + resolution: {integrity: sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.13.5': - resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==} + '@swc/core-linux-arm-gnueabihf@1.15.18': + resolution: {integrity: sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.13.5': - resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==} + '@swc/core-linux-arm64-gnu@1.15.18': + resolution: {integrity: sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.13.5': - resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} + '@swc/core-linux-arm64-musl@1.15.18': + resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.13.5': - resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} + '@swc/core-linux-x64-gnu@1.15.18': + resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.13.5': - resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} + '@swc/core-linux-x64-musl@1.15.18': + resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.13.5': - resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} + '@swc/core-win32-arm64-msvc@1.15.18': + resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.13.5': - resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==} + '@swc/core-win32-ia32-msvc@1.15.18': + resolution: {integrity: sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.13.5': - resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==} + '@swc/core-win32-x64-msvc@1.15.18': + resolution: {integrity: sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.13.5': - resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==} + '@swc/core@1.15.18': + resolution: {integrity: sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1120,22 +1186,26 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.19': + resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tanstack/eslint-plugin-query@5.91.2': - resolution: {integrity: sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw==} + '@tanstack/eslint-plugin-query@5.91.4': + resolution: {integrity: sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@tanstack/query-core@5.90.12': - resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} - '@tanstack/react-query@5.90.12': - resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + '@tanstack/react-query@5.90.21': + resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} peerDependencies: react: ^18 || ^19 @@ -1146,8 +1216,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.12': - resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + '@tanstack/react-virtual@3.13.19': + resolution: {integrity: sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1156,8 +1226,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.12': - resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tanstack/virtual-core@3.13.19': + resolution: {integrity: sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1167,8 +1237,8 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -1299,9 +1369,6 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@20.19.23': - resolution: {integrity: sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==} - '@types/node@24.10.3': resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} @@ -1311,9 +1378,6 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1327,11 +1391,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@18.3.19': - resolution: {integrity: sha512-fcdJqaHOMDbiAwJnXv6XCzX0jDW77yI3tJqYh1Byn8EL5/S628WRx9b/y3DnNe55zTukUQKrfYxiZls2dHcUMw==} - - '@types/react@19.2.7': - resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -1348,122 +1409,70 @@ packages: '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} - '@typescript-eslint/eslint-plugin@8.48.1': - resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.48.1 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.49.0': - resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.49.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.48.1': - resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==} + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.49.0': - resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.48.1': - resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.49.0': - resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.48.1': - resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.49.0': - resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.48.1': - resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==} + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.49.0': - resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.48.1': - resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.49.0': - resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/types@8.48.1': resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.49.0': - resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.48.1': - resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==} + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.49.0': - resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.48.1': - resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.49.0': - resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.48.1': - resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.49.0': - resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -1488,14 +1497,14 @@ packages: '@visx/vendor@3.12.0': resolution: {integrity: sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==} - '@vitejs/plugin-react-swc@4.2.2': - resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} + '@vitejs/plugin-react-swc@4.2.3': + resolution: {integrity: sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitejs/plugin-react@5.1.2': - resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + '@vitejs/plugin-react@5.1.4': + resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1538,224 +1547,243 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@xyflow/react@12.10.0': - resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==} + '@xyflow/react@12.10.1': + resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==} peerDependencies: react: '>=17' react-dom: '>=17' - '@xyflow/system@0.0.74': - resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} + '@xyflow/system@0.0.75': + resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==} + + '@zag-js/accordion@1.35.3': + resolution: {integrity: sha512-wmw6yo5Zr6ShiKGTc5ICEOJCurWAOSGubIpGISiHi3cZ4tlxKF/vpATIUT3eq8xzdB56YK57yKCujs/WmwqqoA==} + + '@zag-js/anatomy@1.35.3': + resolution: {integrity: sha512-oqU9iLNNylrtJMBX5Xu4DsxnPNvtZLiobryv2oNtsDI1mi1Fca/XHghQC9K5aYT0qNsmHj1M3W5WAWTaOtPLkQ==} - '@zag-js/accordion@1.15.0': - resolution: {integrity: sha512-EKNeuKx+lOQ/deCe/ApCjVPxpxpDwT2NXvMPL+YvqXmSv7hAnTLs9fDKjbDUQUMmsyx32BsBd8t6d17DL3rPXg==} + '@zag-js/angle-slider@1.35.3': + resolution: {integrity: sha512-HXRlmsbNEJSBT53fq9XQKL/vwZWwJC3nprskI7s4f/jy8a4uXPTlv7N7zuBYjew+ScTMzZah6fLWzUztBehmSg==} - '@zag-js/anatomy@1.15.0': - resolution: {integrity: sha512-r0l5I7mSsF35HdwXm22TppNhfVftFuqvKfHvTUw+wQZhni4eUL93HypJD0Fl7mDhtP5zfVGfBwR048OzD0+tCw==} + '@zag-js/aria-hidden@1.35.3': + resolution: {integrity: sha512-dk5POebn10WneQfLrEgbTzwolaXWpCSHL6F3jCTinW9IbOx7BXghzJD21iU5Iun+y9CorqJPW3p7LplYNUMO5Q==} - '@zag-js/angle-slider@1.15.0': - resolution: {integrity: sha512-xIZBa9V6d05uK7+XQVhfdsThqbZKimSYVxtMOWJfG0sKn63N9VGPxL1OtOMq7FA4IP3SyvlelsGt+3t82TUiyA==} + '@zag-js/async-list@1.35.3': + resolution: {integrity: sha512-SXX3wGzLK/maKS1PJ3XfLIGWbu0022f/OhcFsT1PbiHnoFZTH7h2fBhirrCBfy2TYFQ6r5uxgjkhPUNkuaeYnA==} - '@zag-js/aria-hidden@1.15.0': - resolution: {integrity: sha512-3ogglAasycekTHI34ph16mqwM+VtHCOMtrFHWzPwB16itV5oDEeeMNdQXenHSSyQ/07nJ2QsRGFFjGhPm1kWNg==} + '@zag-js/auto-resize@1.35.3': + resolution: {integrity: sha512-ufG8HSqzLd9h5rnos8aumj8iORlRskeR/gbpJu1NHrnHBWIrpuXm6KJJR2oZhTFY1BUMMk8eYIBA2QkVuiJzWA==} - '@zag-js/auto-resize@1.15.0': - resolution: {integrity: sha512-EXgrsU7OWxc7obSOt8Okh0144H8DQi1S84OsOUY04Uni11Dnp5/X8+t6mvBbkw4/Qyz5UBjChjocwBcO+HHV8w==} + '@zag-js/avatar@1.35.3': + resolution: {integrity: sha512-lbQ2Q4Va8AAScKULOHw2tCQez+0JRYGHSMFq6i+dJmeT3dlSgRanm69ra6K2po6hM9E4v6pRe+xOVE+9QMDnuA==} - '@zag-js/avatar@1.15.0': - resolution: {integrity: sha512-EHGxzXb1mLf3n6x0z/rqFl1mghDB/gyfPAeaFUoA/cacmmMk8YB3aDUXkS9pTgN9stYJBM5f6T4xB1ZUhrP8tg==} + '@zag-js/carousel@1.35.3': + resolution: {integrity: sha512-F+b8HzUeZfB+xUkAkLG4r0Ubui8pj7pSgZhi26ZiWgsM7tsd7cD+xRMXkvPEITN5Fd5QCe3KlVBuE00w5byjmg==} - '@zag-js/carousel@1.15.0': - resolution: {integrity: sha512-ZI9H34f2utdJ2Ek6GZa+iuRH4eC99GHD/VEOKLdGani8uadpT2v8M5kUwPGrlAJq9SiPbQ2UuXBmCkmurPQqdA==} + '@zag-js/cascade-select@1.35.3': + resolution: {integrity: sha512-Nifdx77hEuAdXqr1wpZSPjLXqygRhq/WvnPjGhCeSqFPpy62uT4JZ3avyjUZ4I0UhvIpkleUcXtFwQ3cSMh4ww==} - '@zag-js/checkbox@1.15.0': - resolution: {integrity: sha512-6lQvPQNJXt7R0xxdpOuh2qtmAkzdBdqSvFIH7fE6GJzJ/AWiRZh0X+9deLQ76CN4EDUdxizEe7MlQfTI3a56aw==} + '@zag-js/checkbox@1.35.3': + resolution: {integrity: sha512-8XBt/Wg2zSQWqV2ZFqZBQUjYRkOYHA2O3IEi0VVYtds3S1n7Pu/HqkZT5qDw+E/SY2+X9Uyx4hO7h2XrlsiZQQ==} - '@zag-js/clipboard@1.15.0': - resolution: {integrity: sha512-Q3kh0fHvOEAJUywQm3zAWyltrYyiI8OpeZQ18k5Mf3/M+bq3gSphZL0+AYsgGbKUg5O2+hJ1SfiErAjyhRtBQA==} + '@zag-js/clipboard@1.35.3': + resolution: {integrity: sha512-obTwynBpp6c17fLHe5tg//FQ497QsyCEry+K3bTdlrivWW200wvfHxZ6RKVbKwDAwhH+ye0bI1xkYAId8j7sdA==} - '@zag-js/collapsible@1.15.0': - resolution: {integrity: sha512-GX0kdMlKk4Yk5k/2wN0prudf21k+TfArGr4EHqimTDR0vQE3dSdb3pYyPjw20fLzceKHBBCLsoi2v+YnS75gHA==} + '@zag-js/collapsible@1.35.3': + resolution: {integrity: sha512-IweG8JOBCerJwLO6QzTZGEMlsYUmQfQSeD0jniFguMM8vcunvGVSrM+AaL8pDbmXd+snXokaGyJpGO3vzMW6Fw==} - '@zag-js/collection@1.15.0': - resolution: {integrity: sha512-oC3i6c/oP/FuNPsfgoC1reSXbAvDBGXl0HU3CcvXiNLHbjg2ek8J7kbow6MNuXK6chiksiOHbzKxHl2Oo0Ox7A==} + '@zag-js/collection@1.35.3': + resolution: {integrity: sha512-BYoWJ4b7ma2PgiuQbRSnP603f2DlK6se5JtViUHTamZScLLLWnWHuQ6zFa1KS5kiIkbb7CFM6/bJ3WNYLch8Ig==} - '@zag-js/color-picker@1.15.0': - resolution: {integrity: sha512-DGujS24h1OWkYL+TWyd+xukOO8NBgcSfFCINffa4ivkHtNx3nC28qkwLPRASbl7AK69pbrcuO6bx1Sy/JQJw0Q==} + '@zag-js/color-picker@1.35.3': + resolution: {integrity: sha512-i9roSgtqeA1b4Q+jWqnxjXB//BQXMP5m1FQ4YcZVq/0yT14A53JIknchuqrh3wC3yPsJMXFqCoKg+NET2+OVig==} - '@zag-js/color-utils@1.15.0': - resolution: {integrity: sha512-SKo+p5Fu0TBtdDua8UHVjptOkwLLBFoD499Z1FER/gr0R/97L03Kdir0YTxvKn5pXWXYY1EQn4hpTuTITN16lQ==} + '@zag-js/color-utils@1.35.3': + resolution: {integrity: sha512-vxkEVgz4YdSbdaPvjiRI1VsJAdwzu/dUNvzqOaiVcPDrHr/FFgmUbv0SOFjnfSb2QWGI8EDEMn02RW9ym+BzGw==} - '@zag-js/combobox@1.15.0': - resolution: {integrity: sha512-HBck3wcEeIOa7IQMsUkUKbm9cAU7bjoklIyq2zFGn90k7DcDa++oXK9Z2pmcd4TPoBYiyVuuXucaCcjmLX8V/Q==} + '@zag-js/combobox@1.35.3': + resolution: {integrity: sha512-s1qmttTGJTMjlDakL+uvWSEggpafKr1vhOeZCh8j+N4eFt9bLAwaffjuh/1JzWBvzovw7WoMVkizdTXPlN8oYg==} - '@zag-js/core@1.15.0': - resolution: {integrity: sha512-P/8F3IXabMhpFnc6hC7GDg3rvUnvY27cuZU04hxjUqTH6+SfORIA/Uvqd4ekhC+dIprL9jicnFrmGgcyelyxfQ==} + '@zag-js/core@1.35.3': + resolution: {integrity: sha512-fGAHyqOYSEFmo52t7wI4dvbFfLyJmUlyf7wknsiUlzUHlrn3yv5PAZYZ2TibpOD1hwXIp4AoCjbiIPPZBxirZw==} - '@zag-js/date-picker@1.15.0': - resolution: {integrity: sha512-IZD0V9MAljp1QhxYbST80AonryuDnyx7hvEy/RrBY/VOx6I4STtKfcSJ5ZZgVIzJfH8Yyaed4+IwcenqG7W5YQ==} + '@zag-js/date-picker@1.35.3': + resolution: {integrity: sha512-4G10h6pzzLbd84SE2CKtqi6Z9wEBhSyx4GRSxxy3tsf5wAxnz4anRFat9CGwn2YVUYcUJpD+umYgBMPt6zGDnA==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/date-utils@1.15.0': - resolution: {integrity: sha512-FX9EesJRnUTYTpbXf5EVfCbsXW5vYtZfc635aQzojc9ekk1FGcHpqQs8ZKfCOTPuauZFOX9i6139A4KoPfQOiw==} + '@zag-js/date-utils@1.35.3': + resolution: {integrity: sha512-1co0FPpZ6nO5dN8sZtECkMYaf+3E5zu0KSIJZpZiXb4TgsZMDyHu7K7IsiKFHk9qmhuF6AdPpNxBju91pSXMFg==} peerDependencies: '@internationalized/date': '>=3.0.0' - '@zag-js/dialog@1.15.0': - resolution: {integrity: sha512-Vlt5vySs4u8c8xBEh2JMUvRfPc+aaVEIIUtFVxpc2ORWhBXs9glijyp1yf3rNHJhjj8gqqhF5sEvs3yUTTAk+Q==} + '@zag-js/dialog@1.35.3': + resolution: {integrity: sha512-byosV+aBHH5LoFKnjEgC7WdqJid7bP9UhgWLSC7+IXbxrif9Czg1YVp6ZlQM6Nx6uD1vnty4touI3P7D7CTKcw==} + + '@zag-js/dismissable@1.35.3': + resolution: {integrity: sha512-XPk+lqmsZp2Z1yMb5K1yj/e7Sobv4D7zK66B1GS97lk9Xzz8vuSgsimcLy0p7RXQl3KL6H5L69inSuQa2exybQ==} + + '@zag-js/dom-query@1.35.3': + resolution: {integrity: sha512-1RbFZoT4CjlHN9TUNse1++ZVOyKo45ktucTIT349o6HMsoWWKmTJDPvFkMBbmu/qY6XXn4dT+LJEp4bL3DR+Qw==} - '@zag-js/dismissable@1.15.0': - resolution: {integrity: sha512-yv575KWy8gA1p4aajOiY5l/nBQ3Xw+Mrjpungp1+wiGd/98eNAIKJ6/adldfbE1Ygd/Q4Dx2VQ7D1AmiTdwUSw==} + '@zag-js/drawer@1.35.3': + resolution: {integrity: sha512-DN5bwa7bDCDaUSbNzFxMc2U/WmbLcXvPSQjyOpKI6CC3VbW2kKaOnjJ5qQG+W5YBO0FpmJBtaxRV7lke4sZH2w==} - '@zag-js/dom-query@1.15.0': - resolution: {integrity: sha512-z8H/j/Zs0eZEsGpbonScmlKSv0jEXKiAwUCrvQ9Mt6Gz9n0CQRM3MkFclSsM8aeiSv6qKLlhPfkzjl18OLkbgA==} + '@zag-js/editable@1.35.3': + resolution: {integrity: sha512-HcjeacS61vQXfNT9IalZj/+oS45yW5bIDO2NjJWV7zNe5AG29NCceUnvBhy+hrUKPnKcjfDocdW5rCL+Lvs/CQ==} - '@zag-js/editable@1.15.0': - resolution: {integrity: sha512-F14HKZuDsfkpfIkaF/ZDYPkz/pFf6VHrvoV0rdhj8wb8QJQ4nB+lgBv2APSwkEaFb/gGrnE19v3Ojlt5tqpPsw==} + '@zag-js/file-upload@1.35.3': + resolution: {integrity: sha512-oIYwnDct4ERo2mfmcxsBIJnlmpzjrzYx82SQsXWD3NGKx3cgdh2lwBX+ebItaLH1jkgzBa3z0TWxc6rfvcUXbw==} - '@zag-js/file-upload@1.15.0': - resolution: {integrity: sha512-2hAlQr9qdT8EH4XnmkNkEIDCCsmp2SMoMAjq6nJKYO8UJNQGRanU2B5S8jV3quJBz0vIY43SwyvqiZ3+1VrJSg==} + '@zag-js/file-utils@1.35.3': + resolution: {integrity: sha512-Tb05RCzx4swc156hd4jLiO7z+Gxg/HQ+JCds03jgTbrFJAz2D56YaMeI7gSDc1m4Xre3nyqQpSo9AeX5nzbE/w==} - '@zag-js/file-utils@1.15.0': - resolution: {integrity: sha512-tahJt3JmrXaOtGiknH5PxIiOyyNvroMfjiBqOqnNksIPzDoWmVNxHOEme/ts7dJlkRD8U2qm2NFC2VS0bKerzg==} + '@zag-js/floating-panel@1.35.3': + resolution: {integrity: sha512-nTZypcS0X46Oo1kpCQTnP5UlzjhypOAj3B4dq2z/3bAOC0TntYTnFkj8PbEJtExk7364xfMyxfgZOiv7Aqq01w==} - '@zag-js/floating-panel@1.15.0': - resolution: {integrity: sha512-AYYFseA1MeQUZl+zjNoKUu4j0kwz8EyJd4oJjs8uJIR6KG8u8QhpWYIBUny63M6AtZTCSYQAgBEcEh+mrbEyyQ==} + '@zag-js/focus-trap@1.35.3': + resolution: {integrity: sha512-evErLlGFdDVCI8xipNS5k0rAvO+KFRA9g273bbfWAL1+mT54mcB/XHa85nC3QpPgMNrSh+6LUNq9fapyOGoyYg==} - '@zag-js/focus-trap@1.15.0': - resolution: {integrity: sha512-N8m/JpNe1gHUPJlr0hyGUdHg6pAuyJKkBaX0s38cyVntlo2CJhyAWZGuUdocpT2Q3HNPql666FNnH986rYPDKQ==} + '@zag-js/focus-visible@1.35.3': + resolution: {integrity: sha512-g4F8PRGIoFoKBrHiQ1HQh5AjCS7brFRXHvpbDNb9+T11FGlF5Turb+6OVRoNV8MmiuqMltO2I28l36YsGc//uQ==} - '@zag-js/focus-visible@1.15.0': - resolution: {integrity: sha512-TPXBf47tj6L0hhZNl9AWhuLoVzfPaNPM+/Gw8t9l9Whvy6v9rk/rqUCidY5LsrQuPiKTi7s5WI5J+Wod8ib3gw==} + '@zag-js/highlight-word@1.35.3': + resolution: {integrity: sha512-K+mvEBbf3SUFjQeMeJQYb3cjri3x6sPaPhcKWayalelSLB/StWEGqcpmz+a6uUYrCUAK5kEi3Hn0YLGfn0GOig==} - '@zag-js/highlight-word@1.15.0': - resolution: {integrity: sha512-Rwr/rRm8BaF2xW9BAEJeA2wpFVx6HzoezfYQX7GFPPgw3N8nBMAYNjx+i1YIwIEcNyad2rbaBB+pSd2fZLIniA==} + '@zag-js/hover-card@1.35.3': + resolution: {integrity: sha512-xVoKOtvrnzhYzciZ1csgiV76IQ4DRtx1lsJeFSrfg5MH0kYWeC/pcmm3yCd2+Qh/45J7DbSXeZneqxpyiF5Vvw==} - '@zag-js/hover-card@1.15.0': - resolution: {integrity: sha512-j6BsE+metdnv/C/Ls0TZzAMN78rtS2r8M1ccHY5FFTGyUvZnlE8BY/QPNyCSSSCUpynymzMYh3IMYlxbJgfpSQ==} + '@zag-js/i18n-utils@1.35.3': + resolution: {integrity: sha512-k7UcNxbnC2jvGwCoHYAkFD3ZaRSMQNVHfuy8TujZQ+ci3IJovwgWLveZoRfFbXHkTLfhmbpE2tFXBdpwOVZutg==} - '@zag-js/i18n-utils@1.15.0': - resolution: {integrity: sha512-anxSbT8kLbJaFJFSb0Ork2j/Lp+XVfMNCIgiBR2BuqUlfX72k23TIJvRxAfwNIkUfs0L8ikaSgLss9OwS4mAnw==} + '@zag-js/image-cropper@1.35.3': + resolution: {integrity: sha512-1PH6bg8JAQESHzNqjka2TJ0QGNBGBAO6rb7AZ+9CaCCLw0pIzbUJhqPMkwd9GhdWGKGP+e7wFitnjcT4W5Js8g==} - '@zag-js/interact-outside@1.15.0': - resolution: {integrity: sha512-OwBf/iesQGU9Oq3xe/tcK7gu7xipiGWsmwl2CcScr0fTp3BIMbQywHS928IgPk1DxA8KTHodY8wBjoY1dskfRA==} + '@zag-js/interact-outside@1.35.3': + resolution: {integrity: sha512-tOcuo/IztzpU7UKXtjVrLZtXzzcbhP4n2WynKwDRkTkq3mRCp61xXJp1csIBycI3JHm/CMeAEcPdRIioxIT/Zw==} - '@zag-js/listbox@1.15.0': - resolution: {integrity: sha512-Gcg76uWZwUAyMFZzGWpHnFCU/aaquNbXmVnyzzBgE3Co2snkv02rK1yG9iBwemZe3e5+VBifMMAtLLPAQJdz+g==} + '@zag-js/json-tree-utils@1.35.3': + resolution: {integrity: sha512-nOv2dPJf+1mxsobYiSlYt96hR1MK7iHKG1iDLoO5wLggS6GQA3ix1BerHJK0zdehoEZ71R45el5ghCG1HB9VzQ==} - '@zag-js/live-region@1.15.0': - resolution: {integrity: sha512-Xy1PqLZD9AKzKuTKCMo9miL1Xizk/N8qFvj64iybBKUYnKr89/af3w7hRFqd2BDX+q3zrNxPp9rZ6L7MlOc7kA==} + '@zag-js/listbox@1.35.3': + resolution: {integrity: sha512-FE6FOuBr6aWtOb8U8oDvAvcUzD6JKLXAe8WngiLFG+b2yyW4nlaz2AcKRG1bjjB066UMxMo9/+2p4D0Kf5Id1Q==} - '@zag-js/menu@1.15.0': - resolution: {integrity: sha512-GbEBVYu0w7+88xrGX2GrjXfnwWuX5jLhoLiEcuxvxJQal/nahKrH4AGXJvHXNaRbj+53V3nWAh3u70C9210PWw==} + '@zag-js/live-region@1.35.3': + resolution: {integrity: sha512-64rWcfggYpyr2Fn4pdrB/lljMgm3quwn9is+vdDN85Vv3WShKWoz08T4njidm0hwcIbzas0bRqQYWDLLsAoSJQ==} - '@zag-js/number-input@1.15.0': - resolution: {integrity: sha512-+kK8kyXJhIAbEUnswoMDR+DSJUmvDNIOW0ffuZ9pbfukN3p6zaA3/dCp2Dtg3bQS7hGrFWgtrdejJ8l+mVvUAA==} + '@zag-js/marquee@1.35.3': + resolution: {integrity: sha512-bKZVpmAJWPDORP7WOWnS+65W5ZQBQmRs8zvV33ZfCpFbkXjhRiqKSzIj223/VOc2NEDjyWagz2vioAxrFYVzww==} - '@zag-js/pagination@1.15.0': - resolution: {integrity: sha512-Z62Q41fQPWqk59QyJk+9J0Ad3H9DCqZ0zZutI6iH8DdzT0A0xxmT6zhup6DM/8C8h0OLlaHFTWQnj0RdRNrnXg==} + '@zag-js/menu@1.35.3': + resolution: {integrity: sha512-KyY0EZXkIU57Mjt+Lg+pupiePk3LcnQcB3Gl05Vva61bNjBjdKV71qwCQru/OxPZEwYgPo46L7TDIb56kfK/VQ==} - '@zag-js/password-input@1.15.0': - resolution: {integrity: sha512-oHuZKDRJIbycqWpTVznufy4L7K2g8kwcEaZ4runkwO2ocF00zP8HVmOZQzmhkUgTny0azErQydg8XE0VR5OfYg==} + '@zag-js/navigation-menu@1.35.3': + resolution: {integrity: sha512-8cCHx0X/KjEpr2BaMOxJS5LiA6fs/CNqVTF/sTTgZAv7Dm+MH0yNuKm4kpPvcLaVeBpVE09bnyCHrNKzZes+Fw==} - '@zag-js/pin-input@1.15.0': - resolution: {integrity: sha512-IykjogZBG+BfbFXymSa+KGpOi5CrV9kl8HRm6G2V2Sr3NA5jEwMFaGSd/QrcHS9vh23D1Smx/io4pvF7c3q0kg==} + '@zag-js/number-input@1.35.3': + resolution: {integrity: sha512-uqawVybAcLcefVEHMVONuAA5kDSDPP5TsROr5PnAyFlhM1iD85+r3KAfCueoDX5w2X4ibbu9o2tdV6zTFKD/nQ==} - '@zag-js/popover@1.15.0': - resolution: {integrity: sha512-cdzEed3zcGbjSgPQnQnrsuXo2hVVslmSNwQbU5dHcNzG1uxxmtPCIMVeBUmGyJbAFF5XQpKCq/7mIr26dT73vw==} + '@zag-js/pagination@1.35.3': + resolution: {integrity: sha512-fKm4s5KAd12RiCI/EDmmGKjPQ+i2qS/UsJPdMe65yb/4mY5OibwV2zyHcVeFsOD4gBZpnU6kYlDAGSttmLWLlQ==} - '@zag-js/popper@1.15.0': - resolution: {integrity: sha512-Ra/0Ko423KN+8D4+mIFFkeTn9uaHfpxn6UUNIWwZKoiJQvED8DH4dPbLbmvGEoKp6qmisnRHAzi71NLgEhk0Mw==} + '@zag-js/password-input@1.35.3': + resolution: {integrity: sha512-etd0gm6ELAm3y+cFhPU+TYm8khm9cL5Mg5m2DcZxu1Mqpj7JY0LsXZ8SFOdCZgTIHuMEhKBiYfnuyMAd4CJztA==} - '@zag-js/presence@1.15.0': - resolution: {integrity: sha512-hoxXis50pm79PpkY2kA1wdhh4AEo7t7pBv0VsQYZYjmzuFh4V5IMw9oa1EOfBlC6f/A+EMZ9E+xg+EVsB68a8w==} + '@zag-js/pin-input@1.35.3': + resolution: {integrity: sha512-ZFt+WIHMdVlSg29BrQLFq5ijabiUO3tXMhoKhjjzTSe/tLqfNeu3UxFB6y/FYpn8+Cvn6xwvhu3lgnORYmI0zQ==} - '@zag-js/progress@1.15.0': - resolution: {integrity: sha512-/Mz26GR2rOAuoErNOiSGRpvwckTmbCD5nWGDE/aYlVRID13HcsmN15Zk2Jfa4LadqK88aIN8Iy0Sk4elG0+Efw==} + '@zag-js/popover@1.35.3': + resolution: {integrity: sha512-+MIEENPsbKPxzoNuDI/C5d5ZN9uxnfZ+MBDc5C5XSgjjg9FcvMXClNq7IFM1aZi24peRXg9cMNf//lApVRT37w==} - '@zag-js/qr-code@1.15.0': - resolution: {integrity: sha512-GkGy5k5tk6DIui9lGjDO8+e8TsSVOxEGp1lblPiaRm1ggIh10GhIfCQWGe/x78ezdie8WzxlSrma89suTpaiAQ==} + '@zag-js/popper@1.35.3': + resolution: {integrity: sha512-gpB7Xn9WtlfrUsIVbSgNQGDwgNOL/cSGt0Id3wEQKArmqVC704EWtPvXzOMMybBEdm8YW2hQrXuo+o66abI1Sg==} - '@zag-js/radio-group@1.15.0': - resolution: {integrity: sha512-+KTebHUtMsE/YDyGE8wF5VnWfZQp+f2WoAwwzBjfhPpRxXbOUMDo0pZEEr3yxkSvQ9hgCcBhMKH8pEk0SPxvjQ==} + '@zag-js/presence@1.35.3': + resolution: {integrity: sha512-ev5E7+U9IZAGvEaflpdVLHaZl8ZaQMhGB3ypd0yKhPwXeM51obV8w3+5HjzTqHPl8TKuoHWL31YaiUBd5EuS6w==} - '@zag-js/rating-group@1.15.0': - resolution: {integrity: sha512-omGKN97FhplFwBX9J/Mj7BCZuwFXSXssSVTKU7Yp2d1Cmxhez4+Ju7KdSRNnIoWB4OxFCxwZyaAPTcg3E0Pjrg==} + '@zag-js/progress@1.35.3': + resolution: {integrity: sha512-u0GxQN1AfXMAgzYOUMxKQA12DyuAP0svh2S//KvOorTSv7d5hAa8nZXi2cEv5abYsyfKJ6/bc1Z56byzW1jVZw==} - '@zag-js/react@1.15.0': - resolution: {integrity: sha512-YSp9QBkdeBfZt4nVhJW+CUd5sNEEVAuwkmoZWDFUoDoWSAXwzSKuHCmTm5/8DaXg1IZD2bMrXgMNDqZv2x0hZw==} + '@zag-js/qr-code@1.35.3': + resolution: {integrity: sha512-t0Ehwogr49vTNtWyNdQU2tYex7uJyfAn7N/5LgD7FXw8aa+RBMWZWlqjCUvHqJ929tVMrn+LIrQnZCcwNunalA==} + + '@zag-js/radio-group@1.35.3': + resolution: {integrity: sha512-kOzocjqWk3dXuRfyfsHwfw63Z99NHbc7rvVUutSsfXANXi+DFYZHuqdPUwMt+29LfaL15XTOfuGV+yUXDCgQHQ==} + + '@zag-js/rating-group@1.35.3': + resolution: {integrity: sha512-BmhJZdbaTnd3nFWMY+nR+HF952UhWXfaXXxiBWptSLMBfAYImQTWBMrLgTHCSnVfmFATj4Gb7xQe79FQU8T5fA==} + + '@zag-js/react@1.35.3': + resolution: {integrity: sha512-x2PxYUCQ6OgOpUdmSkG5tbL9JWVqYRh42r4V2UeAdMh0MRwjAJtxjvAy50DZ8Sfia5o4UGdZMXJyDY2O7Pdhyw==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' - '@zag-js/rect-utils@1.15.0': - resolution: {integrity: sha512-sjAn78x1t3XiDG3NT8SoFfyO0u7/SEJU5RKRhMgjTPoOLXTzZj+lu2d5N4cUw0uZTfeGb/ormObSchMQVhFgYQ==} + '@zag-js/rect-utils@1.35.3': + resolution: {integrity: sha512-mt/oD3RXdyaX6ZPSd8BO13vvPBJ7QpVWieubE3O0WM3OPhU7ykDMRp/tR7cYMQrzUm04GlY9pbkmSSw2uABxlA==} - '@zag-js/remove-scroll@1.15.0': - resolution: {integrity: sha512-vdWSAdgY8wJ7s4YeaKwTMwmZiRMBxCehmdktSxBWvwtAjU1cM3UWvjmZ9E6INJrQXxH9vDpe/rpFSyv1guIQIw==} + '@zag-js/remove-scroll@1.35.3': + resolution: {integrity: sha512-e59z9SbEpPiw0qwNQa2cB5/h30ZCLREaHsCw1TKTANFhwg7v85k9Lq1H/G/49li1CAjmiaOU9BNGlDvbzpNETQ==} - '@zag-js/scroll-snap@1.15.0': - resolution: {integrity: sha512-/LfBlsjoR4tVL3Djus3k9jKLhwC2ApdHTACxEc72TAewoPe4M8icnSDLXmKHvwwOhzK0HlFz8wGm6ZncAbQbuA==} + '@zag-js/scroll-area@1.35.3': + resolution: {integrity: sha512-IQwdUws/AckRIHK1z/wHdHurnOeGd8h8Dmspfh3VT7NkwTnxeJ4SW9di9smuD+d25eXkJRuX5zGEDHAyx2IaPQ==} - '@zag-js/select@1.15.0': - resolution: {integrity: sha512-4urUBADzhrsGEO/UsqHdjsgmDdF15Zzeid3ejEbIMTrkt2/mMMcQ1CShuxtsWqm2EUBz/N1kOcZlE6Tq69n7Xg==} + '@zag-js/scroll-snap@1.35.3': + resolution: {integrity: sha512-NVa2yRm2DQnF6hTV9k7Xz7l8YCZBagZTiqSwNvWKUulKD1csjt2fpBxvUt2cK+1iQnLOey2ydhs7MMsAnXPbJA==} - '@zag-js/signature-pad@1.15.0': - resolution: {integrity: sha512-5Tj8vkrRxEkSV417oR2qdy+TRgDmS3W8dY7xsIjpbBf/kqkt/8Uo4JpaVH2vwQAFw9AwEFogBh9i6dHcXMy0rA==} + '@zag-js/select@1.35.3': + resolution: {integrity: sha512-ztszGHWvlbBDE0YT5LYPH+sMd6VH1ct5pH/M9VSzIUO6C5PARkW0NwSVQ1rCQJMj4sfvSE1gC1/r7urRzqEcUQ==} - '@zag-js/slider@1.15.0': - resolution: {integrity: sha512-NYIsn3GKXIoPmvkDXsQmw9wdYg3QHbYHXnZ8Ewl2fVubN7S5mDlHSZs2iDVsBvX+a4RChWFRO6JHX8E1+BncOg==} + '@zag-js/signature-pad@1.35.3': + resolution: {integrity: sha512-jvtxxzAQ8fre11zWUh6HflG4Ycr5z83Wba4pONRJbUE/vNgkJQ7yJgfyUl1QTlkn8Arfg2Zwoxu9GIq80HLZWg==} - '@zag-js/splitter@1.15.0': - resolution: {integrity: sha512-Xnedl+cpnD/hv9m+GOYCK5K2xRxbs4xuP/EajYtgVcDw8E1X5cBmxHa1hCrp7BMgb2xYCvZ5et4hnmZfb+1X9g==} + '@zag-js/slider@1.35.3': + resolution: {integrity: sha512-Th142JO4Fqla5AWhGrTW6CQicwvTw87PdVpur/WotQ7brlZIww5HipzEMh5eQJSWfwpKD4PI2bYK9V/ZE/mpXA==} - '@zag-js/steps@1.15.0': - resolution: {integrity: sha512-VoIDcDIEErZawmW2m0yTGlffqjfRuSwR37K9LdSRy8Q4Qzz3wV7jASaTjMhTya1hlreJ7tJg+Qbjqowvw9GndA==} + '@zag-js/splitter@1.35.3': + resolution: {integrity: sha512-IsIbRwzjr5amGANEDsZDSToaSn8wHUWvS2l0XHmf3BiiguVApaZgQTlfqthVQC9hBHMOaGIXIW1CFUOrQYkvUQ==} - '@zag-js/store@1.15.0': - resolution: {integrity: sha512-ecqjcy3b1GsULpsT8RVJV9KDaikajRN0XRg48HMvaGkaPIvxI6esyrE6RKnShuqr2eVXIPghgBnCnrJUev4UlA==} + '@zag-js/steps@1.35.3': + resolution: {integrity: sha512-TYIrqV+v9/ULhvrTRBtQFFvJQPPTWOmjFXxlIxDwozek5R4dCIyeUYt1/ChJEc2mNETocbfDVSTxRO1dwCFpwQ==} - '@zag-js/switch@1.15.0': - resolution: {integrity: sha512-2CaAUTi7jM4lJjCYoSE1HWlFPCifI5GR+hufWOCYKpanf8VA/LM+t/a2Aq5QoBsWdcQv3B9mHxF/aVTDbnCKPQ==} + '@zag-js/store@1.35.3': + resolution: {integrity: sha512-7kEV4T/20DU36UIfVMzuDlLhWSSEy/vabmpiB700tcdD9BBBODTiSg3ZeljW17dQbvE545vZOFEjVf/cQ5LVGA==} - '@zag-js/tabs@1.15.0': - resolution: {integrity: sha512-voHWpibC1TKLmbAJfixOesxrCio7wK+gdLRvh7Xh5u+3VSsT2fP2wEw3ySkJbpw3MpEE7R2OWkInbCV/SwPcsA==} + '@zag-js/switch@1.35.3': + resolution: {integrity: sha512-EP/2cJ46sd+6C5x5+89jn/9NOpM05CRESYB4RMhOnTe/WFtcS4IpiYtVHFhikdXkvJoibm67O2EHep2Pm/Xj4w==} - '@zag-js/tags-input@1.15.0': - resolution: {integrity: sha512-CB60z+/I/Nso1gwatTO1qrk4XITxDd4qtRD+l6fuuKyOkZGgKm0AP0W+/6qUuOvtWIuY6fas3yZHFmF2eEZ9vQ==} + '@zag-js/tabs@1.35.3': + resolution: {integrity: sha512-lZKlDmxE25miCikj9QZCCnL02SVV2K14KZy5bn7+XDgrWlfSNTpNTj8r5E3zGlSgio5pkTGou57ASqS7WaPDWg==} - '@zag-js/time-picker@1.15.0': - resolution: {integrity: sha512-4S02433X88X3MW/BxaFJiWna4BIRXsAdrmDcBb0PZ8dln29DUmpD8YHcFtONsKvmCAmrbO7Gr65n86nQwK8zeg==} - peerDependencies: - '@internationalized/date': '>=3.0.0' + '@zag-js/tags-input@1.35.3': + resolution: {integrity: sha512-HqyoQ3DZFhByOGnDShFfxi6u0bIf7aSVTlwmAvcL+b2ZhyU6/wIMGc4WJE7BMx1NYWM/jNLHedvGExAI8R0kXQ==} - '@zag-js/timer@1.15.0': - resolution: {integrity: sha512-gDsYm4C9yju7g/r5u7n7mRQ2UY7diXXVbbLFr5Ja+0iUXgbD+uoSZEt9HypVc5TL9NWEEwn5/tut36owEeW4rw==} + '@zag-js/timer@1.35.3': + resolution: {integrity: sha512-edmgitbRgsq+msxvVB4wc17Q5d5k63zMWaLJnWjUdDGAgEtM6/HNxwGb3riv46S2U3RgYxaaHTNZ/M7EE5mvYw==} - '@zag-js/toast@1.15.0': - resolution: {integrity: sha512-0RupMCXyGr7/La4Zlei7VqBF0VPNJelGd7zimLboe+IKZyy4Ypi/N2IX14rl8JZQDsDEgkLUl33xrSk/9RW2nQ==} + '@zag-js/toast@1.35.3': + resolution: {integrity: sha512-whlR791GHdnMD21nNPsl2Dbql8+qu1wBZl75QzwYrjR8FlKjp8bhr3gXKzQEddcBXe9GPEFGvUs4iCyXsuTbpg==} - '@zag-js/toggle-group@1.15.0': - resolution: {integrity: sha512-992vMz/2sriLrUKI3LpT/01kCGTbPGLgGLibiHRt562i0v9+2tV+GiY2jBctHZjJaKPrzBY3H0l8CCCvDj8gng==} + '@zag-js/toggle-group@1.35.3': + resolution: {integrity: sha512-Gn6JHzkQ4tlttjZcE0ZjIdxYkFeVp9VHrcMVizjJTkGZRmQ+kPZ5G/wOsZhIrvLX3Dw6Y0NkuBcP+jDHz/o3TA==} - '@zag-js/toggle@1.15.0': - resolution: {integrity: sha512-mMSQ1+f1hOMp/7gLA7rTeiSNyeZxsCjRxP4XnTBY4BxJ5LswLuhem9CplBwaVthkhY1Y/5f3HHu80LBcfF+BVQ==} + '@zag-js/toggle@1.35.3': + resolution: {integrity: sha512-aFfHKuR4sKzglhkmWLA+0RTNPs9dfeqwtc96qljawGYfAYWJXkEPYK9dFfVa+arZ7L84xBi24QSLiTg7LGSFLw==} - '@zag-js/tooltip@1.15.0': - resolution: {integrity: sha512-sOpVECyfdS4RZBx46mSV+RPc9C5k9JvYQYUfoOVWh0E5RLSEz5bQm5xxctKOHfCOv+vJNTfG5gP596B1r2+Fkw==} + '@zag-js/tooltip@1.35.3': + resolution: {integrity: sha512-/pImDGYl79MfLdvEphj3rSvNdj2tLW4GwGEncgdLM/GKwQiEUjfi/9EJOfLYP23M4lOOnoW7orehJ9xeaXOAkA==} - '@zag-js/tour@1.15.0': - resolution: {integrity: sha512-EplcxoiE0z9vI0z6675+ABclQ9Mi1YUWhDZOHx7wfjRzpfawmJoBAlNDKzK3wc801d6OxgJx69SPj7ac0BwwwA==} + '@zag-js/tour@1.35.3': + resolution: {integrity: sha512-DI2aCXmZaE9KcPZDs9itc2BO7ixLApJ/yVRfM69pXwVOrucdSeDDNPFkfbhj5XwB+9VjjZEkqWFHKntRIyPl5g==} - '@zag-js/tree-view@1.15.0': - resolution: {integrity: sha512-wqdd+hu1bDOCWtnZ8MarRFHqbZF2t8qKBM3kO42IBq7jTI/93LCkHSlceEPft9dgZ6Ea9km0YJMHhoTqCPZ/fw==} + '@zag-js/tree-view@1.35.3': + resolution: {integrity: sha512-DbHaLxSNa1goE3o3IsXxEdzp8P5dvmkk1rVWgNUUIhpA+44idEjSSNXJkHPl18Mk5blqSMVjK1EX91oqai01Vw==} - '@zag-js/types@1.15.0': - resolution: {integrity: sha512-lV2ov2M07BlmjDUCSwBeHxPApHI3oAiLytG94AqcYvQ0BtsCRo5T60yRQ0syFc6fHf0e9+kwt89uoIgfGFYfmw==} + '@zag-js/types@1.35.3': + resolution: {integrity: sha512-Fnm3AMs1lfb55hlkip/eJeWHOjFB3gSi1JkZlkkdltG2l7y/zsHkumPSe6jIKy+DRRIFKRCyXVTatbPN27bO3w==} - '@zag-js/utils@1.15.0': - resolution: {integrity: sha512-XctFny5H8C00BsougV40Yp0qVEj9M2d/NRme7B33mon9wG+3hscZwP6miJmF6BYI5Pgu6e2P0Sv45FddQU1Tkg==} + '@zag-js/utils@1.35.3': + resolution: {integrity: sha512-LHcC+9y6TFhDsIz9I3koYxONl2JFfx5yQDzc6ZEQO2cqzXedRcN0R9IPqNGCX7JuhGt14ctDkVCm1JWGP2J6Wg==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1775,19 +1803,15 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - anser@2.3.3: - resolution: {integrity: sha512-QGY1oxYE7/kkeNmbtY/2ZjQ07BCG3zYdz+k/+sf69kMzEIxb93guHkPnIXITQ+BYi61oQwG74twMOX1tD4aesg==} - - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + anser@2.3.5: + resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -1798,8 +1822,8 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} anymatch@3.1.3: @@ -1876,8 +1900,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1896,9 +1920,14 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.3: - resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} - engines: {node: 20 || >=22} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -1910,9 +1939,9 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.2: - resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} - engines: {node: 20 || >=22} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1923,6 +1952,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -1962,6 +1996,9 @@ packages: caniuse-lite@1.0.30001707: resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2119,9 +2156,6 @@ packages: css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2233,6 +2267,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.1.0: resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} @@ -2302,8 +2345,11 @@ packages: electron-to-chromium@1.5.123: resolution: {integrity: sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==} - elkjs@0.11.0: - resolution: {integrity: sha512-u4J8h9mwEDaYMqo0RYJpqNMFDoMK7f+pu4GjcV+N8jIC7TRdORgzkfSjTJemhqONFfH6fBI3wpysgWbhgVWIXw==} + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + elkjs@0.11.1: + resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2311,6 +2357,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -2352,8 +2402,8 @@ packages: es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true @@ -2418,8 +2468,8 @@ packages: peerDependencies: eslint: '>=8.45.0' - eslint-plugin-prettier@5.5.4: - resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -2438,10 +2488,10 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-refresh@0.4.24: - resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} peerDependencies: - eslint: '>=8.40' + eslint: ^9 || ^10 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -2471,6 +2521,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.1: resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2539,9 +2593,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fault@1.0.4: resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} @@ -2691,8 +2742,8 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql@16.12.0: - resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + graphql@16.13.1: + resolution: {integrity: sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} handlebars@4.7.8: @@ -2700,8 +2751,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.0.11: - resolution: {integrity: sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==} + happy-dom@20.8.3: + resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==} engines: {node: '>=20.0.0'} has-bigints@1.1.0: @@ -2776,14 +2827,14 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - i18next-browser-languagedetector@8.2.0: - resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==} + i18next-browser-languagedetector@8.2.1: + resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} i18next-http-backend@3.0.2: resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==} - i18next@25.7.1: - resolution: {integrity: sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw==} + i18next@25.8.14: + resolution: {integrity: sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3113,8 +3164,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.5: - resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -3303,6 +3354,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} @@ -3316,8 +3371,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msw@2.12.4: - resolution: {integrity: sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg==} + msw@2.12.10: + resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -3366,6 +3421,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -3482,9 +3540,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3506,8 +3564,8 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - perfect-freehand@1.2.2: - resolution: {integrity: sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==} + perfect-freehand@1.2.3: + resolution: {integrity: sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3527,13 +3585,13 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} hasBin: true - playwright@1.57.0: - resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} engines: {node: '>=18'} hasBin: true @@ -3545,20 +3603,20 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -3595,8 +3653,8 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - react-chartjs-2@5.3.0: - resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} + react-chartjs-2@5.3.1: + resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} peerDependencies: chart.js: ^4.1.1 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3606,8 +3664,8 @@ packages: peerDependencies: react: ^19.2.4 - react-hook-form@7.56.2: - resolution: {integrity: sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==} + react-hook-form@7.71.2: + resolution: {integrity: sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -3634,8 +3692,8 @@ packages: typescript: optional: true - react-icons@5.5.0: - resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + react-icons@5.6.0: + resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} peerDependencies: react: '*' @@ -3667,15 +3725,15 @@ packages: react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-router-dom@7.12.0: - resolution: {integrity: sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==} + react-router-dom@7.13.1: + resolution: {integrity: sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.12.0: - resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} + react-router@7.13.1: + resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -3776,8 +3834,8 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true - rettime@0.7.0: - resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -3815,6 +3873,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -3948,8 +4011,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} strip-indent@3.0.0: @@ -3980,6 +4043,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + synckit@0.11.8: resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4018,11 +4085,11 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} - tldts-core@7.0.19: - resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + tldts-core@7.0.25: + resolution: {integrity: sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==} - tldts@7.0.19: - resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + tldts@7.0.25: + resolution: {integrity: sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==} hasBin: true to-fast-properties@2.0.0: @@ -4046,8 +4113,8 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4065,10 +4132,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -4077,8 +4140,8 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - type-fest@5.3.0: - resolution: {integrity: sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==} + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} engines: {node: '>=20'} typed-array-buffer@1.0.3: @@ -4097,11 +4160,11 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.48.1: - resolution: {integrity: sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==} + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' typescript@5.9.3: @@ -4121,9 +4184,6 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -4154,6 +4214,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uqr@0.1.2: resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} @@ -4163,8 +4229,8 @@ packages: urijs@1.19.11: resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} - use-debounce@10.0.4: - resolution: {integrity: sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==} + use-debounce@10.1.0: + resolution: {integrity: sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==} engines: {node: '>= 16.0.0'} peerDependencies: react: '*' @@ -4178,8 +4244,8 @@ packages: '@types/react': optional: true - use-sync-external-store@1.4.0: - resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4208,8 +4274,8 @@ packages: peerDependencies: vite: '>2.0.0-0' - vite@7.2.6: - resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4341,6 +4407,18 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4360,8 +4438,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -4377,8 +4455,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} zod-validation-error@4.0.2: @@ -4390,8 +4468,8 @@ packages: zod@4.2.1: resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} - zustand@4.5.6: - resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} peerDependencies: '@types/react': '>=16.8' @@ -4405,8 +4483,8 @@ packages: react: optional: true - zustand@5.0.4: - resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==} + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -4451,66 +4529,73 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 - '@ark-ui/react@5.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@internationalized/date': 3.8.1 - '@zag-js/accordion': 1.15.0 - '@zag-js/anatomy': 1.15.0 - '@zag-js/angle-slider': 1.15.0 - '@zag-js/auto-resize': 1.15.0 - '@zag-js/avatar': 1.15.0 - '@zag-js/carousel': 1.15.0 - '@zag-js/checkbox': 1.15.0 - '@zag-js/clipboard': 1.15.0 - '@zag-js/collapsible': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/color-picker': 1.15.0 - '@zag-js/color-utils': 1.15.0 - '@zag-js/combobox': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/date-picker': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/date-utils': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/dialog': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/editable': 1.15.0 - '@zag-js/file-upload': 1.15.0 - '@zag-js/file-utils': 1.15.0 - '@zag-js/floating-panel': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/highlight-word': 1.15.0 - '@zag-js/hover-card': 1.15.0 - '@zag-js/i18n-utils': 1.15.0 - '@zag-js/listbox': 1.15.0 - '@zag-js/menu': 1.15.0 - '@zag-js/number-input': 1.15.0 - '@zag-js/pagination': 1.15.0 - '@zag-js/password-input': 1.15.0 - '@zag-js/pin-input': 1.15.0 - '@zag-js/popover': 1.15.0 - '@zag-js/presence': 1.15.0 - '@zag-js/progress': 1.15.0 - '@zag-js/qr-code': 1.15.0 - '@zag-js/radio-group': 1.15.0 - '@zag-js/rating-group': 1.15.0 - '@zag-js/react': 1.15.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@zag-js/select': 1.15.0 - '@zag-js/signature-pad': 1.15.0 - '@zag-js/slider': 1.15.0 - '@zag-js/splitter': 1.15.0 - '@zag-js/steps': 1.15.0 - '@zag-js/switch': 1.15.0 - '@zag-js/tabs': 1.15.0 - '@zag-js/tags-input': 1.15.0 - '@zag-js/time-picker': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/timer': 1.15.0 - '@zag-js/toast': 1.15.0 - '@zag-js/toggle': 1.15.0 - '@zag-js/toggle-group': 1.15.0 - '@zag-js/tooltip': 1.15.0 - '@zag-js/tour': 1.15.0 - '@zag-js/tree-view': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@ark-ui/react@5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@internationalized/date': 3.11.0 + '@zag-js/accordion': 1.35.3 + '@zag-js/anatomy': 1.35.3 + '@zag-js/angle-slider': 1.35.3 + '@zag-js/async-list': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/avatar': 1.35.3 + '@zag-js/carousel': 1.35.3 + '@zag-js/cascade-select': 1.35.3 + '@zag-js/checkbox': 1.35.3 + '@zag-js/clipboard': 1.35.3 + '@zag-js/collapsible': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/color-picker': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/combobox': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-picker': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dialog': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/drawer': 1.35.3 + '@zag-js/editable': 1.35.3 + '@zag-js/file-upload': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/floating-panel': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/highlight-word': 1.35.3 + '@zag-js/hover-card': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/image-cropper': 1.35.3 + '@zag-js/json-tree-utils': 1.35.3 + '@zag-js/listbox': 1.35.3 + '@zag-js/marquee': 1.35.3 + '@zag-js/menu': 1.35.3 + '@zag-js/navigation-menu': 1.35.3 + '@zag-js/number-input': 1.35.3 + '@zag-js/pagination': 1.35.3 + '@zag-js/password-input': 1.35.3 + '@zag-js/pin-input': 1.35.3 + '@zag-js/popover': 1.35.3 + '@zag-js/presence': 1.35.3 + '@zag-js/progress': 1.35.3 + '@zag-js/qr-code': 1.35.3 + '@zag-js/radio-group': 1.35.3 + '@zag-js/rating-group': 1.35.3 + '@zag-js/react': 1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@zag-js/scroll-area': 1.35.3 + '@zag-js/select': 1.35.3 + '@zag-js/signature-pad': 1.35.3 + '@zag-js/slider': 1.35.3 + '@zag-js/splitter': 1.35.3 + '@zag-js/steps': 1.35.3 + '@zag-js/switch': 1.35.3 + '@zag-js/tabs': 1.35.3 + '@zag-js/tags-input': 1.35.3 + '@zag-js/timer': 1.35.3 + '@zag-js/toast': 1.35.3 + '@zag-js/toggle': 1.35.3 + '@zag-js/toggle-group': 1.35.3 + '@zag-js/tooltip': 1.35.3 + '@zag-js/tour': 1.35.3 + '@zag-js/tree-view': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -4534,6 +4619,8 @@ snapshots: '@babel/compat-data@7.28.5': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -4554,6 +4641,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.17.7': dependencies: '@babel/types': 7.17.0 @@ -4562,7 +4669,7 @@ snapshots: '@babel/generator@7.26.10': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.26.10 '@babel/types': 7.26.10 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 @@ -4576,6 +4683,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.5 @@ -4584,13 +4699,21 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-environment-visitor@7.24.7': dependencies: '@babel/types': 7.26.10 '@babel/helper-function-name@7.24.7': dependencies: - '@babel/template': 7.26.9 + '@babel/template': 7.28.6 '@babel/types': 7.26.10 '@babel/helper-globals@7.28.0': {} @@ -4601,8 +4724,8 @@ snapshots: '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.26.10 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -4613,6 +4736,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -4622,7 +4752,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-split-export-declaration@7.24.7': dependencies: @@ -4645,6 +4784,11 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.28.5 + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.26.10': dependencies: '@babel/types': 7.26.10 @@ -4653,27 +4797,25 @@ snapshots: dependencies: '@babel/types': 7.28.5 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + '@babel/parser@7.29.0': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.29.0 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/runtime@7.26.10': dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.28.4': {} - - '@babel/template@7.26.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/runtime@7.28.6': {} '@babel/template@7.27.2': dependencies: @@ -4681,6 +4823,12 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@babel/traverse@7.23.2': dependencies: '@babel/code-frame': 7.26.2 @@ -4696,18 +4844,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.26.10': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.10 - '@babel/parser': 7.28.5 - '@babel/template': 7.26.9 - '@babel/types': 7.28.5 - debug: 4.4.1 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -4720,6 +4856,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.17.0': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -4735,21 +4883,25 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} '@chakra-ui/anatomy@2.3.4': {} - '@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@ark-ui/react': 5.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.4) + '@ark-ui/react': 5.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4) '@emotion/utils': 1.4.2 - '@pandacss/is-valid-prop': 0.53.6 - csstype: 3.1.3 - fast-safe-stringify: 2.1.1 + '@pandacss/is-valid-prop': 1.9.0 + csstype: 3.2.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -4779,13 +4931,13 @@ snapshots: '@emotion/hash@0.9.2': {} - '@emotion/is-prop-valid@1.3.1': + '@emotion/is-prop-valid@1.4.0': dependencies: '@emotion/memoize': 0.9.0 '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4)': + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)': dependencies: '@babel/runtime': 7.26.10 '@emotion/babel-plugin': 11.13.5 @@ -4797,7 +4949,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.4 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 transitivePeerDependencies: - supports-color @@ -4807,7 +4959,7 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.3 '@emotion/sheet@1.4.0': {} @@ -4821,82 +4973,82 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.25.11': + '@esbuild/aix-ppc64@0.27.3': optional: true - '@esbuild/android-arm64@0.25.11': + '@esbuild/android-arm64@0.27.3': optional: true - '@esbuild/android-arm@0.25.11': + '@esbuild/android-arm@0.27.3': optional: true - '@esbuild/android-x64@0.25.11': + '@esbuild/android-x64@0.27.3': optional: true - '@esbuild/darwin-arm64@0.25.11': + '@esbuild/darwin-arm64@0.27.3': optional: true - '@esbuild/darwin-x64@0.25.11': + '@esbuild/darwin-x64@0.27.3': optional: true - '@esbuild/freebsd-arm64@0.25.11': + '@esbuild/freebsd-arm64@0.27.3': optional: true - '@esbuild/freebsd-x64@0.25.11': + '@esbuild/freebsd-x64@0.27.3': optional: true - '@esbuild/linux-arm64@0.25.11': + '@esbuild/linux-arm64@0.27.3': optional: true - '@esbuild/linux-arm@0.25.11': + '@esbuild/linux-arm@0.27.3': optional: true - '@esbuild/linux-ia32@0.25.11': + '@esbuild/linux-ia32@0.27.3': optional: true - '@esbuild/linux-loong64@0.25.11': + '@esbuild/linux-loong64@0.27.3': optional: true - '@esbuild/linux-mips64el@0.25.11': + '@esbuild/linux-mips64el@0.27.3': optional: true - '@esbuild/linux-ppc64@0.25.11': + '@esbuild/linux-ppc64@0.27.3': optional: true - '@esbuild/linux-riscv64@0.25.11': + '@esbuild/linux-riscv64@0.27.3': optional: true - '@esbuild/linux-s390x@0.25.11': + '@esbuild/linux-s390x@0.27.3': optional: true - '@esbuild/linux-x64@0.25.11': + '@esbuild/linux-x64@0.27.3': optional: true - '@esbuild/netbsd-arm64@0.25.11': + '@esbuild/netbsd-arm64@0.27.3': optional: true - '@esbuild/netbsd-x64@0.25.11': + '@esbuild/netbsd-x64@0.27.3': optional: true - '@esbuild/openbsd-arm64@0.25.11': + '@esbuild/openbsd-arm64@0.27.3': optional: true - '@esbuild/openbsd-x64@0.25.11': + '@esbuild/openbsd-x64@0.27.3': optional: true - '@esbuild/openharmony-arm64@0.25.11': + '@esbuild/openharmony-arm64@0.27.3': optional: true - '@esbuild/sunos-x64@0.25.11': + '@esbuild/sunos-x64@0.27.3': optional: true - '@esbuild/win32-arm64@0.25.11': + '@esbuild/win32-arm64@0.27.3': optional: true - '@esbuild/win32-ia32@0.25.11': + '@esbuild/win32-ia32@0.27.3': optional: true - '@esbuild/win32-x64@0.25.11': + '@esbuild/win32-x64@0.27.3': optional: true '@eslint-community/eslint-utils@4.5.1(eslint@9.39.1(jiti@1.21.7))': @@ -4909,8 +5061,15 @@ snapshots: eslint: 9.39.1(jiti@1.21.7) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1(jiti@1.21.7))': + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} + '@eslint/compat@1.2.9(eslint@9.39.1(jiti@1.21.7))': optionalDependencies: eslint: 9.39.1(jiti@1.21.7) @@ -4958,11 +5117,22 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.9 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + '@floating-ui/dom@1.7.1': dependencies: '@floating-ui/core': 1.7.1 '@floating-ui/utils': 0.2.9 + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.9': {} '@guanmingchiu/sqlparser-ts@0.61.1': {} @@ -4991,45 +5161,47 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@inquirer/confirm@5.1.8(@types/node@24.10.3)': + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@24.10.3)': dependencies: - '@inquirer/core': 10.1.9(@types/node@24.10.3) - '@inquirer/type': 3.0.5(@types/node@24.10.3) + '@inquirer/core': 10.3.2(@types/node@24.10.3) + '@inquirer/type': 3.0.10(@types/node@24.10.3) optionalDependencies: '@types/node': 24.10.3 - '@inquirer/core@10.1.9(@types/node@24.10.3)': + '@inquirer/core@10.3.2(@types/node@24.10.3)': dependencies: - '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5(@types/node@24.10.3) - ansi-escapes: 4.3.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.3) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.10.3 - '@inquirer/figures@1.0.11': {} + '@inquirer/figures@1.0.15': {} - '@inquirer/type@3.0.5(@types/node@24.10.3)': + '@inquirer/type@3.0.10(@types/node@24.10.3)': optionalDependencies: '@types/node': 24.10.3 - '@internationalized/date@3.8.1': + '@internationalized/date@3.11.0': dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.19 - '@internationalized/number@3.6.2': + '@internationalized/number@3.6.5': dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.19 '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -5055,7 +5227,7 @@ snapshots: '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -5097,7 +5269,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@mswjs/interceptors@0.40.0': + '@mswjs/interceptors@0.41.3': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -5115,20 +5287,22 @@ snapshots: '@open-draft/until@2.1.0': {} - '@pandacss/is-valid-prop@0.53.6': {} + '@pandacss/is-valid-prop@1.9.0': {} '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.2.4': {} - '@playwright/test@1.57.0': + '@pkgr/core@0.2.9': {} + + '@playwright/test@1.58.2': dependencies: - playwright: 1.57.0 + playwright: 1.58.2 - '@rolldown/pluginutils@1.0.0-beta.47': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -5207,7 +5381,7 @@ snapshots: '@stylistic/eslint-plugin@2.13.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) eslint-visitor-keys: 4.2.0 espree: 10.3.0 @@ -5217,55 +5391,56 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.13.5': + '@swc/core-darwin-arm64@1.15.18': optional: true - '@swc/core-darwin-x64@1.13.5': + '@swc/core-darwin-x64@1.15.18': optional: true - '@swc/core-linux-arm-gnueabihf@1.13.5': + '@swc/core-linux-arm-gnueabihf@1.15.18': optional: true - '@swc/core-linux-arm64-gnu@1.13.5': + '@swc/core-linux-arm64-gnu@1.15.18': optional: true - '@swc/core-linux-arm64-musl@1.13.5': + '@swc/core-linux-arm64-musl@1.15.18': optional: true - '@swc/core-linux-x64-gnu@1.13.5': + '@swc/core-linux-x64-gnu@1.15.18': optional: true - '@swc/core-linux-x64-musl@1.13.5': + '@swc/core-linux-x64-musl@1.15.18': optional: true - '@swc/core-win32-arm64-msvc@1.13.5': + '@swc/core-win32-arm64-msvc@1.15.18': optional: true - '@swc/core-win32-ia32-msvc@1.13.5': + '@swc/core-win32-ia32-msvc@1.15.18': optional: true - '@swc/core-win32-x64-msvc@1.13.5': + '@swc/core-win32-x64-msvc@1.15.18': optional: true - '@swc/core@1.13.5': + '@swc/core@1.15.18(@swc/helpers@0.5.19)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.13.5 - '@swc/core-darwin-x64': 1.13.5 - '@swc/core-linux-arm-gnueabihf': 1.13.5 - '@swc/core-linux-arm64-gnu': 1.13.5 - '@swc/core-linux-arm64-musl': 1.13.5 - '@swc/core-linux-x64-gnu': 1.13.5 - '@swc/core-linux-x64-musl': 1.13.5 - '@swc/core-win32-arm64-msvc': 1.13.5 - '@swc/core-win32-ia32-msvc': 1.13.5 - '@swc/core-win32-x64-msvc': 1.13.5 + '@swc/core-darwin-arm64': 1.15.18 + '@swc/core-darwin-x64': 1.15.18 + '@swc/core-linux-arm-gnueabihf': 1.15.18 + '@swc/core-linux-arm64-gnu': 1.15.18 + '@swc/core-linux-arm64-musl': 1.15.18 + '@swc/core-linux-x64-gnu': 1.15.18 + '@swc/core-linux-x64-musl': 1.15.18 + '@swc/core-win32-arm64-msvc': 1.15.18 + '@swc/core-win32-ia32-msvc': 1.15.18 + '@swc/core-win32-x64-msvc': 1.15.18 + '@swc/helpers': 0.5.19 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.19': dependencies: tslib: 2.8.1 @@ -5273,19 +5448,20 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/eslint-plugin-query@5.91.2(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.91.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - '@tanstack/query-core@5.90.12': {} + '@tanstack/query-core@5.90.20': {} - '@tanstack/react-query@5.90.12(react@19.2.4)': + '@tanstack/react-query@5.90.21(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.90.12 + '@tanstack/query-core': 5.90.20 react: 19.2.4 '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -5294,20 +5470,20 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/react-virtual@3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-virtual@3.13.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/virtual-core': 3.13.12 + '@tanstack/virtual-core': 3.13.19 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.12': {} + '@tanstack/virtual-core@3.13.19': {} '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -5324,17 +5500,17 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.7.4)': + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1)': dependencies: '@babel/generator': 7.17.7 '@babel/parser': 7.26.10 @@ -5342,7 +5518,7 @@ snapshots: '@babel/types': 7.17.0 javascript-natural-sort: 0.7.1 lodash: 4.17.23 - prettier: 3.7.4 + prettier: 3.8.1 transitivePeerDependencies: - supports-color @@ -5356,24 +5532,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.26.10 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.26.10 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/chai@5.2.2': dependencies: @@ -5399,7 +5575,7 @@ snapshots: '@types/d3-interpolate@3.0.1': dependencies: - '@types/d3-color': 3.1.3 + '@types/d3-color': 3.1.0 '@types/d3-interpolate@3.0.4': dependencies: @@ -5438,7 +5614,7 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.6 '@types/estree@1.0.6': {} @@ -5464,10 +5640,6 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@20.19.23': - dependencies: - undici-types: 6.21.0 - '@types/node@24.10.3': dependencies: undici-types: 7.16.0 @@ -5476,26 +5648,19 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/prop-types@15.7.14': {} - - '@types/react-dom@19.2.3(@types/react@19.2.7)': + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 18.3.19 + '@types/react': 19.2.14 - '@types/react-transition-group@4.4.12(@types/react@19.2.7)': + '@types/react-transition-group@4.4.12(@types/react@19.2.14)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 - '@types/react@18.3.19': - dependencies: - '@types/prop-types': 15.7.14 - csstype: 3.1.3 - - '@types/react@19.2.7': + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -5509,188 +5674,102 @@ snapshots: '@types/whatwg-mimetype@3.0.2': {} - '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@types/ws@8.18.1': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.1 - eslint: 9.39.1(jiti@1.21.7) - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@types/node': 24.10.3 - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 eslint: 9.39.1(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.1 - debug: 4.4.1 + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 eslint: 9.39.1(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 - debug: 4.4.1 - eslint: 9.39.1(jiti@1.21.7) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.48.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - debug: 4.4.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - debug: 4.4.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.48.1': - dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 - - '@typescript-eslint/scope-manager@8.49.0': + '@typescript-eslint/scope-manager@8.56.1': dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 - '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.1 - eslint: 9.39.1(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3 eslint: 9.39.1(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.48.1': {} - '@typescript-eslint/types@8.49.0': {} + '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.48.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/visitor-keys': 8.48.1 - debug: 4.4.1 - minimatch: 9.0.9 - semver: 7.7.1 - tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 - debug: 4.4.1 - minimatch: 9.0.9 - semver: 7.7.1 + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.48.1 - '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/visitor-keys@8.56.1': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - eslint: 9.39.1(jiti@1.21.7) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.48.1': - dependencies: - '@typescript-eslint/types': 8.48.1 - eslint-visitor-keys: 4.2.1 - - '@typescript-eslint/visitor-keys@8.49.0': - dependencies: - '@typescript-eslint/types': 8.49.0 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -5701,7 +5780,7 @@ snapshots: '@visx/group@3.12.0(react@19.2.4)': dependencies: - '@types/react': 18.3.19 + '@types/react': 19.2.14 classnames: 2.5.1 prop-types: 15.8.1 react: 19.2.4 @@ -5715,7 +5794,7 @@ snapshots: '@types/d3-path': 1.0.11 '@types/d3-shape': 1.3.12 '@types/lodash': 4.17.20 - '@types/react': 18.3.19 + '@types/react': 19.2.14 '@visx/curve': 3.12.0 '@visx/group': 3.12.0(react@19.2.4) '@visx/scale': 3.12.0 @@ -5748,27 +5827,27 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@4.2.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@4.2.3(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.47 - '@swc/core': 1.13.5 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + '@rolldown/pluginutils': 1.0.0-rc.2 + '@swc/core': 1.15.18(@swc/helpers@0.5.19) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.1.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2))': dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.53 + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -5783,7 +5862,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -5795,14 +5874,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - msw: 2.12.4(@types/node@24.10.3)(typescript@5.9.3) - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + msw: 2.12.10(@types/node@24.10.3)(typescript@5.9.3) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -5830,18 +5909,18 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@xyflow/react@12.10.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@xyflow/react@12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@xyflow/system': 0.0.74 + '@xyflow/system': 0.0.75 classcat: 5.0.5 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.6(@types/react@19.2.7)(react@19.2.4) + zustand: 4.5.7(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: - '@types/react' - immer - '@xyflow/system@0.0.74': + '@xyflow/system@0.0.75': dependencies: '@types/d3-drag': 3.0.7 '@types/d3-interpolate': 3.0.4 @@ -5853,504 +5932,561 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 - '@zag-js/accordion@1.15.0': + '@zag-js/accordion@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/anatomy@1.15.0': {} + '@zag-js/anatomy@1.35.3': {} - '@zag-js/angle-slider@1.15.0': + '@zag-js/angle-slider@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/rect-utils': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/aria-hidden@1.15.0': {} - - '@zag-js/auto-resize@1.15.0': + '@zag-js/aria-hidden@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/avatar@1.15.0': + '@zag-js/async-list@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/core': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/carousel@1.15.0': + '@zag-js/auto-resize@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/scroll-snap': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/checkbox@1.15.0': + '@zag-js/avatar@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/clipboard@1.15.0': + '@zag-js/carousel@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/scroll-snap': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/collapsible@1.15.0': + '@zag-js/cascade-select@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/collection@1.15.0': + '@zag-js/checkbox@1.35.3': dependencies: - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/color-picker@1.15.0': + '@zag-js/clipboard@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/color-utils': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/color-utils@1.15.0': + '@zag-js/collapsible@1.35.3': dependencies: - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/combobox@1.15.0': + '@zag-js/collection@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/aria-hidden': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/utils': 1.35.3 - '@zag-js/core@1.15.0': + '@zag-js/color-picker@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/color-utils': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/date-picker@1.15.0(@internationalized/date@3.8.1)': + '@zag-js/color-utils@1.35.3': dependencies: - '@internationalized/date': 3.8.1 - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/date-utils': 1.15.0(@internationalized/date@3.8.1) - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/live-region': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/utils': 1.35.3 - '@zag-js/date-utils@1.15.0(@internationalized/date@3.8.1)': + '@zag-js/combobox@1.35.3': dependencies: - '@internationalized/date': 3.8.1 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dialog@1.15.0': + '@zag-js/core@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/aria-hidden': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/remove-scroll': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dismissable@1.15.0': + '@zag-js/date-picker@1.35.3(@internationalized/date@3.11.0)': dependencies: - '@zag-js/dom-query': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/utils': 1.15.0 + '@internationalized/date': 3.11.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/date-utils': 1.35.3(@internationalized/date@3.11.0) + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/dom-query@1.15.0': + '@zag-js/date-utils@1.35.3(@internationalized/date@3.11.0)': dependencies: - '@zag-js/types': 1.15.0 + '@internationalized/date': 3.11.0 - '@zag-js/editable@1.15.0': + '@zag-js/dialog@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/file-upload@1.15.0': + '@zag-js/dismissable@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/file-utils': 1.15.0 - '@zag-js/i18n-utils': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/file-utils@1.15.0': + '@zag-js/dom-query@1.35.3': dependencies: - '@zag-js/i18n-utils': 1.15.0 + '@zag-js/types': 1.35.3 - '@zag-js/floating-panel@1.15.0': + '@zag-js/drawer@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/rect-utils': 1.15.0 - '@zag-js/store': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/focus-trap@1.15.0': + '@zag-js/editable@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/focus-visible@1.15.0': + '@zag-js/file-upload@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/file-utils': 1.35.3 + '@zag-js/i18n-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/highlight-word@1.15.0': {} + '@zag-js/file-utils@1.35.3': + dependencies: + '@zag-js/i18n-utils': 1.35.3 - '@zag-js/hover-card@1.15.0': + '@zag-js/floating-panel@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/i18n-utils@1.15.0': + '@zag-js/focus-trap@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/interact-outside@1.15.0': + '@zag-js/focus-visible@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 + + '@zag-js/highlight-word@1.35.3': {} - '@zag-js/listbox@1.15.0': + '@zag-js/hover-card@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/live-region@1.15.0': {} + '@zag-js/i18n-utils@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 - '@zag-js/menu@1.15.0': + '@zag-js/image-cropper@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/rect-utils': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/number-input@1.15.0': + '@zag-js/interact-outside@1.35.3': dependencies: - '@internationalized/number': 3.6.2 - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/json-tree-utils@1.35.3': {} - '@zag-js/pagination@1.15.0': + '@zag-js/listbox@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/password-input@1.15.0': + '@zag-js/live-region@1.35.3': {} + + '@zag-js/marquee@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/pin-input@1.15.0': + '@zag-js/menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/rect-utils': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/popover@1.15.0': + '@zag-js/navigation-menu@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/aria-hidden': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/remove-scroll': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/popper@1.15.0': + '@zag-js/number-input@1.35.3': dependencies: - '@floating-ui/dom': 1.7.1 - '@zag-js/dom-query': 1.15.0 - '@zag-js/utils': 1.15.0 + '@internationalized/number': 3.6.5 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/presence@1.15.0': + '@zag-js/pagination@1.35.3': dependencies: - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/progress@1.15.0': + '@zag-js/password-input@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/qr-code@1.15.0': + '@zag-js/pin-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popover@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/aria-hidden': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/remove-scroll': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/popper@1.35.3': + dependencies: + '@floating-ui/dom': 1.7.6 + '@zag-js/dom-query': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/presence@1.35.3': + dependencies: + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + + '@zag-js/progress@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/qr-code@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 proxy-memoize: 3.0.1 uqr: 0.1.2 - '@zag-js/radio-group@1.15.0': + '@zag-js/radio-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/rating-group@1.15.0': + '@zag-js/rating-group@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/react@1.15.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@zag-js/react@1.35.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@zag-js/core': 1.15.0 - '@zag-js/store': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/core': 1.35.3 + '@zag-js/store': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@zag-js/rect-utils@1.15.0': {} + '@zag-js/rect-utils@1.35.3': {} + + '@zag-js/remove-scroll@1.35.3': + dependencies: + '@zag-js/dom-query': 1.35.3 - '@zag-js/remove-scroll@1.15.0': + '@zag-js/scroll-area@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/scroll-snap@1.15.0': + '@zag-js/scroll-snap@1.35.3': dependencies: - '@zag-js/dom-query': 1.15.0 + '@zag-js/dom-query': 1.35.3 - '@zag-js/select@1.15.0': + '@zag-js/select@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/signature-pad@1.15.0': + '@zag-js/signature-pad@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - perfect-freehand: 1.2.2 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + perfect-freehand: 1.2.3 - '@zag-js/slider@1.15.0': + '@zag-js/slider@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/splitter@1.15.0': + '@zag-js/splitter@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/steps@1.15.0': + '@zag-js/steps@1.35.3': dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 - '@zag-js/store@1.15.0': + '@zag-js/store@1.35.3': dependencies: proxy-compare: 3.0.1 - '@zag-js/switch@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tabs@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tags-input@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/auto-resize': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/live-region': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/time-picker@1.15.0(@internationalized/date@3.8.1)': - dependencies: - '@internationalized/date': 3.8.1 - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/timer@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/toast@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/toggle-group@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/toggle@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tooltip@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-visible': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/store': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tour@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dismissable': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/focus-trap': 1.15.0 - '@zag-js/interact-outside': 1.15.0 - '@zag-js/popper': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/tree-view@1.15.0': - dependencies: - '@zag-js/anatomy': 1.15.0 - '@zag-js/collection': 1.15.0 - '@zag-js/core': 1.15.0 - '@zag-js/dom-query': 1.15.0 - '@zag-js/types': 1.15.0 - '@zag-js/utils': 1.15.0 - - '@zag-js/types@1.15.0': - dependencies: - csstype: 3.1.3 - - '@zag-js/utils@1.15.0': {} + '@zag-js/switch@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tabs@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tags-input@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/auto-resize': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/live-region': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/timer@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toast@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle-group@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/toggle@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tooltip@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-visible': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tour@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dismissable': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/focus-trap': 1.35.3 + '@zag-js/interact-outside': 1.35.3 + '@zag-js/popper': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/tree-view@1.35.3': + dependencies: + '@zag-js/anatomy': 1.35.3 + '@zag-js/collection': 1.35.3 + '@zag-js/core': 1.35.3 + '@zag-js/dom-query': 1.35.3 + '@zag-js/types': 1.35.3 + '@zag-js/utils': 1.35.3 + + '@zag-js/types@1.35.3': + dependencies: + csstype: 3.2.3 + + '@zag-js/utils@1.35.3': {} acorn-jsx@5.3.2(acorn@8.14.1): dependencies: @@ -6371,15 +6507,11 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - anser@2.3.3: {} - - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 + anser@2.3.5: {} ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: @@ -6387,7 +6519,7 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} anymatch@3.1.3: dependencies: @@ -6486,7 +6618,7 @@ snapshots: axe-core@4.10.3: {} - axios@1.13.5: + axios@1.13.6: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 @@ -6498,7 +6630,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.26.10 cosmiconfig: 7.1.0 resolve: 1.22.10 @@ -6510,7 +6642,9 @@ snapshots: balanced-match@1.0.2: {} - balanced-match@4.0.3: {} + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.0: {} binary-extensions@2.3.0: {} @@ -6523,9 +6657,9 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.2: + brace-expansion@5.0.4: dependencies: - balanced-match: 4.0.3 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -6538,6 +6672,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + builtin-modules@3.3.0: {} c12@1.11.1(magicast@0.3.5): @@ -6582,6 +6724,8 @@ snapshots: caniuse-lite@1.0.30001707: {} + caniuse-lite@1.0.30001777: {} + ccount@2.0.1: {} chai@5.3.3: @@ -6592,12 +6736,12 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 - chakra-react-select@6.1.1(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.7)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + chakra-react-select@6.1.1(@chakra-ui/react@3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@chakra-ui/react': 3.20.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@chakra-ui/react': 3.34.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 - react-select: 5.10.1(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-select: 5.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - '@types/react' - react-dom @@ -6731,8 +6875,6 @@ snapshots: css.escape@1.5.1: {} - csstype@3.1.3: {} - csstype@3.2.3: {} d3-array@3.2.1: @@ -6837,6 +6979,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decode-named-character-reference@1.1.0: dependencies: character-entities: 2.0.2 @@ -6885,8 +7031,8 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.28.4 - csstype: 3.1.3 + '@babel/runtime': 7.28.6 + csstype: 3.2.3 dotenv@16.6.1: {} @@ -6900,12 +7046,16 @@ snapshots: electron-to-chromium@1.5.123: {} - elkjs@0.11.0: {} + electron-to-chromium@1.5.307: {} + + elkjs@0.11.1: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + entities@7.0.1: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -7012,34 +7162,34 @@ snapshots: es6-promise@4.2.8: {} - esbuild@0.25.11: + esbuild@0.27.3: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 escalade@3.2.0: {} @@ -7106,19 +7256,19 @@ snapshots: eslint-plugin-perfectionist@4.15.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): dependencies: '@typescript-eslint/types': 8.48.1 - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.7.4): + eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.8.1): dependencies: eslint: 9.39.1(jiti@1.21.7) - prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.8 + prettier: 3.8.1 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@1.21.7)) @@ -7133,7 +7283,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-react-refresh@0.5.2(eslint@9.39.1(jiti@1.21.7)): dependencies: eslint: 9.39.1(jiti@1.21.7) @@ -7190,6 +7340,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.1(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) @@ -7281,8 +7433,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-safe-stringify@2.1.1: {} - fault@1.0.4: dependencies: format: 0.2.2 @@ -7418,9 +7568,9 @@ snapshots: foreground-child: 3.3.1 jackspeak: 4.2.3 minimatch: 10.2.4 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 - path-scurry: 2.0.1 + path-scurry: 2.0.2 globals@11.12.0: {} @@ -7437,7 +7587,7 @@ snapshots: graphemer@1.4.0: {} - graphql@16.12.0: {} + graphql@16.13.1: {} handlebars@4.7.8: dependencies: @@ -7448,11 +7598,17 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.0.11: + happy-dom@20.8.3: dependencies: - '@types/node': 20.19.23 + '@types/node': 24.10.3 '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 whatwg-mimetype: 3.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate has-bigints@1.1.0: {} @@ -7538,9 +7694,9 @@ snapshots: html-url-attributes@3.0.1: {} - i18next-browser-languagedetector@8.2.0: + i18next-browser-languagedetector@8.2.1: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 i18next-http-backend@3.0.2: dependencies: @@ -7548,9 +7704,9 @@ snapshots: transitivePeerDependencies: - encoding - i18next@25.7.1(typescript@5.9.3): + i18next@25.8.14(typescript@5.9.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 optionalDependencies: typescript: 5.9.3 @@ -7866,7 +8022,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.5: {} + lru-cache@11.2.6: {} lru-cache@5.1.1: dependencies: @@ -7886,7 +8042,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.4 markdown-table@3.0.4: {} @@ -8219,7 +8375,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -8248,7 +8404,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 5.0.4 minimatch@3.1.5: dependencies: @@ -8262,6 +8418,8 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@3.1.0: dependencies: minipass: 7.1.2 @@ -8279,24 +8437,24 @@ snapshots: ms@2.1.3: {} - msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3): + msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3): dependencies: - '@inquirer/confirm': 5.1.8(@types/node@24.10.3) - '@mswjs/interceptors': 0.40.0 + '@inquirer/confirm': 5.1.21(@types/node@24.10.3) + '@mswjs/interceptors': 0.41.3 '@open-draft/deferred-promise': 2.2.0 '@types/statuses': 2.0.6 cookie: 1.1.1 - graphql: 16.12.0 + graphql: 16.13.1 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 path-to-regexp: 6.3.0 picocolors: 1.1.1 - rettime: 0.7.0 + rettime: 0.10.1 statuses: 2.0.2 strict-event-emitter: 0.5.1 tough-cookie: 6.0.0 - type-fest: 5.3.0 + type-fest: 5.4.4 until-async: 3.0.2 yargs: 17.7.2 optionalDependencies: @@ -8327,6 +8485,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.36: {} + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -8462,7 +8622,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.29.0 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -8480,10 +8640,10 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.1: + path-scurry@2.0.2: dependencies: - lru-cache: 11.2.5 - minipass: 7.1.2 + lru-cache: 11.2.6 + minipass: 7.1.3 path-to-regexp@6.3.0: {} @@ -8497,7 +8657,7 @@ snapshots: perfect-debounce@1.0.0: {} - perfect-freehand@1.2.2: {} + perfect-freehand@1.2.3: {} picocolors@1.1.1: {} @@ -8513,11 +8673,11 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - playwright-core@1.57.0: {} + playwright-core@1.58.2: {} - playwright@1.57.0: + playwright@1.58.2: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 @@ -8525,7 +8685,7 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -8533,11 +8693,11 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier@3.7.4: {} + prettier@3.8.1: {} pretty-format@27.5.1: dependencies: @@ -8574,7 +8734,7 @@ snapshots: defu: 6.1.4 destr: 2.0.5 - react-chartjs-2@5.3.0(chart.js@4.5.1)(react@19.2.4): + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.4): dependencies: chart.js: 4.5.1 react: 19.2.4 @@ -8584,7 +8744,7 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 - react-hook-form@7.56.2(react@19.2.4): + react-hook-form@7.71.2(react@19.2.4): dependencies: react: 19.2.4 @@ -8593,34 +8753,34 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-i18next@15.5.1(i18next@25.7.1(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + react-i18next@15.5.1(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: '@babel/runtime': 7.26.10 html-parse-stringify: 3.0.1 - i18next: 25.7.1(typescript@5.9.3) + i18next: 25.8.14(typescript@5.9.3) react: 19.2.4 optionalDependencies: react-dom: 19.2.4(react@19.2.4) typescript: 5.9.3 - react-icons@5.5.0(react@19.2.4): + react-icons@5.6.0(react@19.2.4): dependencies: react: 19.2.4 - react-innertext@1.1.5(@types/react@19.2.7)(react@19.2.4): + react-innertext@1.1.5(@types/react@19.2.14)(react@19.2.4): dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 19.2.4 react-is@16.13.1: {} react-is@17.0.2: {} - react-markdown@9.1.0(@types/react@19.2.7)(react@19.2.4): + react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.4): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.7 + '@types/react': 19.2.14 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -8641,13 +8801,13 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-router-dom@7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-router-dom@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-router: 7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-router: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-router@7.12.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: cookie: 1.1.1 react: 19.2.4 @@ -8655,19 +8815,19 @@ snapshots: optionalDependencies: react-dom: 19.2.4(react@19.2.4) - react-select@5.10.1(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-select@5.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.4) + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) '@floating-ui/dom': 1.7.1 - '@types/react-transition-group': 4.4.12(@types/react@19.2.7) + '@types/react-transition-group': 4.4.12(@types/react@19.2.14) memoize-one: 6.0.0 prop-types: 15.8.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.7)(react@19.2.4) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: - '@types/react' - supports-color @@ -8684,7 +8844,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -8801,7 +8961,7 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rettime@0.7.0: {} + rettime@0.10.1: {} robust-predicates@3.0.2: {} @@ -8863,6 +9023,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.4: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -8971,7 +9133,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 string.prototype.includes@2.0.1: dependencies: @@ -9032,9 +9194,9 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.2.0: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 strip-indent@3.0.0: dependencies: @@ -9062,6 +9224,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + synckit@0.11.8: dependencies: '@pkgr/core': 0.2.4 @@ -9097,11 +9263,11 @@ snapshots: tinyspy@4.0.3: {} - tldts-core@7.0.19: {} + tldts-core@7.0.25: {} - tldts@7.0.19: + tldts@7.0.25: dependencies: - tldts-core: 7.0.19 + tldts-core: 7.0.25 to-fast-properties@2.0.0: {} @@ -9111,7 +9277,7 @@ snapshots: tough-cookie@6.0.0: dependencies: - tldts: 7.0.19 + tldts: 7.0.25 tr46@0.0.3: {} @@ -9119,7 +9285,7 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -9136,13 +9302,11 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.21.3: {} - type-fest@0.6.0: {} type-fest@0.8.1: {} - type-fest@5.3.0: + type-fest@5.4.4: dependencies: tagged-tag: 1.0.0 @@ -9179,12 +9343,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: @@ -9204,8 +9368,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.21.0: {} - undici-types@7.16.0: {} unified@11.0.5: @@ -9249,6 +9411,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uqr@0.1.2: {} uri-js@4.4.1: @@ -9257,17 +9425,17 @@ snapshots: urijs@1.19.11: {} - use-debounce@10.0.4(react@19.2.4): + use-debounce@10.1.0(react@19.2.4): dependencies: react: 19.2.4 - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.7)(react@19.2.4): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4): dependencies: react: 19.2.4 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 - use-sync-external-store@1.4.0(react@19.2.4): + use-sync-external-store@1.6.0(react@19.2.4): dependencies: react: 19.2.4 @@ -9291,13 +9459,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0): + vite-node@3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -9312,29 +9480,29 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)): dependencies: - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) - vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0): + vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2): dependencies: - esbuild: 0.25.11 + esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.10.3 fsevents: 2.3.3 jiti: 1.21.7 - yaml: 2.8.0 + yaml: 2.8.2 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.0.11)(jiti@1.21.7)(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(happy-dom@20.8.3)(jiti@1.21.7)(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(yaml@2.8.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.4(@types/node@24.10.3)(typescript@5.9.3))(vite@7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.12.10(@types/node@24.10.3)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -9352,13 +9520,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.6(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.1(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.3)(jiti@1.21.7)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.3 - happy-dom: 20.0.11 + happy-dom: 20.8.3 transitivePeerDependencies: - jiti - less @@ -9456,9 +9624,11 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 + + ws@8.19.0: {} xtend@4.0.2: {} @@ -9470,7 +9640,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.0: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -9486,7 +9656,7 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.2: {} + yoctocolors-cjs@2.1.3: {} zod-validation-error@4.0.2(zod@4.2.1): dependencies: @@ -9494,17 +9664,17 @@ snapshots: zod@4.2.1: {} - zustand@4.5.6(@types/react@19.2.7)(react@19.2.4): + zustand@4.5.7(@types/react@19.2.14)(react@19.2.4): dependencies: - use-sync-external-store: 1.4.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 19.2.4 - zustand@5.0.4(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): + zustand@5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 19.2.4 - use-sync-external-store: 1.4.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) zwitch@2.0.4: {} diff --git a/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx b/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx index aea00ca4afa1b..716160d260095 100644 --- a/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx +++ b/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx @@ -18,6 +18,7 @@ */ import { chakra } from "@chakra-ui/react"; import Anser, { type AnserJsonEntry } from "anser"; +import type { JSX } from "react"; import * as React from "react"; const fixBackspace = (inputText: string): string => { diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/types.ts b/airflow-core/src/airflow/ui/src/components/DataTable/types.ts index 38a8b0e36b892..5e2c047db6f23 100644 --- a/airflow-core/src/airflow/ui/src/components/DataTable/types.ts +++ b/airflow-core/src/airflow/ui/src/components/DataTable/types.ts @@ -18,7 +18,7 @@ */ import type { SimpleGridProps } from "@chakra-ui/react"; import type { ColumnDef, PaginationState, SortingState, VisibilityState } from "@tanstack/react-table"; -import type { ReactNode } from "react"; +import type { JSX, ReactNode } from "react"; export type TableState = { columnVisibility?: VisibilityState; diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx index 33d7b4e4c54f9..f3febe79687be 100644 --- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx +++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx @@ -18,6 +18,7 @@ */ import { chakra, Code, Link } from "@chakra-ui/react"; import type { TFunction } from "i18next"; +import type { JSX } from "react"; import * as React from "react"; import { Link as RouterLink } from "react-router-dom"; diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx index 6393bc94d719c..253bdd0bc3de8 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx @@ -131,6 +131,6 @@ describe("Task log grouping", () => { fireEvent.click(collapseItem); - await waitFor(() => expect(screen.queryByText(/Marking task as SUCCESS/iu)).toBeVisible()); + await waitFor(() => expect(screen.queryByText(/Marking task as SUCCESS/iu)).not.toBeVisible()); }, 10_000); }); diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx index 694ba0ee87189..f6df9e1c420d0 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx @@ -18,7 +18,7 @@ */ import { Box, Code, VStack, IconButton } from "@chakra-ui/react"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { useLayoutEffect, useRef } from "react"; +import { type JSX, useLayoutEffect, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { FiChevronDown, FiChevronUp } from "react-icons/fi"; diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx index b6278dcdb0bb5..1a82896741468 100644 --- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx +++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx @@ -20,6 +20,7 @@ import { chakra, Box } from "@chakra-ui/react"; import type { UseQueryOptions } from "@tanstack/react-query"; import dayjs from "dayjs"; import type { TFunction } from "i18next"; +import type { JSX } from "react"; import { useTranslation } from "react-i18next"; import innerText from "react-innertext"; diff --git a/airflow-core/src/airflow/ui/src/utils/slots.tsx b/airflow-core/src/airflow/ui/src/utils/slots.tsx index 8211e990c7cef..0d1fc385ce659 100644 --- a/airflow-core/src/airflow/ui/src/utils/slots.tsx +++ b/airflow-core/src/airflow/ui/src/utils/slots.tsx @@ -18,6 +18,8 @@ */ /* eslint-disable perfectionist/sort-objects */ +import type { JSX } from "react"; + import type { PoolResponse } from "openapi/requests/types.gen"; import { StateIcon } from "src/components/StateIcon"; From 68b6ac52bc43bcd4c7d496d34d2f5b7a1db93e22 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Tue, 10 Mar 2026 14:25:21 +0100 Subject: [PATCH 036/595] Fix test_configuration.py to use real deprecated options from shared config (#63263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tests were registering fake deprecated option mappings at module level (core.sql_alchemy_conn → database.sql_alchemy_conn) that had been removed from the shared configuration project. This mutation of the class-level deprecated_options dict caused side-effects when tests ran under xdist in different order, as the shared mutable state leaked across test modules. Replace the removed sql_alchemy_conn deprecated mapping with the real webserver.secret_key → api.secret_key mapping that is already defined in the shared configuration's deprecated_options. Co-authored-by: Claude Opus 4.6 --- .../unit/config_templates/deprecated.cfg | 4 +- .../unit/config_templates/deprecated_cmd.cfg | 4 +- .../config_templates/deprecated_secret.cfg | 4 +- .../tests/unit/core/test_configuration.py | 180 ++++++++---------- 4 files changed, 86 insertions(+), 106 deletions(-) diff --git a/airflow-core/tests/unit/config_templates/deprecated.cfg b/airflow-core/tests/unit/config_templates/deprecated.cfg index 1a79045424816..a3f0fbbc0b0dc 100644 --- a/airflow-core/tests/unit/config_templates/deprecated.cfg +++ b/airflow-core/tests/unit/config_templates/deprecated.cfg @@ -15,5 +15,5 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -[core] -sql_alchemy_conn = mysql:// +[webserver] +secret_key = my_secret_key diff --git a/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg b/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg index dbe819fbb63f8..bcfeec4bc602a 100644 --- a/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg +++ b/airflow-core/tests/unit/config_templates/deprecated_cmd.cfg @@ -15,5 +15,5 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -[core] -sql_alchemy_conn_cmd = echo -n "postgresql://" +[webserver] +secret_key_cmd = echo -n "test_secret_key" diff --git a/airflow-core/tests/unit/config_templates/deprecated_secret.cfg b/airflow-core/tests/unit/config_templates/deprecated_secret.cfg index ca2f5aa637199..b05540d99a496 100644 --- a/airflow-core/tests/unit/config_templates/deprecated_secret.cfg +++ b/airflow-core/tests/unit/config_templates/deprecated_secret.cfg @@ -15,5 +15,5 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -[core] -sql_alchemy_conn_secret = secret_path +[webserver] +secret_key_secret = secret_path diff --git a/airflow-core/tests/unit/core/test_configuration.py b/airflow-core/tests/unit/core/test_configuration.py index 9c7ec9a3d167f..e591421e68b58 100644 --- a/airflow-core/tests/unit/core/test_configuration.py +++ b/airflow-core/tests/unit/core/test_configuration.py @@ -57,9 +57,8 @@ HOME_DIR = os.path.expanduser("~") -# The conf has been updated with sql_alchemy_con and deactivate_stale_dags_interval to test the +# The conf has been updated with deactivate_stale_dags_interval to test the # functionality of deprecated options support. -conf.deprecated_options[("database", "sql_alchemy_conn")] = ("core", "sql_alchemy_conn", "2.3.0") conf.deprecated_options[("scheduler", "parsing_cleanup_interval")] = ( "scheduler", "deactivate_stale_dags_interval", @@ -1183,38 +1182,25 @@ def test_deprecated_values_from_conf(self): ("old", "new"), [ ( - ("core", "sql_alchemy_conn", "postgres+psycopg2://localhost/postgres"), - ("database", "sql_alchemy_conn", "postgresql://localhost/postgres"), + ("webserver", "secret_key", "test_secret_value"), + ("api", "secret_key", "test_secret_value"), ), ], ) - def test_deprecated_env_vars_upgraded_and_removed(self, old, new): - test_conf = AirflowConfigParser( - default_config=""" -[core] -executor=LocalExecutor -[database] -sql_alchemy_conn=sqlite://test -""" - ) + def test_deprecated_env_vars_lookup(self, old, new): + test_conf = AirflowConfigParser() old_section, old_key, old_value = old new_section, new_key, new_value = new old_env_var = test_conf._env_var_name(old_section, old_key) new_env_var = test_conf._env_var_name(new_section, new_key) - with mock.patch.dict("os.environ", **{old_env_var: old_value}): + env_patch = {old_env_var: old_value} + with mock.patch.dict("os.environ", env_patch): # Can't start with the new env var existing... os.environ.pop(new_env_var, None) - with pytest.warns(FutureWarning): - test_conf.validate() - assert test_conf.get(new_section, new_key) == new_value - # We also need to make sure the deprecated env var is removed - # so that any subprocesses don't use it in place of our updated - # value. - assert old_env_var not in os.environ - # and make sure we track the old value as well, under the new section/key - assert test_conf.upgraded_values[(new_section, new_key)] == old_value + with pytest.warns(DeprecationWarning, match="the old setting has been used"): + assert test_conf.get(new_section, new_key) == new_value @pytest.mark.parametrize( "conf_dict", @@ -1328,19 +1314,19 @@ def test_conf_as_dict_when_deprecated_value_in_config(self, display_source: bool include_env=False, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("mysql://", "airflow.cfg") if display_source else "mysql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("my_secret_key", "airflow.cfg") if display_source else "my_secret_key" ) - # database should be None because the deprecated value is set in config - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in config + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "mysql://" + assert conf.get("api", "secret_key") == "my_secret_key" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN": "postgresql://"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY": "env_secret_key"}, clear=True) def test_conf_as_dict_when_deprecated_value_in_both_env_and_config(self, display_source: bool): with use_config(config="deprecated.cfg"): cfg_dict = conf.as_dict( @@ -1350,19 +1336,19 @@ def test_conf_as_dict_when_deprecated_value_in_both_env_and_config(self, display include_env=True, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "env var") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("env_secret_key", "env var") if display_source else "env_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in env value + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "env_secret_key" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN": "postgresql://"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY": "env_secret_key"}, clear=True) def test_conf_as_dict_when_deprecated_value_in_both_env_and_config_exclude_env( self, display_source: bool ): @@ -1374,52 +1360,51 @@ def test_conf_as_dict_when_deprecated_value_in_both_env_and_config_exclude_env( include_env=False, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("mysql://", "airflow.cfg") if display_source else "mysql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("my_secret_key", "airflow.cfg") if display_source else "my_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in config (env excluded) + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "mysql://" + assert conf.get("api", "secret_key") == "my_secret_key" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN": "postgresql://"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY": "env_secret_key"}, clear=True) def test_conf_as_dict_when_deprecated_value_in_env(self, display_source: bool): with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, raw=True, display_sensitive=True, include_env=True ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "env var") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("env_secret_key", "env var") if display_source else "env_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in env value + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "env_secret_key" @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict("os.environ", {}, clear=True) def test_conf_as_dict_when_both_conf_and_env_are_empty(self, display_source: bool): + default_secret_key = conf.get_default_value("api", "secret_key") with use_config(config="empty.cfg"): cfg_dict = conf.as_dict(display_source=display_source, raw=True, display_sensitive=True) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - # database should be taken from default because the deprecated value is missing in config - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + # api should be taken from default because the deprecated value is missing in config + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict("os.environ", {}, clear=True) @@ -1432,20 +1417,20 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_config(self, display_source: include_env=True, include_cmds=True, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "cmd") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("test_secret_key", "cmd") if display_source else "test_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in cmd + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "test_secret_key" @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict( - "os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_CMD": "echo -n 'postgresql://'"}, clear=True + "os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_CMD": "echo -n 'test_secret_key'"}, clear=True ) def test_conf_as_dict_when_deprecated_value_in_cmd_env(self, display_source: bool): with use_config(config="empty.cfg"): @@ -1456,22 +1441,23 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_env(self, display_source: boo include_env=True, include_cmds=True, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "cmd") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("test_secret_key", "cmd") if display_source else "test_secret_key" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in cmd env + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "test_secret_key" @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict( - "os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_CMD": "echo -n 'postgresql://'"}, clear=True + "os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_CMD": "echo -n 'test_secret_key'"}, clear=True ) def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_env(self, display_source: bool): + default_secret_key = conf.get_default_value("api", "secret_key") with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1480,21 +1466,20 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_env(self, display_so include_env=True, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) @mock.patch.dict("os.environ", {}, clear=True) def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_config(self, display_source: bool): + default_secret_key = conf.get_default_value("api", "secret_key") with use_config(config="deprecated_cmd.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1503,25 +1488,23 @@ def test_conf_as_dict_when_deprecated_value_in_cmd_disabled_config(self, display include_env=True, include_cmds=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_SECRET": "secret_path'"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_SECRET": "secret_path"}, clear=True) @mock.patch("airflow.configuration.get_custom_secret_backend") def test_conf_as_dict_when_deprecated_value_in_secrets( self, get_custom_secret_backend, display_source: bool ): - get_custom_secret_backend.return_value.get_config.return_value = "postgresql://" + get_custom_secret_backend.return_value.get_config.return_value = "secret_from_backend" with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1530,24 +1513,25 @@ def test_conf_as_dict_when_deprecated_value_in_secrets( include_env=True, include_secret=True, ) - assert cfg_dict["core"].get("sql_alchemy_conn") == ( - ("postgresql://", "secret") if display_source else "postgresql://" + assert cfg_dict["webserver"].get("secret_key") == ( + ("secret_from_backend", "secret") if display_source else "secret_from_backend" ) - # database should be None because the deprecated value is set in env value - assert cfg_dict["database"].get("sql_alchemy_conn") is None + # api should be None because the deprecated value is set in secret + assert cfg_dict["api"].get("secret_key") is None if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == "postgresql://" + assert conf.get("api", "secret_key") == "secret_from_backend" @pytest.mark.parametrize("display_source", [True, False]) - @mock.patch.dict("os.environ", {"AIRFLOW__CORE__SQL_ALCHEMY_CONN_SECRET": "secret_path'"}, clear=True) + @mock.patch.dict("os.environ", {"AIRFLOW__WEBSERVER__SECRET_KEY_SECRET": "secret_path"}, clear=True) @mock.patch("airflow.configuration.get_custom_secret_backend") def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_env( self, get_custom_secret_backend, display_source: bool ): - get_custom_secret_backend.return_value.get_config.return_value = "postgresql://" + default_secret_key = conf.get_default_value("api", "secret_key") + get_custom_secret_backend.return_value.get_config.return_value = "secret_from_backend" with use_config(config="empty.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1556,17 +1540,15 @@ def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_env( include_env=True, include_secret=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key @pytest.mark.parametrize("display_source", [True, False]) @mock.patch("airflow.configuration.get_custom_secret_backend") @@ -1574,7 +1556,8 @@ def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_env( def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_config( self, get_custom_secret_backend, display_source: bool ): - get_custom_secret_backend.return_value.get_config.return_value = "postgresql://" + default_secret_key = conf.get_default_value("api", "secret_key") + get_custom_secret_backend.return_value.get_config.return_value = "secret_from_backend" with use_config(config="deprecated_secret.cfg"): cfg_dict = conf.as_dict( display_source=display_source, @@ -1583,17 +1566,15 @@ def test_conf_as_dict_when_deprecated_value_in_secrets_disabled_config( include_env=True, include_secret=False, ) - assert cfg_dict["core"].get("sql_alchemy_conn") is None - assert cfg_dict["database"].get("sql_alchemy_conn") == ( - (f"sqlite:///{HOME_DIR}/airflow/airflow.db", "default") - if display_source - else f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert cfg_dict.get("webserver", {}).get("secret_key") is None + assert cfg_dict["api"].get("secret_key") == ( + (default_secret_key, "default") if display_source else default_secret_key ) if not display_source: remove_all_configurations() conf.read_dict(dictionary=cfg_dict) os.environ.clear() - assert conf.get("database", "sql_alchemy_conn") == f"sqlite:///{HOME_DIR}/airflow/airflow.db" + assert conf.get("api", "secret_key") == default_secret_key def test_as_dict_should_not_falsely_emit_future_warning(self): from airflow.configuration import AirflowConfigParser @@ -1815,7 +1796,6 @@ def test_sensitive_values(): ("sentry", "sentry_dsn"), ("database", "sql_alchemy_engine_args"), ("keycloak_auth_manager", "client_secret"), - ("core", "sql_alchemy_conn"), ("celery_broker_transport_options", "sentinel_kwargs"), ("celery", "broker_url"), ("celery", "flower_basic_auth"), From df4cb30b116c8628afc465876e08d58f2bcb897b Mon Sep 17 00:00:00 2001 From: Ash Berlin-Taylor Date: Tue, 10 Mar 2026 13:33:18 +0000 Subject: [PATCH 037/595] Restructure Execution API security to better use FastAPI's Security scopes (#62582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, `JWTBearer` in deps.py does everything: crypto validation, sub-claim matching, and it runs twice per request on ti:self routes because FastAPI includes scopes in dependency cache keys for `HTTPBearer` subclasses, defeating dedup. In a PR that is already created (but not yet merged) we want per-endpoint token type policies (e.g. the /run endpoint will need to accept workload tokens while other routes stay execution-only). This changes is the "foundation" that enables that to work in a nice clear fashion `SecurityScopes` can't express this directly because FastAPI resolves outer router deps before inner ones -- a `token:workload` scope on an endpoint needs to *relax* the default restriction, but `SecurityScopes` only accumulate additively. The fix is a new security.py with a three-layer split: - `JWTBearer` (`_jwt_bearer`) now does only crypto validation and caches the result on the ASGI request scope. It never looks at scopes or token types. - `require_auth` is a plain function (not an `HTTPBearer` subclass) used via `Security(require_auth)` on routers. Because plain functions have `_uses_scopes=False` in FastAPI's dependency system, `_jwt_bearer` (its sub-dep) deduplicates correctly across multiple Security resolutions. It enforces `ti:self` via `SecurityScopes` and reads allowed token types from the matched route object. - `ExecutionAPIRoute` is a custom `APIRoute` subclass that precomputes `allowed_token_types` from `token:*` Security scopes at route registration time — after `include_router` has merged all parent and child dependencies. This sidesteps the resolution ordering problem entirely. To opt a route into workload tokens, it's now a one-liner: ```python @ti_id_router.patch( "/{task_instance_id}/run", dependencies=[Security(require_auth, scopes=["token:execution", "token:workload"])], ) ``` Nothing uses the workload-scoped tokens just yet -- this PR lays the foundation; a follow-up PR will add token:workload to /run. Also cleaned up the module boundaries: security.py owns all auth-related deps (CurrentTIToken, get_team_name_dep, require_auth); deps.py is just the svcs DepContainer. Renamed JWTBearerDep to CurrentTIToken to match the FastAPI current_user convention. I tried _lots_ of different approaches to get this merge/override behaviour, and the cleanest was a custom route class --- .codespellignorelines | 1 + .../api_fastapi/execution_api/AGENTS.md | 4 + .../airflow/api_fastapi/execution_api/app.py | 26 +- .../airflow/api_fastapi/execution_api/deps.py | 90 +------ .../execution_api/routes/__init__.py | 6 +- .../execution_api/routes/connections.py | 4 +- .../execution_api/routes/task_instances.py | 10 +- .../execution_api/routes/variables.py | 4 +- .../api_fastapi/execution_api/routes/xcoms.py | 4 +- .../api_fastapi/execution_api/security.py | 243 ++++++++++++++++++ .../api_fastapi/execution_api/conftest.py | 60 ++--- .../api_fastapi/execution_api/test_app.py | 17 ++ .../execution_api/test_security.py | 138 ++++++++++ .../versions/head/test_task_instances.py | 116 +++++++-- .../versions/head/test_variables.py | 4 +- .../execution_api/versions/head/test_xcoms.py | 4 +- 16 files changed, 563 insertions(+), 168 deletions(-) create mode 100644 airflow-core/src/airflow/api_fastapi/execution_api/security.py create mode 100644 airflow-core/tests/unit/api_fastapi/execution_api/test_security.py diff --git a/.codespellignorelines b/.codespellignorelines index 1234698be3071..5e8e365086240 100644 --- a/.codespellignorelines +++ b/.codespellignorelines @@ -4,3 +4,4 @@ The platform supports **C**reate, **R**ead, **U**pdate, and **D**elete operations on most resources.
Code block\ndoes not\nrespect\nnewlines\n
"trough", + assert "task_instance_id" in route.dependant.path_param_names, ( diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md b/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md index 39e083345735f..32500df182e3a 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md +++ b/airflow-core/src/airflow/api_fastapi/execution_api/AGENTS.md @@ -63,3 +63,7 @@ Adding a new Execution API feature touches multiple packages. All of these must - Triggerer handler: `airflow-core/src/airflow/jobs/triggerer_job_runner.py` - Task SDK generated models: `task-sdk/src/airflow/sdk/api/datamodels/_generated.py` - Full versioning guide: [`contributing-docs/19_execution_api_versioning.rst`](../../../../contributing-docs/19_execution_api_versioning.rst) + +## Token Scope Infrastructure + +Token types (`"execution"`, `"workload"`), route-level enforcement via `ExecutionAPIRoute` + `require_auth`, and the `ti:self` path-parameter validation are documented in the module docstring of `security.py`. diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/app.py b/airflow-core/src/airflow/api_fastapi/execution_api/app.py index ac0d8012a903f..c7a9593c3c82f 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/app.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/app.py @@ -220,6 +220,15 @@ def replace_any_of_with_one_of(spec): if prop.get("type") == "string" and (const := prop.pop("const", None)): prop["enum"] = [const] + # Remove internal x-airflow-* extension fields from OpenAPI spec + # These are used for runtime validation but shouldn't be exposed in the public API + for path_item in openapi_schema.get("paths", {}).values(): + for operation in path_item.values(): + if isinstance(operation, dict): + keys_to_remove = [key for key in operation.keys() if key.startswith("x-airflow-")] + for key in keys_to_remove: + del operation[key] + return openapi_schema @@ -304,23 +313,26 @@ def app(self): if not self._app: from airflow.api_fastapi.common.dagbag import create_dag_bag from airflow.api_fastapi.execution_api.app import create_task_execution_api_app - from airflow.api_fastapi.execution_api.deps import ( - JWTBearerDep, - JWTBearerTIPathDep, - ) + from airflow.api_fastapi.execution_api.datamodels.token import TIToken from airflow.api_fastapi.execution_api.routes.connections import has_connection_access from airflow.api_fastapi.execution_api.routes.variables import has_variable_access from airflow.api_fastapi.execution_api.routes.xcoms import has_xcom_access + from airflow.api_fastapi.execution_api.security import _jwt_bearer self._app = create_task_execution_api_app() # Set up dag_bag in app state for dependency injection self._app.state.dag_bag = create_dag_bag() - async def always_allow(): ... + async def always_allow(request: Request): + from uuid import UUID + + ti_id = UUID( + request.path_params.get("task_instance_id", "00000000-0000-0000-0000-000000000000") + ) + return TIToken(id=ti_id, claims={"scope": "execution"}) - self._app.dependency_overrides[JWTBearerDep.dependency] = always_allow - self._app.dependency_overrides[JWTBearerTIPathDep.dependency] = always_allow + self._app.dependency_overrides[_jwt_bearer] = always_allow self._app.dependency_overrides[has_connection_access] = always_allow self._app.dependency_overrides[has_variable_access] = always_allow self._app.dependency_overrides[has_xcom_access] = always_allow diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/deps.py b/airflow-core/src/airflow/api_fastapi/execution_api/deps.py index 9fc8c30cb926e..192309a8e403f 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/deps.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/deps.py @@ -18,23 +18,8 @@ # Disable future annotations in this file to work around https://github.com/fastapi/fastapi/issues/13056 # ruff: noqa: I002 -from typing import Any - -import structlog import svcs -from fastapi import Depends, HTTPException, Request, status -from fastapi.security import HTTPBearer -from sqlalchemy import select - -from airflow.api_fastapi.auth.tokens import JWTValidator -from airflow.api_fastapi.common.db.common import AsyncSessionDep -from airflow.api_fastapi.execution_api.datamodels.token import TIToken -from airflow.configuration import conf -from airflow.models import DagModel, TaskInstance -from airflow.models.dagbundle import DagBundleModel -from airflow.models.team import Team - -log = structlog.get_logger(logger_name=__name__) +from fastapi import Depends, Request # See https://github.com/fastapi/fastapi/issues/13056 @@ -44,76 +29,3 @@ async def _container(request: Request): DepContainer: svcs.Container = Depends(_container) - - -class JWTBearer(HTTPBearer): - """ - A FastAPI security dependency that validates JWT tokens using for the Execution API. - - This will validate the tokens are signed and that the ``sub`` is a UUID, but nothing deeper than that. - - The dependency result will be an `TIToken` object containing the ``id`` UUID (from the ``sub``) and other - validated claims. - """ - - def __init__( - self, - path_param_name: str | None = None, - required_claims: dict[str, Any] | None = None, - ): - super().__init__(auto_error=False) - self.path_param_name = path_param_name - self.required_claims = required_claims or {} - - async def __call__( # type: ignore[override] - self, - request: Request, - services=DepContainer, - ) -> TIToken | None: - creds = await super().__call__(request) - if not creds: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing auth token") - - validator: JWTValidator = await services.aget(JWTValidator) - - try: - # Example: Validate "task_instance_id" component of the path matches the one in the token - if self.path_param_name: - id = request.path_params[self.path_param_name] - validators: dict[str, Any] = { - **self.required_claims, - "sub": {"essential": True, "value": id}, - } - else: - validators = self.required_claims - claims = await validator.avalidated_claims(creds.credentials, validators) - return TIToken(id=claims["sub"], claims=claims) - except Exception as err: - log.warning( - "Failed to validate JWT", - exc_info=True, - token=creds.credentials, - ) - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid auth token: {err}") - - -JWTBearerDep: TIToken = Depends(JWTBearer()) - -# This checks that the UUID in the url matches the one in the token for us. -JWTBearerTIPathDep = Depends(JWTBearer(path_param_name="task_instance_id")) - - -async def get_team_name_dep(session: AsyncSessionDep, token=JWTBearerDep) -> str | None: - """Return the team name associated to the task (if any).""" - if not conf.getboolean("core", "multi_team"): - return None - - stmt = ( - select(Team.name) - .select_from(TaskInstance) - .join(DagModel, DagModel.dag_id == TaskInstance.dag_id) - .join(DagBundleModel, DagBundleModel.name == DagModel.bundle_name) - .join(DagBundleModel.teams) - .where(TaskInstance.id == token.id) - ) - return await session.scalar(stmt) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py index 562b8588fbf2c..aeef4d092b194 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py @@ -17,9 +17,8 @@ from __future__ import annotations from cadwyn import VersionedAPIRouter -from fastapi import APIRouter +from fastapi import APIRouter, Security -from airflow.api_fastapi.execution_api.deps import JWTBearerDep from airflow.api_fastapi.execution_api.routes import ( asset_events, assets, @@ -32,12 +31,13 @@ variables, xcoms, ) +from airflow.api_fastapi.execution_api.security import require_auth execution_api_router = APIRouter() execution_api_router.include_router(health.router, prefix="/health", tags=["Health"]) # _Every_ single endpoint under here must be authenticated. Some do further checks on top of these -authenticated_router = VersionedAPIRouter(dependencies=[JWTBearerDep]) # type: ignore[list-item] +authenticated_router = VersionedAPIRouter(dependencies=[Security(require_auth)]) # type: ignore[list-item] authenticated_router.include_router(assets.router, prefix="/assets", tags=["Assets"]) authenticated_router.include_router(asset_events.router, prefix="/asset-events", tags=["Asset Events"]) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py index 44cc3bfbd79bb..a7bb9959c6db7 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/connections.py @@ -23,14 +23,14 @@ from fastapi import APIRouter, Depends, HTTPException, Path, status from airflow.api_fastapi.execution_api.datamodels.connection import ConnectionResponse -from airflow.api_fastapi.execution_api.deps import JWTBearerDep, get_team_name_dep +from airflow.api_fastapi.execution_api.security import CurrentTIToken, get_team_name_dep from airflow.exceptions import AirflowNotFoundException from airflow.models.connection import Connection async def has_connection_access( connection_id: str = Path(), - token=JWTBearerDep, + token=CurrentTIToken, ) -> bool: """Check if the task has access to the connection.""" # TODO: Placeholder for actual implementation diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py index 53e3bbb2a9f9c..9273cc8b3d487 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py @@ -28,7 +28,7 @@ import attrs import structlog from cadwyn import VersionedAPIRouter -from fastapi import Body, HTTPException, Query, status +from fastapi import Body, HTTPException, Query, Security, status from pydantic import JsonValue from sqlalchemy import func, or_, tuple_, update from sqlalchemy.engine import CursorResult @@ -59,7 +59,7 @@ TISuccessStatePayload, TITerminalStatePayload, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerTIPathDep +from airflow.api_fastapi.execution_api.security import ExecutionAPIRoute, require_auth from airflow.exceptions import TaskNotFound from airflow.models.asset import AssetActive from airflow.models.dag import DagModel @@ -78,10 +78,10 @@ router = VersionedAPIRouter() ti_id_router = VersionedAPIRouter( + route_class=ExecutionAPIRoute, dependencies=[ - # This checks that the UUID in the url matches the one in the token for us. - JWTBearerTIPathDep - ] + Security(require_auth, scopes=["ti:self"]), + ], ) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py index 5621b6cd081ba..1e2e2058932da 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py @@ -26,14 +26,14 @@ VariablePostBody, VariableResponse, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerDep, get_team_name_dep +from airflow.api_fastapi.execution_api.security import CurrentTIToken, get_team_name_dep from airflow.models.variable import Variable async def has_variable_access( request: Request, variable_key: str = Path(), - token=JWTBearerDep, + token=CurrentTIToken, ): """Check if the task has access to the variable.""" write = request.method not in {"GET", "HEAD", "OPTIONS"} diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py index ec77b64dc4496..9b83c40db5e30 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py @@ -32,7 +32,7 @@ XComSequenceIndexResponse, XComSequenceSliceResponse, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerDep +from airflow.api_fastapi.execution_api.security import CurrentTIToken from airflow.models.taskmap import TaskMap from airflow.models.xcom import XComModel from airflow.utils.db import get_query_count @@ -44,7 +44,7 @@ async def has_xcom_access( task_id: str, xcom_key: Annotated[str, Path(alias="key", min_length=1)], request: Request, - token=JWTBearerDep, + token=CurrentTIToken, ) -> bool: """Check if the task has access to the XCom.""" # TODO: Placeholder for actual implementation diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/security.py b/airflow-core/src/airflow/api_fastapi/execution_api/security.py new file mode 100644 index 0000000000000..215997d28d9c7 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/execution_api/security.py @@ -0,0 +1,243 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Execution API security: JWT validation, token scopes, and route-level access control. + +Token types (``TokenType``): + +``"execution"`` + Default scope, accepted by all endpoints. Short-lived, automatically + refreshed by ``JWTReissueMiddleware``. + +``"workload"`` + Restricted scope, only accepted on routes that opt in via + ``Security(require_auth, scopes=["token:workload"])``. + +Tokens without a ``scope`` claim default to ``"execution"`` for backwards +compatibility (``claims.setdefault("scope", "execution")``). + +Enforcement flow: + 1. ``JWTBearer.__call__`` validates the JWT once per request (crypto + + signature verification), caching the result on the ASGI request scope. + Subsequent FastAPI dependency resolutions and Cadwyn replays return + the cache. + 2. ``require_auth`` is the Security dependency on routers. It receives + the token from ``JWTBearer`` and enforces: + - Token type against the route's ``allowed_token_types`` (precomputed + by ``ExecutionAPIRoute`` from ``token:*`` Security scopes). + - ``ti:self`` scope — checks that the JWT ``sub`` matches the + ``{task_instance_id}`` path parameter. + 3. ``ExecutionAPIRoute`` precomputes ``allowed_token_types`` from + ``token:*`` Security scopes at route registration time. Routes + without explicit ``token:*`` scopes default to execution-only. + +Why ``ExecutionAPIRoute`` is needed: + FastAPI resolves router-level ``Security()`` dependencies from outermost + to innermost. A ``token:workload`` scope on an inner endpoint would need + to *relax* the outer router's default execution-only restriction, but + ``SecurityScopes`` only accumulate additively — an outer dependency + cannot see scopes declared by inner ones. ``ExecutionAPIRoute`` solves + this by inspecting the **merged** dependency list at route registration + time (after ``include_router`` has combined all parent and child + dependencies) and precomputing the full ``allowed_token_types`` set. + ``require_auth`` then reads this precomputed set from the matched route + at request time, avoiding the ordering problem entirely. + + Any router whose routes need non-default token type policies must use + ``route_class=ExecutionAPIRoute``. Routers that only need the default + (execution-only) can use the standard route class — ``require_auth`` + falls back to ``{"execution"}`` when the attribute is absent. +""" + +# Disable future annotations in this file to work around https://github.com/fastapi/fastapi/issues/13056 +# ruff: noqa: I002 + +from typing import Any, Literal, get_args + +import structlog +from fastapi import Depends, HTTPException, Request, status +from fastapi.params import Security as SecurityParam +from fastapi.routing import APIRoute +from fastapi.security import HTTPBearer, SecurityScopes +from sqlalchemy import select + +from airflow.api_fastapi.auth.tokens import JWTValidator +from airflow.api_fastapi.common.db.common import AsyncSessionDep +from airflow.api_fastapi.execution_api.datamodels.token import TIToken +from airflow.api_fastapi.execution_api.deps import DepContainer + +log = structlog.get_logger(logger_name=__name__) + +TokenType = Literal["execution", "workload"] + +VALID_TOKEN_TYPES: frozenset[str] = frozenset(get_args(TokenType)) + +_REQUEST_SCOPE_TOKEN_KEY = "ti_token" + + +class JWTBearer(HTTPBearer): + """ + Validates JWT tokens for the Execution API. + + Performs cryptographic validation once per request and caches the result + on the ASGI request scope. Subsequent resolutions (FastAPI dependency + dedup or Cadwyn replays) return the cached token. + + This dependency handles ONLY crypto validation and token construction. + All route-specific authorization (token type, ti:self) is handled by + ``require_auth``. + """ + + def __init__(self, required_claims: dict[str, Any] | None = None): + super().__init__(auto_error=False) + self.required_claims = required_claims or {} + + async def __call__( # type: ignore[override] + self, + request: Request, + services=DepContainer, + ) -> TIToken | None: + # Return cached token (handles both FastAPI dependency dedup and Cadwyn replays). + if cached := request.scope.get(_REQUEST_SCOPE_TOKEN_KEY): + return cached + + # First resolution — full cryptographic validation. + creds = await super().__call__(request) + if not creds: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing auth token") + + validator: JWTValidator = await services.aget(JWTValidator) + + try: + claims = await validator.avalidated_claims(creds.credentials, dict(self.required_claims)) + except Exception as err: + log.warning("Failed to validate JWT", exc_info=True, token=creds.credentials) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid auth token: {err}") + + claims.setdefault("scope", "execution") + + token = TIToken(id=claims["sub"], claims=claims) + request.scope[_REQUEST_SCOPE_TOKEN_KEY] = token + return token + + +_jwt_bearer = JWTBearer() + + +async def require_auth( + security_scopes: SecurityScopes, + request: Request, + token: TIToken = Depends(_jwt_bearer), +) -> TIToken: + """ + Security dependency that enforces token type and ``ti:self`` scope. + + Used via ``Security(require_auth)`` on routers. ``SecurityScopes`` are + accumulated by FastAPI from all parent ``Security()`` declarations. + + Token type enforcement reads ``route.allowed_token_types`` (precomputed + by ``ExecutionAPIRoute``) or defaults to ``{"execution"}``. + """ + token_scope = token.claims.get("scope", "execution") + + if token_scope not in VALID_TOKEN_TYPES: + log.warning("Invalid token scope in claims", token_scope=token_scope, path=request.url.path) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Invalid token scope: {token_scope}", + ) + + route = request.scope.get("route") + allowed_token_types = getattr(route, "allowed_token_types", frozenset({"execution"})) + + if token_scope not in allowed_token_types: + log.warning( + "Token type not allowed for endpoint", + token_scope=token_scope, + allowed_types=sorted(allowed_token_types), + path=request.url.path, + ) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Token type '{token_scope}' not allowed for this endpoint. " + f"Allowed types: {', '.join(sorted(allowed_token_types))}", + ) + + if "ti:self" in security_scopes.scopes: + ti_self_id = str(request.path_params["task_instance_id"]) + if str(token.id) != ti_self_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Token subject does not match task instance ID", + ) + + return token + + +CurrentTIToken: TIToken = Depends(require_auth) + + +class ExecutionAPIRoute(APIRoute): + """ + Custom route class that precomputes allowed token types from Security scopes. + + Scopes prefixed with ``token:`` (e.g., ``token:execution``, ``token:workload``) + are extracted at route registration time and stored as ``allowed_token_types``. + If no ``token:*`` scopes are declared, defaults to ``{"execution"}``. + + ``require_auth`` reads ``route.allowed_token_types`` at request time. + """ + + allowed_token_types: frozenset[str] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + all_scopes: set[str] = set() + for dep in self.dependencies: + if isinstance(dep, SecurityParam): + all_scopes.update(dep.scopes or []) + + token_scopes = {s.removeprefix("token:") for s in all_scopes if s.startswith("token:")} + + if token_scopes and not token_scopes <= VALID_TOKEN_TYPES: + invalid = token_scopes - VALID_TOKEN_TYPES + raise ValueError(f"Invalid token types in Security scopes: {invalid}") + + self.allowed_token_types = frozenset(token_scopes) if token_scopes else frozenset({"execution"}) + + +async def get_team_name_dep(session: AsyncSessionDep, token=CurrentTIToken) -> str | None: + """Return the team name associated to the task (if any).""" + from airflow.configuration import conf + from airflow.models import DagModel, TaskInstance + from airflow.models.dagbundle import DagBundleModel + from airflow.models.team import Team + + if not conf.getboolean("core", "multi_team"): + return None + + stmt = ( + select(Team.name) + .select_from(TaskInstance) + .join(DagModel, DagModel.dag_id == TaskInstance.dag_id) + .join(DagBundleModel, DagBundleModel.name == DagModel.bundle_name) + .join(DagBundleModel.teams) + .where(TaskInstance.id == token.id) + ) + return await session.scalar(stmt) diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py b/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py index 9e26937b63c06..78bd0548df9d2 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/conftest.py @@ -16,51 +16,43 @@ # under the License. from __future__ import annotations -from unittest.mock import AsyncMock - import pytest +from fastapi import FastAPI, Request from fastapi.testclient import TestClient from airflow.api_fastapi.app import cached_app -from airflow.api_fastapi.auth.tokens import JWTValidator -from airflow.api_fastapi.execution_api.app import lifespan +from airflow.api_fastapi.execution_api.datamodels.token import TIToken +from airflow.api_fastapi.execution_api.security import _jwt_bearer + + +def _get_execution_api_app(root_app: FastAPI) -> FastAPI: + """Find the mounted execution API sub-app.""" + for route in root_app.routes: + if hasattr(route, "path") and route.path == "/execution": + return route.app + raise RuntimeError("Execution API sub-app not found") + + +@pytest.fixture +def exec_app(client): + """Return the execution API sub-app.""" + return _get_execution_api_app(client.app) @pytest.fixture def client(request: pytest.FixtureRequest): app = cached_app(apps="execution") + exec_app = _get_execution_api_app(app) - with TestClient(app, headers={"Authorization": "Bearer fake"}) as client: - auth = AsyncMock(spec=JWTValidator) - - # Create a side_effect function that dynamically extracts the task instance ID from validators - def smart_validated_claims(cred, validators=None): - # Extract task instance ID from validators if present - # This handles the JWTBearerTIPathDep case where the validator contains the task ID from the path - if ( - validators - and "sub" in validators - and isinstance(validators["sub"], dict) - and "value" in validators["sub"] - ): - return { - "sub": validators["sub"]["value"], - "exp": 9999999999, # Far future expiration - "iat": 1000000000, # Past issuance time - "aud": "test-audience", - } + async def mock_jwt_bearer(request: Request): + from uuid import UUID - # For other cases (like JWTBearerDep) where no specific validators are provided - # Return a default UUID with all required claims - return { - "sub": "00000000-0000-0000-0000-000000000000", - "exp": 9999999999, # Far future expiration - "iat": 1000000000, # Past issuance time - "aud": "test-audience", - } + ti_id = UUID(request.path_params.get("task_instance_id", "00000000-0000-0000-0000-000000000000")) + return TIToken(id=ti_id, claims={"sub": str(ti_id), "scope": "execution"}) - # Set the side_effect for avalidated_claims - auth.avalidated_claims.side_effect = smart_validated_claims - lifespan.registry.register_value(JWTValidator, auth) + exec_app.dependency_overrides[_jwt_bearer] = mock_jwt_bearer + with TestClient(app, headers={"Authorization": "Bearer fake"}) as client: yield client + + exec_app.dependency_overrides.pop(_jwt_bearer, None) diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py b/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py index 640d920137c7b..b0cb1d85c2e33 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py @@ -43,6 +43,23 @@ def test_access_api_contract(client): assert response.headers["airflow-api-version"] == bundle.versions[0].value +def test_ti_self_routes_have_task_instance_id_param(client): + """Every route with ti:self scope must have a {task_instance_id} path parameter.""" + from fastapi.params import Security as SecurityParam + from fastapi.routing import APIRoute + + app = client.app + + for route in app.routes: + if not isinstance(route, APIRoute): + continue + for dep in route.dependencies: + if isinstance(dep, SecurityParam) and "ti:self" in (dep.scopes or []): + assert "task_instance_id" in route.dependant.path_param_names, ( + f"Route {route.path} has ti:self scope but no {{task_instance_id}} path parameter" + ) + + class TestCorrelationIdMiddleware: def test_correlation_id_echoed_in_response_headers(self, client): """Test that correlation-id from request is echoed back in response headers.""" diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/test_security.py b/airflow-core/tests/unit/api_fastapi/execution_api/test_security.py new file mode 100644 index 0000000000000..8fff2c9f7322a --- /dev/null +++ b/airflow-core/tests/unit/api_fastapi/execution_api/test_security.py @@ -0,0 +1,138 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from uuid import UUID + +import pytest +from fastapi import APIRouter, FastAPI, Request, Security +from fastapi.testclient import TestClient + +from airflow.api_fastapi.execution_api.datamodels.token import TIToken +from airflow.api_fastapi.execution_api.security import ExecutionAPIRoute, _jwt_bearer, require_auth + + +class TestExecutionAPIRoute: + """Unit tests for ExecutionAPIRoute precomputing allowed_token_types from Security scopes.""" + + def test_defaults_to_execution_only(self): + route = ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[Security(require_auth)], + ) + assert route.allowed_token_types == frozenset({"execution"}) + + def test_extracts_token_scopes(self): + route = ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[ + Security(require_auth), + Security(require_auth, scopes=["token:execution", "token:workload"]), + ], + ) + assert route.allowed_token_types == frozenset({"execution", "workload"}) + + def test_ignores_non_token_scopes(self): + route = ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[ + Security(require_auth, scopes=["ti:self", "token:execution"]), + ], + ) + assert route.allowed_token_types == frozenset({"execution"}) + + def test_rejects_invalid_token_types(self): + with pytest.raises(ValueError, match="Invalid token types"): + ExecutionAPIRoute( + path="/test", + endpoint=lambda: None, + dependencies=[ + Security(require_auth, scopes=["token:bogus"]), + ], + ) + + +class TestTokenTypeScopeEnforcement: + """End-to-end: ExecutionAPIRoute + require_auth enforce token types via Security scopes.""" + + @pytest.fixture + def token_type_app(self): + """ + Mirrors the real router structure: an authenticated_router with Security(require_auth), + a child ti_id_router with ExecutionAPIRoute and ti:self, and a specific endpoint on that + router opting in to workload tokens via endpoint-level Security scopes. + """ + app = FastAPI() + + authenticated_router = APIRouter(dependencies=[Security(require_auth)]) + ti_id_router = APIRouter( + route_class=ExecutionAPIRoute, + dependencies=[Security(require_auth, scopes=["ti:self"])], + ) + + @ti_id_router.get("/{task_instance_id}/state") + def default_endpoint(task_instance_id: str): + return {"ok": True} + + @ti_id_router.get( + "/{task_instance_id}/run", + dependencies=[Security(require_auth, scopes=["token:execution", "token:workload"])], + ) + def workload_endpoint(task_instance_id: str): + return {"ok": True} + + authenticated_router.include_router(ti_id_router, prefix="/task-instances") + app.include_router(authenticated_router) + + return app + + TI_ID = "00000000-0000-0000-0000-000000000001" + + def _override_jwt(self, app, scope: str): + ti_id = self.TI_ID + + async def mock_jwt(request: Request): + return TIToken(id=UUID(ti_id), claims={"scope": scope}) + + app.dependency_overrides[_jwt_bearer] = mock_jwt + + def test_workload_token_rejected_on_default_route(self, token_type_app): + self._override_jwt(token_type_app, "workload") + client = TestClient(token_type_app) + + resp = client.get(f"/task-instances/{self.TI_ID}/state", headers={"Authorization": "Bearer fake"}) + assert resp.status_code == 403 + assert "Token type 'workload' not allowed" in resp.json()["detail"] + + def test_workload_token_accepted_on_opted_in_route(self, token_type_app): + self._override_jwt(token_type_app, "workload") + client = TestClient(token_type_app) + + resp = client.get(f"/task-instances/{self.TI_ID}/run", headers={"Authorization": "Bearer fake"}) + assert resp.status_code == 200 + + def test_execution_token_accepted_on_both_routes(self, token_type_app): + self._override_jwt(token_type_app, "execution") + client = TestClient(token_type_app) + + state = client.get(f"/task-instances/{self.TI_ID}/state", headers={"Authorization": "Bearer fake"}) + run = client.get(f"/task-instances/{self.TI_ID}/run", headers={"Authorization": "Bearer fake"}) + assert state.status_code == 200 + assert run.status_code == 200 diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py index ea1153f01cba5..d9ec3916187ee 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py @@ -76,13 +76,16 @@ def _create_asset_aliases(session, num: int = 2) -> None: @pytest.fixture -def client_with_extra_route(): ... +def _use_real_jwt_bearer(exec_app): + """Remove the mock jwt_bearer override so the real JWTBearer.__call__ runs.""" + from airflow.api_fastapi.execution_api.security import _jwt_bearer + exec_app.dependency_overrides.pop(_jwt_bearer, None) -def test_id_matches_sub_claim(client, session, create_task_instance): - # Test that this is validated at the router level, so we don't have to test it in each component - # We validate it is set correctly, and test it once +@pytest.mark.usefixtures("_use_real_jwt_bearer") +def test_id_matches_sub_claim(client, session, create_task_instance): + """Test that scope validation (ti:self) is enforced at the router level.""" ti = create_task_instance( task_id="test_ti_run_state_conflict_if_not_queued", state="queued", @@ -90,17 +93,10 @@ def test_id_matches_sub_claim(client, session, create_task_instance): session.commit() validator = mock.AsyncMock(spec=JWTValidator) - claims = {"sub": ti.id} - - def side_effect(cred, validators): - if not validators: - return claims - if str(validators["sub"]["value"]) != str(ti.id): - raise RuntimeError("Fake auth denied") - return claims - - validator.avalidated_claims.side_effect = side_effect - + validator.avalidated_claims.return_value = { + "sub": str(ti.id), + "scope": "execution", + } lifespan.registry.register_value(JWTValidator, validator) payload = { @@ -113,15 +109,10 @@ def side_effect(cred, validators): resp = client.patch("/execution/task-instances/9c230b40-da03-451d-8bd7-be30471be383/run", json=payload) assert resp.status_code == 403 - assert validator.avalidated_claims.call_args_list[1] == mock.call( - mock.ANY, {"sub": {"essential": True, "value": "9c230b40-da03-451d-8bd7-be30471be383"}} - ) validator.avalidated_claims.reset_mock() resp = client.patch(f"/execution/task-instances/{ti.id}/run", json=payload) - assert resp.status_code == 200, resp.json() - validator.avalidated_claims.assert_awaited() @@ -2925,3 +2916,88 @@ def test_ti_patch_rendered_map_index_empty_string(self, client, session, create_ ) assert response.status_code == 422 + + +@pytest.mark.usefixtures("_use_real_jwt_bearer") +class TestTokenTypeValidation: + """Test token scope enforcement (workload vs execution).""" + + def test_workload_scope_rejected_on_default_endpoints(self, client, session, create_task_instance): + """workload scoped tokens should be rejected on endpoints without token:workload Security scope.""" + ti = create_task_instance(task_id="test_ti_run_heartbeat", state=State.RUNNING) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "scope": "workload", + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = {"hostname": "test-host", "pid": 100} + resp = client.put(f"/execution/task-instances/{ti.id}/heartbeat", json=payload) + assert resp.status_code == 403 + assert "Token type 'workload' not allowed" in resp.json()["detail"] + + def test_execution_scope_accepted_on_all_endpoints(self, client, session, create_task_instance): + """execution scoped tokens should be able to call all endpoints.""" + ti = create_task_instance(task_id="test_ti_star", state=State.RUNNING) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "scope": "execution", + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = {"state": "success", "end_date": "2024-10-31T13:00:00Z"} + resp = client.patch(f"/execution/task-instances/{ti.id}/state", json=payload) + assert resp.status_code in [200, 204] + + def test_invalid_scope_value_rejected(self, client, session, create_task_instance): + """Tokens with unrecognized scope values should be rejected.""" + ti = create_task_instance(task_id="test_invalid_scope", state=State.QUEUED) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "scope": "bogus:scope", + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = { + "state": "running", + "hostname": "test-host", + "unixname": "test-user", + "pid": 100, + "start_date": "2024-10-31T12:00:00Z", + } + + resp = client.patch(f"/execution/task-instances/{ti.id}/run", json=payload) + assert resp.status_code == 403 + assert "Invalid token scope" in resp.json()["detail"] + + def test_no_scope_defaults_to_execution(self, client, session, create_task_instance): + """Tokens without scope claim should default to 'execution'.""" + ti = create_task_instance(task_id="test_no_scope", state=State.RUNNING) + session.commit() + + validator = mock.AsyncMock(spec=JWTValidator) + validator.avalidated_claims.side_effect = lambda cred, validators: { + "sub": str(ti.id), + "exp": 9999999999, + "iat": 1000000000, + } + lifespan.registry.register_value(JWTValidator, validator) + + payload = {"state": "success", "end_date": "2024-10-31T13:00:00Z"} + resp = client.patch(f"/execution/task-instances/{ti.id}/state", json=payload) + assert resp.status_code in [200, 204] diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py index 59b206441dea6..93cd8ca672e9c 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_variables.py @@ -41,15 +41,15 @@ def setup_method(): @pytest.fixture def access_denied(client): - from airflow.api_fastapi.execution_api.deps import JWTBearerDep from airflow.api_fastapi.execution_api.routes.variables import has_variable_access + from airflow.api_fastapi.execution_api.security import CurrentTIToken last_route = client.app.routes[-1] assert isinstance(last_route, Mount) assert isinstance(last_route.app, FastAPI) exec_app = last_route.app - async def _(request: Request, variable_key: str, token=JWTBearerDep): + async def _(request: Request, variable_key: str, token=CurrentTIToken): await has_variable_access(request, variable_key, token) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py index 554c2ad2c8437..2135cb970a48b 100644 --- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py +++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py @@ -48,8 +48,8 @@ def reset_db(): @pytest.fixture def access_denied(client): - from airflow.api_fastapi.execution_api.deps import JWTBearerDep from airflow.api_fastapi.execution_api.routes.xcoms import has_xcom_access + from airflow.api_fastapi.execution_api.security import CurrentTIToken last_route = client.app.routes[-1] assert isinstance(last_route.app, FastAPI) @@ -61,7 +61,7 @@ async def _( run_id: str = Path(), task_id: str = Path(), xcom_key: str = Path(alias="key"), - token=JWTBearerDep, + token=CurrentTIToken, ): await has_xcom_access(dag_id, run_id, task_id, xcom_key, request, token) raise HTTPException( From 900e5e222f8ed842918134572fd6a1c3c176cf0b Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Tue, 10 Mar 2026 14:43:46 +0100 Subject: [PATCH 038/595] Make example_xcom resistant to escaping issues. (#63200) --- .../src/airflow/example_dags/example_xcom.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/example_dags/example_xcom.py b/airflow-core/src/airflow/example_dags/example_xcom.py index e17ad56d8cab1..304e7fa0c6569 100644 --- a/airflow-core/src/airflow/example_dags/example_xcom.py +++ b/airflow-core/src/airflow/example_dags/example_xcom.py @@ -77,12 +77,22 @@ def pull_value_from_bash_push(ti=None): 'echo "value_by_return"', ) + # This example shows a safe way of passing XCom values to a BashOperator via environment variables. + # The values are templated into the bash_command and then set as environment variables for the + # command to use. This is a recommended pattern for passing XCom values to BashOperator, as it avoids + # issues with quoting and escaping that can arise when trying to directly template XCom values + # into. bash_pull = BashOperator( task_id="bash_pull", bash_command='echo "bash pull demo" && ' - f'echo "The xcom pushed manually is {XComArg(bash_push, key="manually_pushed_value")}" && ' - f'echo "The returned_value xcom is {XComArg(bash_push)}" && ' + "echo \"The xcom pushed manually is '$MANUALLY_PUSHED_VALUE'\" && " + "echo \"The returned_value xcom is '$RETURNED_VALUE'\" && " 'echo "finished"', + env={ + "MANUALLY_PUSHED_VALUE": str(XComArg(bash_push, key="manually_pushed_value")), + "RETURNED_VALUE": str(XComArg(bash_push)), + }, + append_env=True, do_xcom_push=False, ) From bbf289779ca91f5b03772908743d8a5804487b84 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Tue, 10 Mar 2026 21:56:56 +0800 Subject: [PATCH 039/595] fix(migration): disable disable_sqlite_fkeys for migration 0087 (#63256) --- airflow-core/docs/img/airflow_erd.sha256 | 2 +- airflow-core/docs/img/airflow_erd.svg | 788 +++++++++--------- ...hange_signed_url_template_from_varchar_.py | 38 +- 3 files changed, 415 insertions(+), 413 deletions(-) diff --git a/airflow-core/docs/img/airflow_erd.sha256 b/airflow-core/docs/img/airflow_erd.sha256 index a75e1304429ad..62481785d172d 100644 --- a/airflow-core/docs/img/airflow_erd.sha256 +++ b/airflow-core/docs/img/airflow_erd.sha256 @@ -1 +1 @@ -615d06d0b6fd8cf40d0e4f6f7f9eda2c56013fa390b437f569c9eb1a627f40c3 \ No newline at end of file +a1be27dc622032d8c01e617348bc82c07e5f86448e109a3c15b6753e2aba9bd5 \ No newline at end of file diff --git a/airflow-core/docs/img/airflow_erd.svg b/airflow-core/docs/img/airflow_erd.svg index f9fc89140cce3..63a3c13c26a27 100644 --- a/airflow-core/docs/img/airflow_erd.svg +++ b/airflow-core/docs/img/airflow_erd.svg @@ -4,11 +4,11 @@ - - + + %3 - + dag_bundle @@ -678,7 +678,7 @@ [TIMESTAMP] - + trigger:id--task_instance:trigger_id 0..N @@ -796,7 +796,7 @@ NOT NULL - + asset_alias:id--asset_alias_asset_event:alias_id 0..N @@ -947,7 +947,7 @@ NOT NULL - + asset:id--dag_schedule_asset_reference:asset_id 0..N @@ -1104,7 +1104,7 @@ NOT NULL - + asset_event:id--asset_alias_asset_event:event_id 0..N @@ -1127,7 +1127,7 @@ NOT NULL - + asset_event:id--dagrun_asset_event:event_id 0..N @@ -1411,7 +1411,7 @@ 1 - + dag:dag_id--dag_schedule_asset_reference:dag_id 0..N @@ -1725,7 +1725,7 @@ NOT NULL - + dag_version:id--dag_run:created_dag_version_id 0..N @@ -1869,14 +1869,14 @@ NOT NULL - + log_template:id--dag_run:log_template_id 0..N 1 - + dag_run:id--dagrun_asset_event:dag_run_id 0..N @@ -1926,18 +1926,18 @@ -dag_run:run_id--task_instance:run_id - -0..N -1 - - - dag_run:dag_id--task_instance:dag_id 0..N 1 + + +dag_run:run_id--task_instance:run_id + +0..N +1 + backfill_dag_run @@ -2268,31 +2268,31 @@ -task_instance:run_id--task_map:run_id - -0..N -1 +task_instance:task_id--task_map:task_id + +0..N +1 -task_instance:dag_id--task_map:dag_id - -0..N -1 - - - task_instance:map_index--task_map:map_index 0..N 1 + +task_instance:run_id--task_map:run_id + +0..N +1 + + -task_instance:task_id--task_map:task_id - -0..N -1 +task_instance:dag_id--task_map:dag_id + +0..N +1 @@ -2384,31 +2384,31 @@ -task_instance:run_id--xcom:run_id - -0..N -1 +task_instance:task_id--xcom:task_id + +0..N +1 -task_instance:dag_id--xcom:dag_id - -0..N -1 - - - task_instance:map_index--xcom:map_index - + 0..N 1 + +task_instance:run_id--xcom:run_id + +0..N +1 + + -task_instance:task_id--xcom:task_id - -0..N -1 +task_instance:dag_id--xcom:dag_id + +0..N +1 @@ -2621,18 +2621,18 @@ -task_instance:dag_id--task_instance_history:dag_id - -0..N -1 - - - task_instance:run_id--task_instance_history:run_id 0..N 1 + + +task_instance:dag_id--task_instance_history:dag_id + +0..N +1 + rendered_task_instance_fields @@ -2670,10 +2670,10 @@ -task_instance:run_id--rendered_task_instance_fields:run_id - -0..N -1 +task_instance:dag_id--rendered_task_instance_fields:dag_id + +0..N +1 @@ -2685,16 +2685,16 @@ task_instance:map_index--rendered_task_instance_fields:map_index - + 0..N 1 -task_instance:dag_id--rendered_task_instance_fields:dag_id - -0..N -1 +task_instance:run_id--rendered_task_instance_fields:run_id + +0..N +1 @@ -2885,466 +2885,470 @@ edge_worker - -edge_worker - -worker_name - - [VARCHAR(64)] - NOT NULL - -first_online - - [TIMESTAMP] - -jobs_active - - [INTEGER] - NOT NULL - -jobs_failed - - [INTEGER] - NOT NULL - -jobs_success - - [INTEGER] - NOT NULL - -jobs_taken - - [INTEGER] - NOT NULL - -last_update - - [TIMESTAMP] - -maintenance_comment - - [VARCHAR(1024)] - -queues - - [VARCHAR(256)] - -state - - [VARCHAR(20)] - NOT NULL - -sysinfo - - [VARCHAR(256)] + +edge_worker + +worker_name + + [VARCHAR(64)] + NOT NULL + +concurrency + + [INTEGER] + +first_online + + [TIMESTAMP] + +jobs_active + + [INTEGER] + NOT NULL + +jobs_failed + + [INTEGER] + NOT NULL + +jobs_success + + [INTEGER] + NOT NULL + +jobs_taken + + [INTEGER] + NOT NULL + +last_update + + [TIMESTAMP] + +maintenance_comment + + [VARCHAR(1024)] + +queues + + [VARCHAR(256)] + +state + + [VARCHAR(20)] + NOT NULL + +sysinfo + + [VARCHAR(256)] alembic_version_edge3 - -alembic_version_edge3 - -version_num - - [VARCHAR(32)] - NOT NULL + +alembic_version_edge3 + +version_num + + [VARCHAR(32)] + NOT NULL ab_user - -ab_user + +ab_user + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +active + + [BOOLEAN] -active - - [BOOLEAN] +changed_by_fk + + [INTEGER] -changed_by_fk - - [INTEGER] +changed_on + + [TIMESTAMP] -changed_on - - [TIMESTAMP] +created_by_fk + + [INTEGER] -created_by_fk - - [INTEGER] +created_on + + [TIMESTAMP] -created_on - - [TIMESTAMP] +email + + [VARCHAR(512)] + NOT NULL -email - - [VARCHAR(512)] - NOT NULL +fail_login_count + + [INTEGER] -fail_login_count - - [INTEGER] +first_name + + [VARCHAR(256)] + NOT NULL -first_name - - [VARCHAR(256)] - NOT NULL +last_login + + [TIMESTAMP] -last_login - - [TIMESTAMP] +last_name + + [VARCHAR(256)] + NOT NULL -last_name - - [VARCHAR(256)] - NOT NULL +login_count + + [INTEGER] -login_count - - [INTEGER] +password + + [VARCHAR(256)] -password - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL +username + + [VARCHAR(512)] + NOT NULL ab_user:id--ab_user:changed_by_fk - -0..N -{0,1} + +0..N +{0,1} ab_user:id--ab_user:created_by_fk - -0..N -{0,1} + +0..N +{0,1} ab_user_role - -ab_user_role + +ab_user_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +role_id + + [INTEGER] -role_id - - [INTEGER] - -user_id - - [INTEGER] +user_id + + [INTEGER] ab_user:id--ab_user_role:user_id - -0..N -{0,1} + +0..N +{0,1} ab_user_group - -ab_user_group + +ab_user_group + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +group_id + + [INTEGER] -group_id - - [INTEGER] - -user_id - - [INTEGER] +user_id + + [INTEGER] - + ab_user:id--ab_user_group:user_id - -0..N -{0,1} + +0..N +{0,1} ab_register_user - -ab_register_user + +ab_register_user + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +email + + [VARCHAR(512)] + NOT NULL -email - - [VARCHAR(512)] - NOT NULL +first_name + + [VARCHAR(256)] + NOT NULL -first_name - - [VARCHAR(256)] - NOT NULL +last_name + + [VARCHAR(256)] + NOT NULL -last_name - - [VARCHAR(256)] - NOT NULL +password + + [VARCHAR(256)] -password - - [VARCHAR(256)] +registration_date + + [TIMESTAMP] -registration_date - - [TIMESTAMP] +registration_hash + + [VARCHAR(256)] -registration_hash - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL +username + + [VARCHAR(512)] + NOT NULL ab_group - -ab_group + +ab_group + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +description + + [VARCHAR(512)] -description - - [VARCHAR(512)] +label + + [VARCHAR(150)] -label - - [VARCHAR(150)] - -name - - [VARCHAR(100)] - NOT NULL +name + + [VARCHAR(100)] + NOT NULL ab_group_role - -ab_group_role + +ab_group_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +group_id + + [INTEGER] -group_id - - [INTEGER] - -role_id - - [INTEGER] +role_id + + [INTEGER] - + ab_group:id--ab_group_role:group_id - -0..N -{0,1} + +0..N +{0,1} - + ab_group:id--ab_user_group:group_id - -0..N -{0,1} + +0..N +{0,1} ab_role - -ab_role + +ab_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(64)] - NOT NULL +name + + [VARCHAR(64)] + NOT NULL - + ab_role:id--ab_group_role:role_id - -0..N -{0,1} + +0..N +{0,1} ab_role:id--ab_user_role:role_id - -0..N -{0,1} + +0..N +{0,1} ab_permission_view_role - -ab_permission_view_role + +ab_permission_view_role + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +permission_view_id + + [INTEGER] -permission_view_id - - [INTEGER] - -role_id - - [INTEGER] +role_id + + [INTEGER] - + ab_role:id--ab_permission_view_role:role_id - -0..N -{0,1} + +0..N +{0,1} ab_permission - -ab_permission + +ab_permission + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(100)] - NOT NULL +name + + [VARCHAR(100)] + NOT NULL ab_permission_view - -ab_permission_view + +ab_permission_view + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +permission_id + + [INTEGER] + NOT NULL -permission_id - - [INTEGER] - NOT NULL - -view_menu_id - - [INTEGER] - NOT NULL +view_menu_id + + [INTEGER] + NOT NULL - + ab_permission:id--ab_permission_view:permission_id - -0..N -1 + +0..N +1 - + ab_permission_view:id--ab_permission_view_role:permission_view_id - -0..N -{0,1} + +0..N +{0,1} ab_view_menu - -ab_view_menu + +ab_view_menu + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(250)] - NOT NULL +name + + [VARCHAR(250)] + NOT NULL - + ab_view_menu:id--ab_permission_view:view_menu_id - -0..N -1 + +0..N +1 alembic_version_fab - -alembic_version_fab - -version_num - - [VARCHAR(32)] - NOT NULL + +alembic_version_fab + +version_num + + [VARCHAR(32)] + NOT NULL session - -session + +session + +id + + [INTEGER] + NOT NULL -id - - [INTEGER] - NOT NULL +data + + [BYTEA] -data - - [BYTEA] +expiry + + [TIMESTAMP] -expiry - - [TIMESTAMP] - -session_id - - [VARCHAR(255)] +session_id + + [VARCHAR(255)] diff --git a/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py b/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py index 9fae0722c8ca3..7966e10d1f2f2 100644 --- a/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py +++ b/airflow-core/src/airflow/migrations/versions/0087_3_1_8_change_signed_url_template_from_varchar_.py @@ -30,6 +30,8 @@ import sqlalchemy as sa from alembic import op +from airflow.migrations.utils import disable_sqlite_fkeys + # revision identifiers, used by Alembic. revision = "509b94a1042d" down_revision = "82dbd68e6171" @@ -40,27 +42,23 @@ def upgrade(): """Apply Change signed_url_template from VARCHAR(200) to TEXT.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("dag_bundle", schema=None) as batch_op: - batch_op.alter_column( - "signed_url_template", - existing_type=sa.VARCHAR(length=200), - type_=sa.Text(), - existing_nullable=True, - ) - - # ### end Alembic commands ### + with disable_sqlite_fkeys(op): + with op.batch_alter_table("dag_bundle", schema=None) as batch_op: + batch_op.alter_column( + "signed_url_template", + existing_type=sa.VARCHAR(length=200), + type_=sa.Text(), + existing_nullable=True, + ) def downgrade(): """Unapply Change signed_url_template from VARCHAR(200) to TEXT.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("dag_bundle", schema=None) as batch_op: - batch_op.alter_column( - "signed_url_template", - existing_type=sa.Text(), - type_=sa.VARCHAR(length=200), - existing_nullable=True, - ) - - # ### end Alembic commands ### + with disable_sqlite_fkeys(op): + with op.batch_alter_table("dag_bundle", schema=None) as batch_op: + batch_op.alter_column( + "signed_url_template", + existing_type=sa.Text(), + type_=sa.VARCHAR(length=200), + existing_nullable=True, + ) From 211b96ce63cd8884e4db912e9c36e46e6e48852d Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Tue, 10 Mar 2026 22:10:20 +0800 Subject: [PATCH 040/595] Filter backfills list by readable DAGs (#63003) --- .../core_api/routes/ui/backfills.py | 5 ++-- .../airflow/api_fastapi/core_api/security.py | 10 +++++++ .../core_api/routes/ui/test_backfills.py | 26 ++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py index 32b2891b9543a..02583a8355b9d 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py @@ -36,7 +36,7 @@ from airflow.api_fastapi.core_api.openapi.exceptions import ( create_openapi_http_exception_doc, ) -from airflow.api_fastapi.core_api.security import requires_access_backfill +from airflow.api_fastapi.core_api.security import ReadableBackfillsFilterDep, requires_access_backfill from airflow.models.backfill import Backfill backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills") @@ -56,6 +56,7 @@ def list_backfills_ui( SortParam, Depends(SortParam(["id"], Backfill).dynamic_depends()), ], + readable_backfills_filter: ReadableBackfillsFilterDep, session: SessionDep, dag_id: Annotated[FilterParam[str | None], Depends(filter_param_factory(Backfill.dag_id, str | None))], active: Annotated[ @@ -65,7 +66,7 @@ def list_backfills_ui( ) -> BackfillCollectionResponse: select_stmt, total_entries = paginated_select( statement=select(Backfill).options(joinedload(Backfill.dag_model)), - filters=[dag_id, active], + filters=[dag_id, active, readable_backfills_filter], order_by=order_by, offset=offset, limit=limit, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/security.py b/airflow-core/src/airflow/api_fastapi/core_api/security.py index 0e59fb3550c9f..774cfa9ed6a74 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/security.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py @@ -238,6 +238,13 @@ def to_orm(self, select: Select) -> Select: return select.where(DagVersion.dag_id.in_(self.value or set())) +class PermittedBackfillFilter(PermittedDagFilter): + """A parameter that filters the permitted backfills for the user.""" + + def to_orm(self, select: Select) -> Select: + return select.where(Backfill.dag_id.in_(self.value or set())) + + def permitted_dag_filter_factory( method: ResourceMethod, filter_class=PermittedDagFilter ) -> Callable[[BaseUser, BaseAuthManager], PermittedDagFilter]: @@ -282,6 +289,9 @@ def depends_permitted_dags_filter( ReadableDagVersionsFilterDep = Annotated[ PermittedDagVersionFilter, Depends(permitted_dag_filter_factory("GET", PermittedDagVersionFilter)) ] +ReadableBackfillsFilterDep = Annotated[ + PermittedBackfillFilter, Depends(permitted_dag_filter_factory("GET", PermittedBackfillFilter)) +] def requires_access_backfill( diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py index b14ef9cf1a069..21ae10d23b3a9 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py @@ -153,7 +153,7 @@ def test_should_response_200( expected_response = [] for backfill in response_params: expected_response.append(backfill_responses[backfill]) - with assert_queries_count(2 if test_params.get("dag_id") is None else 3): + with assert_queries_count(3 if test_params.get("dag_id") is None else 4): response = test_client.get("/backfills", params=test_params) assert response.status_code == 200 assert response.json() == { @@ -168,3 +168,27 @@ def test_should_response_401(self, unauthenticated_test_client): def test_should_response_403(self, unauthorized_test_client): response = unauthorized_test_client.get("/backfills", params={}) assert response.status_code == 403 + + @mock.patch("airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids") + def test_should_only_return_authorized_dag_backfills( + self, mock_get_authorized_dag_ids, test_client, session, testing_dag_bundle + ): + dags = self._create_dag_models() + from_date = timezone.utcnow() + to_date = timezone.utcnow() + backfills = [ + Backfill(dag_id=dags[0].dag_id, from_date=from_date, to_date=to_date), + Backfill(dag_id=dags[1].dag_id, from_date=from_date, to_date=to_date), + Backfill(dag_id=dags[2].dag_id, from_date=from_date, to_date=to_date), + ] + session.add_all(backfills) + session.commit() + + mock_get_authorized_dag_ids.return_value = {"TEST_DAG_2", "TEST_DAG_3"} + response = test_client.get("/backfills") + + mock_get_authorized_dag_ids.assert_called_once_with(user=mock.ANY, method="GET") + assert response.status_code == 200 + body = response.json() + assert body["total_entries"] == 2 + assert {b["dag_id"] for b in body["backfills"]} == {"TEST_DAG_2", "TEST_DAG_3"} From 4f4c8a91beb7ef241db58bdc5e33764bd11debf0 Mon Sep 17 00:00:00 2001 From: Elad Kalif <45845474+eladkal@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:16:23 +0200 Subject: [PATCH 041/595] Add kacpermuda to triage team (#63271) --- .asf.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.asf.yaml b/.asf.yaml index a99d2462db09c..b8fb1be32036f 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -171,6 +171,7 @@ github: - gyli - jroachgolf84 - Dev-iL + - kacpermuda notifications: jobs: jobs@airflow.apache.org From 429f5c6b610122d14cae25e50f30063c46b33410 Mon Sep 17 00:00:00 2001 From: Mathieu Monet <60776491+stegololz@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:19:39 +0100 Subject: [PATCH 042/595] Add TTL cache with single-flight dedup to Keycloak filter_authorized_dag_ids (#63184) --- .../providers/keycloak/auth_manager/cache.py | 84 ++++++++++ .../auth_manager/keycloak_auth_manager.py | 19 +++ .../unit/keycloak/auth_manager/test_cache.py | 137 ++++++++++++++++ .../test_keycloak_auth_manager.py | 146 ++++++++++++++++++ 4 files changed, 386 insertions(+) create mode 100644 providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py create mode 100644 providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py diff --git a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py new file mode 100644 index 0000000000000..75ccbf99c51d1 --- /dev/null +++ b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cache.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import threading +import time +from collections.abc import Callable + +_CACHE_TTL_SECONDS = 30 +_SINGLE_FLIGHT_TIMEOUT_SECONDS = 60 + +# Maps cache keys to (timestamp, result) pairs for TTL-based expiration. +_cache: dict[tuple, tuple[float, frozenset[str]]] = {} +# Tracks in-flight requests: maps cache keys to events that waiting threads block on. +_pending_requests: dict[tuple, threading.Event] = {} +_cache_lock = threading.Lock() + + +def _cache_get(key: tuple) -> frozenset[str] | None: + entry = _cache.get(key) + if entry and (time.monotonic() - entry[0]) < _CACHE_TTL_SECONDS: + return entry[1] + return None + + +def _cache_set(key: tuple, value: frozenset[str]) -> None: + with _cache_lock: + _cache[key] = (time.monotonic(), value) + now = time.monotonic() + for k in [k for k, (ts, _) in _cache.items() if now - ts > _CACHE_TTL_SECONDS * 2]: + _cache.pop(k, None) + + +def single_flight(cache_key: tuple, query_keycloak: Callable[[], set[str]]) -> set[str]: + """Return cached result, wait for a pending request, or run the query ourselves.""" + # Fast path: check cache without lock + cached = _cache_get(cache_key) + if cached is not None: + return set(cached) + + with _cache_lock: + cached = _cache_get(cache_key) + if cached is not None: + return set(cached) + + event = _pending_requests.get(cache_key) + if event is not None: + is_worker = False + else: + event = threading.Event() + _pending_requests[cache_key] = event + is_worker = True + + if not is_worker: + # Wait for the other thread to finish + event.wait(timeout=_SINGLE_FLIGHT_TIMEOUT_SECONDS) + cached = _cache_get(cache_key) + if cached is not None: + return set(cached) + # If the other thread failed, fall through and do the work ourselves + + try: + result = query_keycloak() + _cache_set(cache_key, frozenset(result)) + return result + finally: + with _cache_lock: + event = _pending_requests.pop(cache_key, None) + if event is not None: + event.set() diff --git a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py index bc39802084428..9844873c031a6 100644 --- a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py +++ b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py @@ -49,6 +49,7 @@ except ModuleNotFoundError: from airflow.configuration import conf from airflow.exceptions import AirflowException +from airflow.providers.keycloak.auth_manager.cache import single_flight from airflow.providers.keycloak.auth_manager.constants import ( CONF_CLIENT_ID_KEY, CONF_CLIENT_SECRET_KEY, @@ -440,6 +441,24 @@ def _is_authorized( ) raise AirflowException(f"Unexpected error: {resp.status_code} - {resp.text}") + def filter_authorized_dag_ids( + self, + *, + dag_ids: set[str], + user: KeycloakAuthManagerUser, + method: ResourceMethod = "GET", + team_name: str | None = None, + ) -> set[str]: + cache_key = (user.get_id(), method, team_name, frozenset(dag_ids)) + + def query_keycloak() -> set[str]: + kwargs: dict = dict(dag_ids=dag_ids, user=user, method=method) + if team_name is not None: + kwargs["team_name"] = team_name + return super(KeycloakAuthManager, self).filter_authorized_dag_ids(**kwargs) + + return single_flight(cache_key, query_keycloak) + def _is_batch_authorized( self, *, diff --git a/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py b/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py new file mode 100644 index 0000000000000..9125d8d4b72c4 --- /dev/null +++ b/providers/keycloak/tests/unit/keycloak/auth_manager/test_cache.py @@ -0,0 +1,137 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import threading +import time + +import pytest + +from airflow.providers.keycloak.auth_manager import cache as cache_module +from airflow.providers.keycloak.auth_manager.cache import single_flight + + +@pytest.fixture(autouse=True) +def _clear_cache(): + cache_module._cache.clear() + cache_module._pending_requests.clear() + yield + cache_module._cache.clear() + cache_module._pending_requests.clear() + + +class TestSingleFlight: + def test_returns_query_result(self): + result = single_flight(("key",), lambda: {"a", "b"}) + assert result == {"a", "b"} + + def test_cache_hit(self): + call_count = 0 + + def query(): + nonlocal call_count + call_count += 1 + return {"a"} + + single_flight(("key",), query) + single_flight(("key",), query) + + assert call_count == 1 + + def test_different_keys_not_cached(self): + call_count = 0 + + def query(): + nonlocal call_count + call_count += 1 + return {"a"} + + single_flight(("key1",), query) + single_flight(("key2",), query) + + assert call_count == 2 + + def test_cache_expires(self): + call_count = 0 + + def query(): + nonlocal call_count + call_count += 1 + return {"a"} + + single_flight(("key",), query) + + # Expire the cache entry by backdating its timestamp + for k in cache_module._cache: + ts, val = cache_module._cache[k] + cache_module._cache[k] = (ts - cache_module._CACHE_TTL_SECONDS - 1, val) + + single_flight(("key",), query) + assert call_count == 2 + + def test_concurrent_dedup(self): + """Multiple threads with the same key coalesce into one call.""" + gate = threading.Event() + call_count = 0 + + def slow_query(): + nonlocal call_count + call_count += 1 + gate.wait(timeout=5) + return {"a"} + + results = [None] * 5 + errors = [] + + def run(index): + try: + results[index] = single_flight(("key",), slow_query) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=run, args=(i,)) for i in range(5)] + for t in threads: + t.start() + + time.sleep(0.1) + gate.set() + + for t in threads: + t.join(timeout=5) + + assert not errors, f"Threads raised errors: {errors}" + for r in results: + assert r == {"a"} + assert call_count == 1 + + def test_failed_query_allows_retry(self): + """If the worker thread fails, another thread can retry.""" + call_count = 0 + + def failing_then_ok(): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise ValueError("boom") + return {"a"} + + with pytest.raises(ValueError, match="boom"): + single_flight(("key",), failing_then_ok) + + result = single_flight(("key",), failing_then_ok) + assert result == {"a"} + assert call_count == 2 diff --git a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py index b1c28714f6bcb..9c8ed9dd5b610 100644 --- a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py +++ b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py @@ -51,6 +51,7 @@ from airflow.providers.common.compat.sdk import AirflowException except ModuleNotFoundError: from airflow.exceptions import AirflowException +from airflow.providers.keycloak.auth_manager import cache as cache_module from airflow.providers.keycloak.auth_manager.constants import ( CONF_CLIENT_ID_KEY, CONF_CLIENT_SECRET_KEY, @@ -109,6 +110,16 @@ def user(): return user +@pytest.fixture(autouse=True) +def _clear_filter_cache(): + """Clear module-level single-flight cache between tests.""" + cache_module._cache.clear() + cache_module._pending_requests.clear() + yield + cache_module._cache.clear() + cache_module._pending_requests.clear() + + class TestKeycloakAuthManager: def test_deserialize_user(self, auth_manager): result = auth_manager.deserialize_user( @@ -960,3 +971,138 @@ def test_get_teams(self, mock_get_keycloak_client, auth_manager_multi_team): headers={"Authorization": "Bearer pat-token"}, timeout=5, ) + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200, 200], True), + ([200, 403, 200], False), + ([401, 200, 200], False), + ([200, 200, 401], False), + ], + ) + def test_batch_is_authorized_dag(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [{"method": "GET", "details": DagDetails(id=f"dag_{i}")} for i in range(len(status_codes))] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_dag(requests, user=user) + + assert result == expected + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200], True), + ([200, 403], False), + ], + ) + def test_batch_is_authorized_connection(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [ + {"method": "GET", "details": ConnectionDetails(conn_id=f"conn_{i}")} + for i in range(len(status_codes)) + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_connection(requests, user=user) + + assert result == expected + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200], True), + ([403, 200], False), + ], + ) + def test_batch_is_authorized_pool(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [ + {"method": "GET", "details": PoolDetails(name=f"pool_{i}")} for i in range(len(status_codes)) + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_pool(requests, user=user) + + assert result == expected + + @pytest.mark.parametrize( + ("status_codes", "expected"), + [ + ([200, 200], True), + ([200, 401], False), + ], + ) + def test_batch_is_authorized_variable(self, status_codes, expected, auth_manager, user): + return_values = [code == 200 for code in status_codes] + + requests = [ + {"method": "GET", "details": VariableDetails(key=f"var_{i}")} for i in range(len(status_codes)) + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", side_effect=return_values): + result = auth_manager.batch_is_authorized_variable(requests, user=user) + + assert result == expected + + def test_batch_is_authorized_dag_empty_requests(self, auth_manager, user): + result = auth_manager.batch_is_authorized_dag([], user=user) + assert result is True + + def test_batch_is_authorized_dag_with_access_entity(self, auth_manager, user): + requests = [ + { + "method": "GET", + "access_entity": DagAccessEntity.TASK_INSTANCE, + "details": DagDetails(id="dag_1"), + } + ] + + with patch.object(KeycloakAuthManager, "_is_authorized", return_value=True) as mock_is_authorized: + result = auth_manager.batch_is_authorized_dag(requests, user=user) + + assert result is True + # Verify the call included the dag_entity attribute + call_kwargs = mock_is_authorized.call_args + assert call_kwargs.kwargs["attributes"] == {"dag_entity": "TASK_INSTANCE"} + + @patch.object( + KeycloakAuthManager, + "is_authorized_dag", + side_effect=lambda *, details, **kw: {"dag_0": True, "dag_1": False, "dag_2": True}[details.id], + ) + def test_filter_authorized_dag_ids(self, mock_is_authorized, auth_manager, user): + result = auth_manager.filter_authorized_dag_ids( + dag_ids={"dag_0", "dag_1", "dag_2"}, user=user, method="GET" + ) + + assert result == {"dag_0", "dag_2"} + assert mock_is_authorized.call_count == 3 + + def test_filter_authorized_dag_ids_empty(self, auth_manager, user): + result = auth_manager.filter_authorized_dag_ids(dag_ids=set(), user=user, method="GET") + assert result == set() + + @patch.object(KeycloakAuthManager, "is_authorized_dag", return_value=False) + def test_filter_authorized_dag_ids_all_denied(self, mock_is_authorized, auth_manager, user): + result = auth_manager.filter_authorized_dag_ids(dag_ids={"dag_0", "dag_1"}, user=user, method="GET") + + assert result == set() + assert mock_is_authorized.call_count == 2 + + @patch.object(KeycloakAuthManager, "is_authorized_dag", return_value=True) + def test_filter_authorized_dag_ids_cache_hit(self, mock_is_authorized, auth_manager, user): + """Second call with same args should return cached result without hitting Keycloak.""" + dag_ids = {"dag_0", "dag_1"} + + result1 = auth_manager.filter_authorized_dag_ids(dag_ids=dag_ids, user=user, method="GET") + result2 = auth_manager.filter_authorized_dag_ids(dag_ids=dag_ids, user=user, method="GET") + + assert result1 == dag_ids + assert result2 == dag_ids + # is_authorized_dag should only be called for the first invocation (2 dag_ids × 1 call) + assert mock_is_authorized.call_count == 2 From 39aa7a6206d5a440e32684ef9ee48beaa799466e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:45:43 +0100 Subject: [PATCH 043/595] chore(deps): bump @tanstack/react-virtual (#63264) Bumps the core-ui-package-updates group with 1 update in the /airflow-core/src/airflow/ui directory: [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual). Updates `@tanstack/react-virtual` from 3.13.19 to 3.13.21 - [Release notes](https://github.com/TanStack/virtual/releases) - [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md) - [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.13.21/packages/react-virtual) --- updated-dependencies: - dependency-name: "@tanstack/react-virtual" dependency-version: 3.13.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: core-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- airflow-core/src/airflow/ui/package.json | 2 +- airflow-core/src/airflow/ui/pnpm-lock.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 97c6685793ea3..4ec9f5c0fb188 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -33,7 +33,7 @@ "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.19", + "@tanstack/react-virtual": "^3.13.21", "@visx/group": "^3.12.0", "@visx/shape": "^3.12.0", "@xyflow/react": "^12.10.1", diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index 4a698017fa63c..bfaefb374b46e 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-virtual': - specifier: ^3.13.19 - version: 3.13.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.13.21 + version: 3.13.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@visx/group': specifier: ^3.12.0 version: 3.12.0(react@19.2.4) @@ -1216,8 +1216,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.19': - resolution: {integrity: sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==} + '@tanstack/react-virtual@3.13.21': + resolution: {integrity: sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1226,8 +1226,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.19': - resolution: {integrity: sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==} + '@tanstack/virtual-core@3.13.21': + resolution: {integrity: sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -5470,15 +5470,15 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/react-virtual@3.13.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-virtual@3.13.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/virtual-core': 3.13.19 + '@tanstack/virtual-core': 3.13.21 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.19': {} + '@tanstack/virtual-core@3.13.21': {} '@testing-library/dom@10.4.0': dependencies: From 4527afd5777b959ba45c3b9429f06939908e4220 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 15:02:40 +0000 Subject: [PATCH 044/595] Add `breeze registry backfill` command for older provider versions (#63269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new breeze subcommand that extracts runtime parameters and connection types for previously released provider versions using `uv run --with` — no Docker or breeze CI image needed. Also includes: - Unit tests for all helper functions (16 tests) - Breeze docs for the backfill command - GitHub Actions workflow (registry-backfill.yml) that runs providers in parallel via matrix strategy, then publishes versions.json - Fix providerVersions.js to use runtime module_counts from modules.json instead of AST-based counts from providers.json Two issues: - `tomllib` is Python 3.11+; use try/except fallback to `tomli` (same pattern as other breeze modules) - `TestReadProviderYamlInfo` tests used real filesystem paths that depend on `tomllib`; replaced with `tmp_path`-based mock files --- .github/workflows/registry-backfill.yml | 266 ++++++++++++++++++ dev/breeze/doc/11_registry_tasks.rst | 41 +++ dev/breeze/doc/images/output_registry.svg | 24 +- dev/breeze/doc/images/output_registry.txt | 2 +- .../doc/images/output_registry_backfill.svg | 126 +++++++++ .../doc/images/output_registry_backfill.txt | 1 + ...utput_setup_check-all-params-in-groups.svg | 4 +- ...utput_setup_check-all-params-in-groups.txt | 2 +- ...output_setup_regenerate-command-images.svg | 2 +- ...output_setup_regenerate-command-images.txt | 2 +- .../commands/registry_commands.py | 191 ++++++++++++- .../commands/registry_commands_config.py | 10 + dev/breeze/tests/test_registry_backfill.py | 189 +++++++++++++ registry/src/_data/providerVersions.js | 11 + 14 files changed, 857 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/registry-backfill.yml create mode 100644 dev/breeze/doc/images/output_registry_backfill.svg create mode 100644 dev/breeze/doc/images/output_registry_backfill.txt create mode 100644 dev/breeze/tests/test_registry_backfill.py diff --git a/.github/workflows/registry-backfill.yml b/.github/workflows/registry-backfill.yml new file mode 100644 index 0000000000000..5a0b39d661f05 --- /dev/null +++ b/.github/workflows/registry-backfill.yml @@ -0,0 +1,266 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +--- +name: Registry Backfill +on: # yamllint disable-line rule:truthy + workflow_dispatch: + inputs: + destination: + description: > + Publish to live or staging S3 bucket + required: true + type: choice + options: + - staging + - live + default: staging + providers: + description: > + Space-separated provider IDs + (e.g. 'amazon google databricks') + required: true + type: string + versions: + description: > + Space-separated versions to backfill + (e.g. '9.15.0 9.14.0'). Applied to ALL providers. + required: true + type: string + +permissions: + contents: read + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + bucket: ${{ steps.destination.outputs.bucket }} + steps: + - name: "Build provider matrix" + id: matrix + env: + PROVIDERS: ${{ inputs.providers }} + run: | + MATRIX=$(echo "${PROVIDERS}" \ + | tr ' ' '\n' | jq -R . \ + | jq -cs '{"provider": .}') + echo "matrix=${MATRIX}" >> "${GITHUB_OUTPUT}" + + - name: "Determine S3 destination" + id: destination + env: + DESTINATION: ${{ inputs.destination }} + run: | + if [[ "${DESTINATION}" == "live" ]]; then + URL="s3://live-docs-airflow-apache-org" + else + URL="s3://staging-docs-airflow-apache-org" + fi + echo "bucket=${URL}/registry/" \ + >> "${GITHUB_OUTPUT}" + + backfill: + needs: prepare + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }} + name: "Backfill ${{ matrix.provider }}" + if: > + contains(fromJSON('[ + "ashb", + "bugraoz93", + "eladkal", + "ephraimbuddy", + "jedcunningham", + "jscheffl", + "kaxil", + "pierrejeambrun", + "shahar1", + "potiuk", + "utkarsharma2", + "vincbeck" + ]'), github.event.sender.login) + steps: + - name: "Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: "Fetch provider tags" + env: + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + for VERSION in ${VERSIONS}; do + TAG="providers-${PROVIDER}/${VERSION}" + echo "Fetching tag: ${TAG}" + git fetch origin tag "${TAG}" \ + 2>/dev/null || echo "Tag not found" + done + + - name: "Install uv" + uses: astral-sh/setup-uv@bd01e18f51369d5765a7df3681d34498e332e27e # v6.3.1 + + - name: "Install Breeze" + uses: ./.github/actions/breeze + with: + python-version: "3.12" + + - name: "Install AWS CLI v2" + run: | + curl -sSf \ + "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \ + -o /tmp/awscliv2.zip + unzip -q /tmp/awscliv2.zip -d /tmp + rm /tmp/awscliv2.zip + sudo /tmp/aws/install --update + rm -rf /tmp/aws/ + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - name: "Download existing providers.json" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + run: | + aws s3 cp \ + "${S3_BUCKET}api/providers.json" \ + dev/registry/providers.json || true + + - name: "Extract version metadata from git tags" + env: + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + VERSION_ARGS="" + for VERSION in ${VERSIONS}; do + VERSION_ARGS="${VERSION_ARGS} --version ${VERSION}" + done + uv run python dev/registry/extract_versions.py \ + --provider "${PROVIDER}" ${VERSION_ARGS} || true + + - name: "Run breeze registry backfill" + env: + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + VERSION_ARGS="" + for VERSION in ${VERSIONS}; do + VERSION_ARGS="${VERSION_ARGS} --version ${VERSION}" + done + breeze registry backfill \ + --provider "${PROVIDER}" ${VERSION_ARGS} + + - name: "Download data files from S3 for build" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + run: | + aws s3 cp \ + "${S3_BUCKET}api/providers.json" \ + registry/src/_data/providers.json + aws s3 cp \ + "${S3_BUCKET}api/modules.json" \ + registry/src/_data/modules.json + + - name: "Setup pnpm" + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + with: + version: 9 + + - name: "Setup Node.js" + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: 'registry/pnpm-lock.yaml' + + - name: "Install Node.js dependencies" + working-directory: registry + run: pnpm install --frozen-lockfile + + - name: "Build registry site" + working-directory: registry + env: + REGISTRY_PATH_PREFIX: "/registry/" + run: pnpm build + + - name: "Sync backfilled version pages to S3" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + CACHE_CONTROL: "public, max-age=300" + VERSIONS: ${{ inputs.versions }} + PROVIDER: ${{ matrix.provider }} + run: | + for VERSION in ${VERSIONS}; do + echo "Syncing ${PROVIDER}/${VERSION}..." + aws s3 sync \ + "registry/_site/providers/${PROVIDER}/${VERSION}/" \ + "${S3_BUCKET}providers/${PROVIDER}/${VERSION}/" \ + --cache-control "${CACHE_CONTROL}" + aws s3 sync \ + "registry/_site/api/providers/${PROVIDER}/${VERSION}/" \ + "${S3_BUCKET}api/providers/${PROVIDER}/${VERSION}/" \ + --cache-control "${CACHE_CONTROL}" + done + + publish-versions: + needs: [prepare, backfill] + runs-on: ubuntu-latest + name: "Publish versions.json" + steps: + - name: "Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: "Install Breeze" + uses: ./.github/actions/breeze + with: + python-version: "3.12" + + - name: "Install AWS CLI v2" + run: | + curl -sSf \ + "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \ + -o /tmp/awscliv2.zip + unzip -q /tmp/awscliv2.zip -d /tmp + rm /tmp/awscliv2.zip + sudo /tmp/aws/install --update + rm -rf /tmp/aws/ + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + aws-access-key-id: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - name: "Publish version metadata" + env: + S3_BUCKET: ${{ needs.prepare.outputs.bucket }} + run: > + breeze registry publish-versions + --s3-bucket "${S3_BUCKET}" diff --git a/dev/breeze/doc/11_registry_tasks.rst b/dev/breeze/doc/11_registry_tasks.rst index c64828a8b7b07..6b01d9065dcd6 100644 --- a/dev/breeze/doc/11_registry_tasks.rst +++ b/dev/breeze/doc/11_registry_tasks.rst @@ -50,6 +50,47 @@ Example usage: # Extract with a specific Python version breeze registry extract-data --python 3.12 +Backfilling older versions +.......................... + +The ``breeze registry backfill`` command extracts runtime parameters and connection +types for older provider versions without Docker. It uses ``uv run --with`` to +install the specific provider version in a temporary environment and runs +``extract_parameters.py`` and ``extract_connections.py``. + +This is useful when you need to add pages for previously released versions that +were not included in the initial registry build. + +.. image:: ./images/output_registry_backfill.svg + :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_registry_backfill.svg + :width: 100% + :alt: Breeze registry backfill + +Example usage: + +.. code-block:: bash + + # Backfill a single version + breeze registry backfill --provider amazon --version 9.15.0 + + # Backfill multiple versions at once + breeze registry backfill --provider amazon --version 9.15.0 --version 9.14.0 --version 9.13.0 + + # Backfill a hyphenated provider + breeze registry backfill --provider microsoft-azure --version 11.0.0 + +Output is written to ``registry/src/_data/versions/{provider}/{version}/``: + +- ``parameters.json`` — operator/sensor/hook parameters +- ``connections.json`` — connection type definitions + +After backfilling, you still need to: + +1. Extract metadata from git tags: ``uv run python dev/registry/extract_versions.py --provider {id} --version {version}`` +2. Build the Eleventy site: ``cd registry && pnpm build`` +3. Sync new version pages to S3 +4. Run ``breeze registry publish-versions`` to update version dropdowns + Publishing version metadata .......................... diff --git a/dev/breeze/doc/images/output_registry.svg b/dev/breeze/doc/images/output_registry.svg index 80d5d4def0813..e4b4f92c4f861 100644 --- a/dev/breeze/doc/images/output_registry.svg +++ b/dev/breeze/doc/images/output_registry.svg @@ -1,4 +1,4 @@ - + diff --git a/dev/breeze/doc/images/output_registry.txt b/dev/breeze/doc/images/output_registry.txt index 6888f6b8f6fb7..dae5504430be8 100644 --- a/dev/breeze/doc/images/output_registry.txt +++ b/dev/breeze/doc/images/output_registry.txt @@ -1 +1 @@ -94b4d28badb1f32f4e3c2d24bf337d78 +297843509448a55e7941eed3c0485df8 diff --git a/dev/breeze/doc/images/output_registry_backfill.svg b/dev/breeze/doc/images/output_registry_backfill.svg new file mode 100644 index 0000000000000..12b49bb040261 --- /dev/null +++ b/dev/breeze/doc/images/output_registry_backfill.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Command: registry backfill + + + + + + + + + + +Usage:breeze registry backfill[OPTIONS] + +Extract runtime parameters and connections for older provider versions. Uses 'uv run --with' to install the specific  +version in a temporary environment and runs extract_parameters.py + extract_connections.py. No Docker needed. + +╭─ Backfill flags ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +*--providerProvider ID (e.g. 'amazon', 'google', 'microsoft-azure'). [required](TEXT) +*--version Version(s) to extract. Can be specified multiple times: --version 9.21.0 --version 9.20.0 [required] +(TEXT) +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--verbose-vPrint verbose information about performed steps. +--dry-run-DIf dry-run is set, commands are only printed, not executed. +--help   -hShow this message and exit. +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/dev/breeze/doc/images/output_registry_backfill.txt b/dev/breeze/doc/images/output_registry_backfill.txt new file mode 100644 index 0000000000000..78e2c611d7680 --- /dev/null +++ b/dev/breeze/doc/images/output_registry_backfill.txt @@ -0,0 +1 @@ +e83ed21dca79179e4d064a17f8cd08be diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg index 34ca9601a7928..b33ae7f03e5f7 100644 --- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg +++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg @@ -203,8 +203,8 @@ | k8s:create-cluster | k8s:delete-cluster | k8s:deploy-airflow | k8s:dev | k8s:k9s | k8s:logs |  k8s:run-complete-tests | k8s:setup-env | k8s:shell | k8s:status | k8s:tests | k8s:upload-k8s-image | pr | pr:auto-triage | prod-image | prod-image:build | prod-image:load | prod-image:pull | prod-image:save |  -prod-image:verify | registry | registry:extract-data | registry:publish-versions | release-management |  -release-management:add-back-references | release-management:check-release-files |  +prod-image:verify | registry | registry:backfill | registry:extract-data | registry:publish-versions |  +release-management | release-management:add-back-references | release-management:check-release-files |  release-management:clean-old-provider-artifacts | release-management:constraints-version-check |  release-management:create-minor-branch | release-management:generate-constraints |  release-management:generate-issue-content-core | release-management:generate-issue-content-helm-chart |  diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt index 71800db5118f5..4b4b042063c05 100644 --- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt +++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt @@ -1 +1 @@ -37967154c159533e69675ebf1a2ad104 +acfe23e5b4622df765994caf52f455d2 diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg index b08e9e63156aa..d44be5b47add9 100644 --- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg +++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg @@ -223,7 +223,7 @@ k8s:deploy-airflow | k8s:dev | k8s:k9s | k8s:logs | k8s:run-complete-tests | k8s:setup-env | k8s:shell | k8s:status | k8s:tests | k8s:upload-k8s-image | pr | pr:auto-triage | prod-image | prod-image:build  | prod-image:load | prod-image:pull | prod-image:save | prod-image:verify | registry |  -registry:extract-data | registry:publish-versions | release-management |  +registry:backfill | registry:extract-data | registry:publish-versions | release-management |  release-management:add-back-references | release-management:check-release-files |  release-management:clean-old-provider-artifacts | release-management:constraints-version-check |  release-management:create-minor-branch | release-management:generate-constraints |  diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt index ac78480ec0c36..5cb537dfca986 100644 --- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt +++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt @@ -1 +1 @@ -5bd6b8a7281b30045aadce6f7c2576c6 +cd2b1818493fedb67afad21a6a46f98d diff --git a/dev/breeze/src/airflow_breeze/commands/registry_commands.py b/dev/breeze/src/airflow_breeze/commands/registry_commands.py index 68b74f6a1139a..b09b4be4c183b 100644 --- a/dev/breeze/src/airflow_breeze/commands/registry_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/registry_commands.py @@ -16,8 +16,10 @@ # under the License. from __future__ import annotations +import json import sys import uuid +from pathlib import Path import click @@ -27,6 +29,8 @@ from airflow_breeze.utils.ci_group import ci_group from airflow_breeze.utils.click_utils import BreezeGroup from airflow_breeze.utils.docker_command_utils import execute_command_in_shell, fix_ownership_using_docker +from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH +from airflow_breeze.utils.run_utils import run_command @click.group(cls=BreezeGroup, name="registry", help="Tools for the Airflow Provider Registry") @@ -96,9 +100,192 @@ def extract_data(python: str, provider: str | None): help="Path to providers.json. Auto-detected if not provided.", ) def publish_versions(s3_bucket: str, providers_json: str | None): - from pathlib import Path - from airflow_breeze.utils.publish_registry_versions import publish_versions as _publish_versions providers_path = Path(providers_json) if providers_json else None _publish_versions(s3_bucket, providers_json_path=providers_path) + + +PROVIDERS_DIR = AIRFLOW_ROOT_PATH / "providers" +DEV_REGISTRY_DIR = AIRFLOW_ROOT_PATH / "dev" / "registry" + +PROVIDERS_JSON_PATH = DEV_REGISTRY_DIR / "providers.json" + +EXTRACT_SCRIPTS = [ + DEV_REGISTRY_DIR / "extract_parameters.py", + DEV_REGISTRY_DIR / "extract_connections.py", +] + + +def _find_provider_yaml(provider_id: str) -> Path: + """Find provider.yaml for a given provider ID (e.g. 'amazon', 'apache-beam', 'microsoft-azure').""" + # Provider ID uses hyphens; directory structure uses slashes (e.g. microsoft-azure -> microsoft/azure) + parts = provider_id.split("-") + # Try nested first (e.g. 'microsoft/azure'), then single directory (e.g. 'amazon') + candidates = [PROVIDERS_DIR / provider_id / "provider.yaml"] + if len(parts) >= 2: + candidates.insert(0, PROVIDERS_DIR / "/".join(parts) / "provider.yaml") + for candidate in candidates: + if candidate.exists(): + return candidate + raise click.ClickException( + f"provider.yaml not found for '{provider_id}'. Tried: {', '.join(str(c) for c in candidates)}" + ) + + +def _read_provider_yaml_info(provider_id: str) -> tuple[str, list[str]]: + """Read package name from provider.yaml and extras from pyproject.toml.""" + try: + import tomllib + except ImportError: + import tomli as tomllib + + import yaml + + provider_yaml_path = _find_provider_yaml(provider_id) + with open(provider_yaml_path) as f: + data = yaml.safe_load(f) + package_name = data["package-name"] + + pyproject = provider_yaml_path.parent / "pyproject.toml" + extras: list[str] = [] + if pyproject.exists(): + with open(pyproject, "rb") as f: + toml_data = tomllib.load(f) + optional_deps = toml_data.get("project", {}).get("optional-dependencies", {}) + extras = sorted(optional_deps.keys()) + + return package_name, extras + + +def _build_pip_spec(package_name: str, extras: list[str], version: str) -> str: + """Build pip install spec, e.g. 'apache-airflow-providers-amazon[pandas,s3fs]==9.21.0'.""" + if extras: + extras_str = ",".join(extras) + return f"{package_name}[{extras_str}]=={version}" + return f"{package_name}=={version}" + + +def _ensure_providers_json(provider_id: str, package_name: str) -> Path: + """Ensure dev/registry/providers.json exists with the target provider. + + The extraction scripts read this to determine which version to tag output with. + If it exists (from a previous extract-data or S3 download), use it. + If the provider is missing from an existing file, append it rather than replacing. + + NOTE: Does NOT touch registry/src/_data/providers.json, which is used by + the Eleventy build and must contain all providers. + """ + PROVIDERS_JSON_PATH.parent.mkdir(parents=True, exist_ok=True) + + if PROVIDERS_JSON_PATH.exists(): + with open(PROVIDERS_JSON_PATH) as f: + data = json.load(f) + if any(p["id"] == provider_id for p in data.get("providers", [])): + return PROVIDERS_JSON_PATH + # Provider not in file — append it rather than replacing + data["providers"].append({"id": provider_id, "package_name": package_name, "version": "0.0.0"}) + click.echo(f"Added {provider_id} to existing {PROVIDERS_JSON_PATH}") + else: + data = {"providers": [{"id": provider_id, "package_name": package_name, "version": "0.0.0"}]} + click.echo(f"Created minimal {PROVIDERS_JSON_PATH}") + + with open(PROVIDERS_JSON_PATH, "w") as f: + json.dump(data, f, indent=2) + return PROVIDERS_JSON_PATH + + +def _patch_providers_json(providers_json_path: Path, provider_id: str, version: str) -> str: + """Patch providers.json to set the target version. Returns the original version.""" + with open(providers_json_path) as f: + data = json.load(f) + for p in data["providers"]: + if p["id"] == provider_id: + original_version = p["version"] + p["version"] = version + with open(providers_json_path, "w") as f: + json.dump(data, f, indent=2) + return original_version + raise click.ClickException(f"Provider '{provider_id}' not found in {providers_json_path}") + + +# TODO: The backfill command processes versions sequentially because extract_parameters.py +# and extract_connections.py write to shared files (modules.json, providers.json). +# To parallelize, each provider would need its own isolated output directory so that +# concurrent runs don't clobber each other. See also the registry-backfill.yml workflow +# which uses a GitHub Actions matrix to run providers in parallel CI jobs. + + +@registry_group.command( + name="backfill", + help="Extract runtime parameters and connections for older provider versions. " + "Uses 'uv run --with' to install the specific version in a temporary environment " + "and runs extract_parameters.py + extract_connections.py. No Docker needed.", +) +@click.option( + "--provider", + required=True, + help="Provider ID (e.g. 'amazon', 'google', 'microsoft-azure').", +) +@click.option( + "--version", + "versions", + required=True, + multiple=True, + help="Version(s) to extract. Can be specified multiple times: --version 9.21.0 --version 9.20.0", +) +@option_verbose +@option_dry_run +def backfill(provider: str, versions: tuple[str, ...]): + package_name, extras = _read_provider_yaml_info(provider) + providers_json_path = _ensure_providers_json(provider, package_name) + + click.echo(f"Provider: {provider} ({package_name})") + click.echo(f"Versions: {', '.join(versions)}") + if extras: + click.echo(f"Extras: {', '.join(extras)}") + click.echo() + + failed: list[str] = [] + + for version in versions: + click.echo(f"{'=' * 60}") + click.echo(f"Extracting {provider} {version}") + click.echo(f"{'=' * 60}") + + original_version = _patch_providers_json(providers_json_path, provider, version) + + try: + pip_spec = _build_pip_spec(package_name, extras, version) + base_spec = f"{package_name}=={version}" + for script in EXTRACT_SCRIPTS: + click.echo(f"\nRunning {script.name} with {pip_spec}...") + result = run_command( + ["uv", "run", "--with", pip_spec, "python", str(script)], + check=False, + cwd=str(AIRFLOW_ROOT_PATH), + ) + if result.returncode != 0 and pip_spec != base_spec: + click.echo(f"Retrying {script.name} without extras...") + result = run_command( + ["uv", "run", "--with", base_spec, "python", str(script)], + check=False, + cwd=str(AIRFLOW_ROOT_PATH), + ) + if result.returncode != 0: + click.echo(f"WARNING: {script.name} failed for {version} (exit {result.returncode})") + failed.append(f"{version}/{script.name}") + finally: + _patch_providers_json(providers_json_path, provider, original_version) + + click.echo(f"\n{'=' * 60}") + if failed: + click.echo(f"Completed with failures: {', '.join(failed)}") + sys.exit(1) + else: + click.echo(f"Successfully extracted {len(versions)} version(s) for {provider}") + click.echo( + f"\nOutput written to:\n" + f" registry/src/_data/versions/{provider}//parameters.json\n" + f" registry/src/_data/versions/{provider}//connections.json" + ) diff --git a/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py b/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py index 2e3f579e50b6f..fdd156d45a34b 100644 --- a/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py +++ b/dev/breeze/src/airflow_breeze/commands/registry_commands_config.py @@ -20,6 +20,7 @@ "name": "Registry commands", "commands": [ "extract-data", + "backfill", "publish-versions", ], } @@ -34,6 +35,15 @@ ], }, ], + "breeze registry backfill": [ + { + "name": "Backfill flags", + "options": [ + "--provider", + "--version", + ], + }, + ], "breeze registry publish-versions": [ { "name": "Publish versions flags", diff --git a/dev/breeze/tests/test_registry_backfill.py b/dev/breeze/tests/test_registry_backfill.py new file mode 100644 index 0000000000000..2eb4b732eb550 --- /dev/null +++ b/dev/breeze/tests/test_registry_backfill.py @@ -0,0 +1,189 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Unit tests for the registry backfill command helpers.""" + +from __future__ import annotations + +import json +from unittest.mock import patch + +import pytest + +from airflow_breeze.commands.registry_commands import ( + _build_pip_spec, + _ensure_providers_json, + _find_provider_yaml, + _patch_providers_json, + _read_provider_yaml_info, +) + + +# --------------------------------------------------------------------------- +# _find_provider_yaml +# --------------------------------------------------------------------------- +class TestFindProviderYaml: + def test_simple_provider(self): + path = _find_provider_yaml("amazon") + assert path.name == "provider.yaml" + assert "providers/amazon" in str(path) + + def test_hyphenated_provider(self): + path = _find_provider_yaml("microsoft-azure") + assert path.name == "provider.yaml" + assert "providers/microsoft/azure" in str(path) + + def test_triple_hyphenated_provider(self): + path = _find_provider_yaml("apache-beam") + assert path.name == "provider.yaml" + assert "providers/apache/beam" in str(path) or "providers/apache-beam" in str(path) + + def test_unknown_provider_raises(self): + with pytest.raises(Exception, match="provider.yaml not found"): + _find_provider_yaml("nonexistent-provider-xyz") + + +# --------------------------------------------------------------------------- +# _read_provider_yaml_info +# --------------------------------------------------------------------------- +class TestReadProviderYamlInfo: + def test_reads_package_name_and_extras(self, tmp_path): + provider_dir = tmp_path / "providers" / "amazon" + provider_dir.mkdir(parents=True) + (provider_dir / "provider.yaml").write_text("package-name: apache-airflow-providers-amazon\n") + (provider_dir / "pyproject.toml").write_text( + '[project.optional-dependencies]\npandas = ["pandas>=2.1.2"]\ns3fs = ["s3fs>=2024.6.1"]\n' + ) + with patch("airflow_breeze.commands.registry_commands.PROVIDERS_DIR", tmp_path / "providers"): + package_name, extras = _read_provider_yaml_info("amazon") + assert package_name == "apache-airflow-providers-amazon" + assert extras == ["pandas", "s3fs"] + + def test_no_pyproject_returns_empty_extras(self, tmp_path): + provider_dir = tmp_path / "providers" / "ftp" + provider_dir.mkdir(parents=True) + (provider_dir / "provider.yaml").write_text("package-name: apache-airflow-providers-ftp\n") + with patch("airflow_breeze.commands.registry_commands.PROVIDERS_DIR", tmp_path / "providers"): + package_name, extras = _read_provider_yaml_info("ftp") + assert package_name == "apache-airflow-providers-ftp" + assert extras == [] + + def test_pyproject_without_optional_deps(self, tmp_path): + provider_dir = tmp_path / "providers" / "sqlite" + provider_dir.mkdir(parents=True) + (provider_dir / "provider.yaml").write_text("package-name: apache-airflow-providers-sqlite\n") + (provider_dir / "pyproject.toml").write_text("[project]\nname = 'test'\n") + with patch("airflow_breeze.commands.registry_commands.PROVIDERS_DIR", tmp_path / "providers"): + _, extras = _read_provider_yaml_info("sqlite") + assert extras == [] + + +# --------------------------------------------------------------------------- +# _build_pip_spec +# --------------------------------------------------------------------------- +class TestBuildPipSpec: + def test_with_extras(self): + result = _build_pip_spec("apache-airflow-providers-amazon", ["pandas", "s3fs"], "9.21.0") + assert result == "apache-airflow-providers-amazon[pandas,s3fs]==9.21.0" + + def test_without_extras(self): + result = _build_pip_spec("apache-airflow-providers-ftp", [], "1.0.0") + assert result == "apache-airflow-providers-ftp==1.0.0" + + def test_single_extra(self): + result = _build_pip_spec("apache-airflow-providers-google", ["leveldb"], "10.0.0") + assert result == "apache-airflow-providers-google[leveldb]==10.0.0" + + +# --------------------------------------------------------------------------- +# _ensure_providers_json +# --------------------------------------------------------------------------- +class TestEnsureProvidersJson: + def test_creates_new_file(self, tmp_path): + providers_json = tmp_path / "dev" / "registry" / "providers.json" + with patch( + "airflow_breeze.commands.registry_commands.PROVIDERS_JSON_PATH", + providers_json, + ): + result = _ensure_providers_json("amazon", "apache-airflow-providers-amazon") + + assert result == providers_json + data = json.loads(providers_json.read_text()) + assert len(data["providers"]) == 1 + assert data["providers"][0]["id"] == "amazon" + assert data["providers"][0]["package_name"] == "apache-airflow-providers-amazon" + + def test_appends_to_existing_file(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text( + json.dumps({"providers": [{"id": "google", "package_name": "pkg-google", "version": "1.0.0"}]}) + ) + with patch( + "airflow_breeze.commands.registry_commands.PROVIDERS_JSON_PATH", + providers_json, + ): + _ensure_providers_json("amazon", "apache-airflow-providers-amazon") + + data = json.loads(providers_json.read_text()) + assert len(data["providers"]) == 2 + ids = [p["id"] for p in data["providers"]] + assert "google" in ids + assert "amazon" in ids + + def test_skips_if_provider_already_present(self, tmp_path): + providers_json = tmp_path / "providers.json" + original = {"providers": [{"id": "amazon", "package_name": "pkg", "version": "1.0.0"}]} + providers_json.write_text(json.dumps(original)) + with patch( + "airflow_breeze.commands.registry_commands.PROVIDERS_JSON_PATH", + providers_json, + ): + _ensure_providers_json("amazon", "pkg") + + # File should be unchanged + data = json.loads(providers_json.read_text()) + assert len(data["providers"]) == 1 + + +# --------------------------------------------------------------------------- +# _patch_providers_json +# --------------------------------------------------------------------------- +class TestPatchProvidersJson: + def test_patches_version(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text(json.dumps({"providers": [{"id": "amazon", "version": "9.22.0"}]})) + original = _patch_providers_json(providers_json, "amazon", "9.15.0") + assert original == "9.22.0" + + data = json.loads(providers_json.read_text()) + assert data["providers"][0]["version"] == "9.15.0" + + def test_raises_for_missing_provider(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text(json.dumps({"providers": [{"id": "google", "version": "1.0.0"}]})) + with pytest.raises(Exception, match="not found"): + _patch_providers_json(providers_json, "amazon", "9.15.0") + + def test_restores_original_version(self, tmp_path): + providers_json = tmp_path / "providers.json" + providers_json.write_text(json.dumps({"providers": [{"id": "amazon", "version": "9.22.0"}]})) + # Patch to target version + _patch_providers_json(providers_json, "amazon", "9.15.0") + # Restore + _patch_providers_json(providers_json, "amazon", "9.22.0") + + data = json.loads(providers_json.read_text()) + assert data["providers"][0]["version"] == "9.22.0" diff --git a/registry/src/_data/providerVersions.js b/registry/src/_data/providerVersions.js index 52bad7160d60c..774a0342e5ffd 100644 --- a/registry/src/_data/providerVersions.js +++ b/registry/src/_data/providerVersions.js @@ -72,6 +72,17 @@ module.exports = function () { const latestAirflow = provider.airflow_versions && provider.airflow_versions.length > 0 ? provider.airflow_versions[provider.airflow_versions.length - 1] : null; + + // Compute module_counts from modules.json (runtime discovery) when available, + // since providers.json may only have AST-based counts which undercount. + if (latestModules.length > 0) { + const counts = {}; + for (const m of latestModules) { + counts[m.type] = (counts[m.type] || 0) + 1; + } + provider.module_counts = counts; + } + result.push({ provider, version: provider.version, From 3a40a9420fe215e8d86e1f4291f199b6f2666092 Mon Sep 17 00:00:00 2001 From: GPK Date: Tue, 10 Mar 2026 15:59:49 +0000 Subject: [PATCH 045/595] Fix JWTBearerTIPathDep import errors in HITL routes (#63277) --- .../api_fastapi/execution_api/routes/hitl.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py index 92e973a3d0015..802b09502e09b 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py @@ -19,7 +19,8 @@ from uuid import UUID import structlog -from fastapi import APIRouter, HTTPException, status +from cadwyn import VersionedAPIRouter +from fastapi import HTTPException, Security, status from sqlalchemy import select from airflow._shared.timezones import timezone @@ -29,14 +30,15 @@ HITLDetailResponse, UpdateHITLDetailPayload, ) -from airflow.api_fastapi.execution_api.deps import JWTBearerTIPathDep +from airflow.api_fastapi.execution_api.security import ExecutionAPIRoute, require_auth from airflow.models.hitl import HITLDetail -router = APIRouter( +router = VersionedAPIRouter( + route_class=ExecutionAPIRoute, dependencies=[ - # This checks that the UUID in the url matches the one in the token for us. - JWTBearerTIPathDep - ] + # Validates that the JWT sub matches the task_instance_id path parameter. + Security(require_auth, scopes=["ti:self"]), + ], ) log = structlog.get_logger(__name__) From 44900b84a9c9fcdbb979e91543a51d777f15e34a Mon Sep 17 00:00:00 2001 From: Shivam Rastogi <6463385+shivaam@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:15:09 -0700 Subject: [PATCH 046/595] fix: Replace expunge_all with expunge in MetastoreBackend (#63080) MetastoreBackend.get_connection() and get_variable() called session.expunge_all() to detach the returned object from the session. This removed all objects from the shared scoped session, including unrelated pending objects added by other code sharing the same thread-local session. This caused team-scoped DAG bundles to silently fail to persist when sync_bundles_to_db triggered a connection lookup through S3DagBundle's view_url_template, which initializes an S3Hook and calls get_connection. Replace expunge_all() with expunge(obj) to only detach the specific queried Connection or Variable, leaving all other session state intact. closes: #62244 --- airflow-core/src/airflow/secrets/metastore.py | 5 +- .../unit/always/test_secrets_metastore.py | 110 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 airflow-core/tests/unit/always/test_secrets_metastore.py diff --git a/airflow-core/src/airflow/secrets/metastore.py b/airflow-core/src/airflow/secrets/metastore.py index bde8423158d3d..e7f25a9f9179f 100644 --- a/airflow-core/src/airflow/secrets/metastore.py +++ b/airflow-core/src/airflow/secrets/metastore.py @@ -57,7 +57,8 @@ def get_connection( ) .limit(1) ) - session.expunge_all() + if conn: + session.expunge(conn) return conn @provide_session @@ -79,7 +80,7 @@ def get_variable( .where(Variable.key == key, or_(Variable.team_name == team_name, Variable.team_name.is_(None))) .limit(1) ) - session.expunge_all() if var_value: + session.expunge(var_value) return var_value.val return None diff --git a/airflow-core/tests/unit/always/test_secrets_metastore.py b/airflow-core/tests/unit/always/test_secrets_metastore.py new file mode 100644 index 0000000000000..d13419563f701 --- /dev/null +++ b/airflow-core/tests/unit/always/test_secrets_metastore.py @@ -0,0 +1,110 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import pytest + +from airflow.models.connection import Connection +from airflow.models.variable import Variable +from airflow.secrets.metastore import MetastoreBackend +from airflow.utils.session import create_session + +from tests_common.test_utils.db import clear_db_connections, clear_db_variables + +pytestmark = pytest.mark.db_test + + +class TestMetastoreBackendSessionSafety: + """MetastoreBackend must not corrupt the shared scoped session. + + Regression tests for https://github.com/apache/airflow/issues/62244. + """ + + def setup_method(self) -> None: + clear_db_connections() + clear_db_variables() + + def teardown_method(self) -> None: + clear_db_connections() + clear_db_variables() + + @pytest.mark.parametrize("conn_exists", [True, False], ids=["found", "not_found"]) + def test_get_connection_preserves_pending_session_objects(self, conn_exists): + """get_connection must not remove unrelated pending objects from session.new.""" + if conn_exists: + with create_session() as session: + session.add(Connection(conn_id="target_conn", conn_type="mysql")) + session.commit() + + with create_session() as session: + # Simulate pending work from another function sharing the session + pending = Connection(conn_id="pending_conn", conn_type="http") + session.add(pending) + + # Same session passed to simulate shared scoped session behavior + backend = MetastoreBackend() + result = backend.get_connection("target_conn", session=session) + + if conn_exists: + assert result is not None + assert result.conn_id == "target_conn" + else: + assert result is None + # The pending object must still be in session.new — expunge(conn) should only + # detach the queried Connection, not wipe unrelated pending objects. + assert pending in session.new + + @pytest.mark.parametrize("var_exists", [True, False], ids=["found", "not_found"]) + def test_get_variable_preserves_pending_session_objects(self, var_exists): + """get_variable must not remove unrelated pending objects from session.new.""" + if var_exists: + Variable.set(key="test_key", value="test_value") + + with create_session() as session: + # Use any ORM model as the pending object to detect session corruption + pending = Connection(conn_id="pending_conn", conn_type="http") + session.add(pending) + + backend = MetastoreBackend() + result = backend.get_variable("test_key", session=session) + + if var_exists: + assert result == "test_value" + else: + assert result is None + # The pending object must still be in session.new — expunge(var_value) should only + # detach the queried Variable, not wipe unrelated pending objects. + assert pending in session.new + + def test_get_connection_returns_detached_object(self): + """Returned connection must be detached so callers can use it freely.""" + from sqlalchemy import inspect as sa_inspect + + with create_session() as session: + session.add(Connection(conn_id="test_conn", conn_type="mysql", host="localhost")) + session.commit() + + backend = MetastoreBackend() + conn = backend.get_connection("test_conn") + + assert conn is not None + # Object should be detached — not tracked by any session + assert sa_inspect(conn).detached + # Attributes should still be accessible + assert conn.conn_id == "test_conn" + assert conn.host == "localhost" From 6e6c50f24eb92a6acfec015ea12395e9d52d27a7 Mon Sep 17 00:00:00 2001 From: Saksham Singhal Date: Tue, 10 Mar 2026 21:45:48 +0530 Subject: [PATCH 047/595] fix: add null checks for dag_version access in scheduler (#62225) * fix: add null checks for dag_version access in scheduler * Fix nullable dag_version for scheduler callbacks (AIP-66) Replace silent skip-on-None guards with a proper fallback strategy for all four TaskCallbackRequest / EmailRequest creation sites in scheduler_job_runner.py. Problem: When ti.dag_version is None (tasks migrated from Airflow 2 to 3), the scheduler crashed with AttributeError when accessing ti.dag_version.bundle_name / bundle_version, and the previous fix silently suppressed callbacks entirely for legacy tasks. Fix: Use an inline ternary fallback, mirroring the existing pattern in dagrun.py (DagCallbackRequest): bundle_name <- dag_version.bundle_name OR dag_model.bundle_name bundle_version <- dag_version.bundle_version OR dag_run.bundle_version DagModel.bundle_name is NOT NULL so the fallback is always safe. DagRun.bundle_version is nullable (str | None), matching the type expected by BaseCallbackRequest. Affected sites: 1. process_executor_events - TaskCallbackRequest (externally killed) 2. process_executor_events - EmailRequest (email on failure/retry) 3. _maybe_requeue_stuck_ti - TaskCallbackRequest (stuck-in-queued) 4. _purge_task_instances_without_heartbeats - TaskCallbackRequest Tests: Added TestSchedulerCallbackBundleInfoDagVersionNullable to test_scheduler_job.py with parameterized cases covering: - dag_version present -> bundle info from dag_version - dag_version None -> bundle info from dag_model / dag_run - no AttributeError crash in either case - correct precedence of dag_version over fallback values * fix: backfill dag_version_id for legacy tasks to avoid Pydantic ValidationError The Pydantic TaskInstance datamodel requires dag_version_id to be a strict uuid.UUID (not None). After removing the old 'skip when dag_version is None' guard, legacy tasks migrated from Airflow 2 triggered a ValidationError when constructing TaskCallbackRequest. Fix: Add _ensure_ti_has_dag_version_id() helper that: 1. Returns True immediately if dag_version_id is already set 2. Backfills from DagVersion.get_latest_version() when missing 3. Returns False (skip with warning) only when no DagVersion exists at all for the dag_id This helper is called at all 4 callback creation sites, right before constructing TaskCallbackRequest / EmailRequest. It ensures: - Callbacks are sent whenever possible (backfill succeeds) - Clean skip with warning only in edge case (no DagVersion at all) - Pydantic validation never sees None for dag_version_id Updated test_purge_without_heartbeat_skips_when_missing_dag_version to verify the new backfill behavior: since dag_maker creates a DagVersion, the backfill succeeds and the callback IS sent. * fix: replace invalid 'continue' with 'if' guard in _maybe_requeue_stuck_ti The 'continue' statement was inside _maybe_requeue_stuck_ti() method, which is not a loop - causing a SyntaxError at import time. This broke all CI jobs that import scheduler_job_runner. Restructured the logic to use an if-guard: when dag_version_id cannot be backfilled, we skip the callback but still let the finally block mark the task as FAILED. * fixed static check --- .../src/airflow/jobs/scheduler_job_runner.py | 122 +++++++++++--- .../tests/unit/jobs/test_scheduler_job.py | 155 +++++++++++++++++- 2 files changed, 243 insertions(+), 34 deletions(-) diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 24dbf8d1e98c0..2d58f295c6ecc 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -167,6 +167,41 @@ def _eager_load_dag_run_for_validation() -> tuple[LoaderOption, LoaderOption]: ) +def _ensure_ti_has_dag_version_id(ti: TaskInstance, session: Session, log: Logger) -> bool: + """ + Ensure a TaskInstance has a valid dag_version_id for Pydantic serialisation. + + Legacy tasks migrated from Airflow 2 may have dag_version_id = None. + The Pydantic TaskInstance datamodel requires dag_version_id to be a strict + uuid.UUID, so we must backfill it before constructing TaskCallbackRequest + or EmailRequest. + + Returns True if dag_version_id is present (or was successfully backfilled), + False if it could not be resolved (caller should skip the callback). + """ + if ti.dag_version_id is not None: + return True + + latest_version = DagVersion.get_latest_version(ti.dag_id, session=session) + if latest_version is None: + log.warning( + "TaskInstance %s has no dag_version_id and no DagVersion could be found " + "for dag_id=%s. Skipping callback. " + "This can happen for tasks migrated from Airflow 2 with no subsequent DAG parse.", + ti, + ti.dag_id, + ) + return False + + ti.dag_version_id = latest_version.id + log.info( + "Backfilled dag_version_id for legacy TaskInstance %s from latest DagVersion %s.", + ti, + latest_version.id, + ) + return True + + class ConcurrencyMap: """ Dataclass to represent concurrency maps. @@ -1344,10 +1379,20 @@ def process_executor_events( # Only log the error/extra info here, since the `ti.handle_failure()` path will log it # too, which would lead to double logging cls.logger().error(msg) + # Safely extract bundle info: prefer dag_version when available, + # fall back to dag_model/dag_run for legacy tasks migrated from + # Airflow 2 where dag_version may be None (AIP-66). + _bundle_name = ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + _bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + if not _ensure_ti_has_dag_version_id(ti, session, cls.logger()): + continue request = TaskCallbackRequest( filepath=ti.dag_model.relative_fileloc or "", - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_version.bundle_version, + bundle_name=_bundle_name, + bundle_version=_bundle_version, ti=ti, msg=msg, task_callback_type=( @@ -1381,10 +1426,21 @@ def process_executor_events( "Sending email request for task %s to DAG Processor", ti, ) + # Safely extract bundle info with fallback for legacy tasks + # (dag_version may be None after Airflow 2 → 3 migration). + _email_bundle_name = ( + ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + ) + _email_bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + if not _ensure_ti_has_dag_version_id(ti, session, cls.logger()): + continue email_request = EmailRequest( filepath=ti.dag_model.relative_fileloc or "", - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_version.bundle_version, + bundle_name=_email_bundle_name, + bundle_version=_email_bundle_version, ti=ti, msg=msg, email_type="retry" if ti.is_eligible_to_retry() else "failure", @@ -2670,21 +2726,33 @@ def _maybe_requeue_stuck_ti(self, *, ti, session, executor): if task.has_on_failure_callback: if inspect(ti).detached: ti = session.merge(ti) - request = TaskCallbackRequest( - filepath=ti.dag_model.relative_fileloc, - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_version.bundle_version, - ti=ti, - msg=msg, - context_from_server=TIRunContext( - dag_run=ti.dag_run, - max_tries=ti.max_tries, - variables=[], - connections=[], - xcom_keys_to_clear=[], - ), + # Safely extract bundle info with fallback for legacy tasks + # (dag_version may be None after Airflow 2 → 3 migration). + _stuck_bundle_name = ( + ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name ) - executor.send_callback(request) + _stuck_bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + # Note: we cannot use `continue` here because this method is not + # inside a loop. If backfilling fails we simply skip the callback. + if _ensure_ti_has_dag_version_id(ti, session, self.log): + request = TaskCallbackRequest( + filepath=ti.dag_model.relative_fileloc or "", + bundle_name=_stuck_bundle_name, + bundle_version=_stuck_bundle_version, + ti=ti, + msg=msg, + context_from_server=TIRunContext( + dag_run=ti.dag_run, + max_tries=ti.max_tries, + variables=[], + connections=[], + xcom_keys_to_clear=[], + ), + ) + executor.send_callback(request) finally: ti.set_state(TaskInstanceState.FAILED, session=session) executor.fail(ti.key) @@ -3025,17 +3093,19 @@ def _purge_task_instances_without_heartbeats( task_instance_heartbeat_timeout_message_details = ( self._generate_task_instance_heartbeat_timeout_message_details(ti) ) - if not ti.dag_version: - # If old ti from Airflow 2 and dag_version is None, skip heartbeat timeout handling. - self.log.warning( - "DAG Version not found for TaskInstance %s. Skipping heartbeat timeout handling.", - ti, - ) + # Safely extract bundle info with fallback for legacy tasks + # (dag_version may be None after Airflow 2 → 3 migration). + _hb_bundle_name = ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + _hb_bundle_version = ( + ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + ) + # Backfill dag_version_id for legacy tasks (Pydantic requires uuid.UUID). + if not _ensure_ti_has_dag_version_id(ti, session, self.log): continue request = TaskCallbackRequest( filepath=ti.dag_model.relative_fileloc or "", - bundle_name=ti.dag_version.bundle_name, - bundle_version=ti.dag_run.bundle_version, + bundle_name=_hb_bundle_name, + bundle_version=_hb_bundle_version, ti=ti, msg=str(task_instance_heartbeat_timeout_message_details), context_from_server=TIRunContext( diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index 57536416caba6..3aeebcedbdc2c 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -2956,20 +2956,18 @@ def test_purge_without_heartbeat_skips_when_missing_dag_version(self, dag_maker, ti.state = TaskInstanceState.RUNNING ti.queued_by_job_id = scheduler_job.id ti.last_heartbeat_at = timezone.utcnow() - timedelta(hours=1) - # Simulate missing dag_version + # Simulate missing dag_version (legacy Airflow 2 task) ti.dag_version_id = None session.merge(ti) session.commit() - with caplog.at_level("WARNING", logger="airflow.jobs.scheduler_job_runner"): + with caplog.at_level("INFO", logger="airflow.jobs.scheduler_job_runner"): self.job_runner._purge_task_instances_without_heartbeats([ti], session=session) - # Should log a warning and skip processing - assert any("DAG Version not found for TaskInstance" in rec.message for rec in caplog.records) - mock_executor.send_callback.assert_not_called() - # State should be unchanged (not failed) - ti.refresh_from_db(session=session) - assert ti.state == TaskInstanceState.RUNNING + # dag_version_id should be backfilled from the latest DagVersion in the DB + # (dag_maker creates one) and the callback should be sent + assert any("Backfilled dag_version_id" in rec.message for rec in caplog.records) + mock_executor.send_callback.assert_called_once() @staticmethod def mock_failure_callback(context): @@ -9157,3 +9155,144 @@ def test_consumer_dag_listen_to_two_partitioned_asset_with_key_1_mapper( assert asset_event.source_task_id == "hi" assert "asset-event-producer-" in asset_event.source_dag_id assert asset_event.source_run_id == "test" + + +# --------------------------------------------------------------------------- +# Tests for nullable dag_version in scheduler callbacks (AIP-66) +# +# Verifies that TaskCallbackRequest and EmailRequest are always created +# with correct bundle_name/bundle_version whether ti.dag_version is a real +# DagVersion object (normal) or None (legacy tasks migrated from Airflow 2). +# +# Fallback mirrors DagCallbackRequest in dagrun.py: +# bundle_name <- ti.dag_version.bundle_name OR ti.dag_model.bundle_name +# bundle_version <- ti.dag_version.bundle_version OR ti.dag_run.bundle_version +# --------------------------------------------------------------------------- + + +def _make_ti_with_dag_version( + dag_version, dag_model_bundle_name="fallback-bundle", dag_run_bundle_version="v1.0-fallback" +): + """Build a minimal mock TaskInstance for dag_version nullable tests.""" + ti = mock.MagicMock() + ti.dag_version = dag_version + ti.dag_model = mock.MagicMock() + ti.dag_model.bundle_name = dag_model_bundle_name + ti.dag_model.relative_fileloc = "/dags/test_dag.py" + ti.dag_run = mock.MagicMock() + ti.dag_run.bundle_version = dag_run_bundle_version + return ti + + +def _make_dag_version(bundle_name="my-bundle", bundle_version="v2.0"): + """Create a simple mock DagVersion.""" + dv = mock.MagicMock() + dv.bundle_name = bundle_name + dv.bundle_version = bundle_version + return dv + + +def _extract_bundle_name(ti): + """Mirror the inline fallback logic from scheduler_job_runner.py.""" + return ti.dag_version.bundle_name if ti.dag_version else ti.dag_model.bundle_name + + +def _extract_bundle_version(ti): + """Mirror the inline fallback logic from scheduler_job_runner.py.""" + return ti.dag_version.bundle_version if ti.dag_version else ti.dag_run.bundle_version + + +class TestSchedulerCallbackBundleInfoDagVersionNullable: + """ + Verify the bundle_name / bundle_version extraction logic used at all four + TaskCallbackRequest / EmailRequest creation sites in scheduler_job_runner.py. + + When dag_version is present -> use dag_version.bundle_name / bundle_version. + When dag_version is None -> fall back to dag_model.bundle_name / dag_run.bundle_version. + """ + + # ── With dag_version present ────────────────────────────────────────── + + @pytest.mark.parametrize( + ("dv_bundle_name", "dv_bundle_version"), + [ + pytest.param("my-bundle", "v2.0", id="normal"), + pytest.param("dags-folder", None, id="version_none"), + pytest.param("custom-bundle", "v99.0", id="custom"), + ], + ) + def test_bundle_info_from_dag_version_when_present(self, dv_bundle_name, dv_bundle_version): + """When dag_version is set, bundle info must come from it.""" + dv = _make_dag_version(bundle_name=dv_bundle_name, bundle_version=dv_bundle_version) + ti = _make_ti_with_dag_version(dag_version=dv, dag_model_bundle_name="SHOULD-NOT-USE") + + assert _extract_bundle_name(ti) == dv_bundle_name + assert _extract_bundle_version(ti) == dv_bundle_version + + # ── With dag_version None (legacy Airflow 2 task) ───────────────────── + + @pytest.mark.parametrize( + ("model_bundle_name", "run_bundle_version"), + [ + pytest.param("fallback-bundle", "v1.0-fallback", id="normal_fallback"), + pytest.param("dags-folder", None, id="version_none_fallback"), + pytest.param("another-bundle", "v3.5", id="custom_fallback"), + ], + ) + def test_bundle_info_falls_back_when_dag_version_none(self, model_bundle_name, run_bundle_version): + """When dag_version is None, bundle info must fall back to dag_model / dag_run.""" + ti = _make_ti_with_dag_version( + dag_version=None, + dag_model_bundle_name=model_bundle_name, + dag_run_bundle_version=run_bundle_version, + ) + + assert _extract_bundle_name(ti) == model_bundle_name + assert _extract_bundle_version(ti) == run_bundle_version + + # ── No AttributeError crash ──────────────────────────────────────────── + + @pytest.mark.parametrize( + "dag_version_present", + [ + pytest.param(True, id="dag_version_present"), + pytest.param(False, id="dag_version_none"), + ], + ) + def test_no_attribute_error_regardless_of_dag_version(self, dag_version_present): + """ + The old code crashed with AttributeError when ti.dag_version was None. + The new fallback must never raise regardless of dag_version state. + """ + ti = _make_ti_with_dag_version(dag_version=_make_dag_version() if dag_version_present else None) + + name = _extract_bundle_name(ti) + version = _extract_bundle_version(ti) + + assert isinstance(name, str) + assert version is None or isinstance(version, str) + + # ── Precedence: dag_version wins over fallback ───────────────────────── + + def test_dag_version_takes_precedence_over_fallback_values(self): + """When dag_version is set, dag_model/dag_run fallbacks must NOT be used.""" + dv = _make_dag_version(bundle_name="preferred-bundle", bundle_version="preferred-v1") + ti = _make_ti_with_dag_version( + dag_version=dv, + dag_model_bundle_name="fallback-bundle", + dag_run_bundle_version="fallback-v1", + ) + + assert _extract_bundle_name(ti) == "preferred-bundle" + assert _extract_bundle_version(ti) == "preferred-v1" + + def test_fallback_values_used_only_when_dag_version_is_none(self): + """When dag_version is None, fallback values must be used.""" + ti = _make_ti_with_dag_version( + dag_version=None, + dag_model_bundle_name="fallback-bundle", + dag_run_bundle_version="fallback-v1", + ) + + assert _extract_bundle_name(ti) == "fallback-bundle" + assert _extract_bundle_version(ti) == "fallback-v1" From 191eca02c136d0b1330ff2cf60789b0beb47dbfc Mon Sep 17 00:00:00 2001 From: Ash Berlin-Taylor Date: Tue, 10 Mar 2026 16:30:34 +0000 Subject: [PATCH 048/595] Make start_date optional for @continuous schedule (#61405) When start_date is not specified, continuous DAGs will begin running immediately when unpaused, using the current time as the starting point. Previously if you didn't have a start date (the default in 3.0) it would do nothing when it was unpaused. This aligns with Airflow 3.0's philosophy of making start_date optional for schedules that don't require historical backfilling. --- ...inuous-optional-start-date.improvement.rst | 1 + airflow-core/src/airflow/timetables/simple.py | 10 ++++------ .../timetables/test_continuous_timetable.py | 20 +++++++++++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 airflow-core/newsfragments/continuous-optional-start-date.improvement.rst diff --git a/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst b/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst new file mode 100644 index 0000000000000..c001c62093345 --- /dev/null +++ b/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst @@ -0,0 +1 @@ +The ``schedule="@continuous"`` parameter now works without requiring a ``start_date``, and any DAGs with this schedule will begin running immediately when unpaused. diff --git a/airflow-core/src/airflow/timetables/simple.py b/airflow-core/src/airflow/timetables/simple.py index 082cf90e56dc1..01fb12f81dd0c 100644 --- a/airflow-core/src/airflow/timetables/simple.py +++ b/airflow-core/src/airflow/timetables/simple.py @@ -161,14 +161,12 @@ def next_dagrun_info( last_automated_data_interval: DataInterval | None, restriction: TimeRestriction, ) -> DagRunInfo | None: - if restriction.earliest is None: # No start date, won't run. - return None - current_time = timezone.coerce_datetime(timezone.utcnow()) + start_date = restriction.earliest or current_time if last_automated_data_interval is not None: # has already run once if last_automated_data_interval.end > current_time: # start date is future - start = restriction.earliest + start = start_date elapsed = last_automated_data_interval.end - last_automated_data_interval.start end = start + elapsed.as_timedelta() @@ -176,8 +174,8 @@ def next_dagrun_info( start = last_automated_data_interval.end end = current_time else: # first run - start = restriction.earliest - end = max(restriction.earliest, current_time) + start = start_date + end = max(start_date, current_time) if restriction.latest is not None and end > restriction.latest: return None diff --git a/airflow-core/tests/unit/timetables/test_continuous_timetable.py b/airflow-core/tests/unit/timetables/test_continuous_timetable.py index 73a4be5ea48a2..03babcea63839 100644 --- a/airflow-core/tests/unit/timetables/test_continuous_timetable.py +++ b/airflow-core/tests/unit/timetables/test_continuous_timetable.py @@ -41,12 +41,28 @@ def timetable(): return ContinuousTimetable() -def test_no_runs_without_start_date(timetable): +@time_machine.travel(DURING_DATE) +def test_runs_without_start_date(timetable): next_info = timetable.next_dagrun_info( last_automated_data_interval=None, restriction=TimeRestriction(earliest=None, latest=None, catchup=False), ) - assert next_info is None + assert next_info is not None + assert next_info.run_after == DURING_DATE + assert next_info.data_interval.start == DURING_DATE + assert next_info.data_interval.end == DURING_DATE + + +@time_machine.travel(AFTER_DATE) +def test_subsequent_runs_without_start_date(timetable): + next_info = timetable.next_dagrun_info( + last_automated_data_interval=DataInterval(DURING_DATE, DURING_DATE), + restriction=TimeRestriction(earliest=None, latest=None, catchup=False), + ) + assert next_info is not None + assert next_info.run_after == AFTER_DATE + assert next_info.data_interval.start == DURING_DATE + assert next_info.data_interval.end == AFTER_DATE @time_machine.travel(DURING_DATE) From c475a483f1f31c76d0ced648880317eddfc7e562 Mon Sep 17 00:00:00 2001 From: deepinsight coder <32898216+Vamsi-klu@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:45:58 -0700 Subject: [PATCH 049/595] Validate update_mask fields in PATCH endpoints against Pydantic models (#62657) * Validate update_mask fields in PATCH endpoints against partial Pydantic models When update_mask is provided in PATCH endpoints, validation was either skipped or run against full models with required fields, causing failures. Now uses partial Pydantic models (all fields Optional) for update_mask validation, while keeping full-model validation when no update_mask. - Add make_partial_model() helper that creates a subclass with all fields Optional while preserving validators, model_config, and field metadata - Create ConnectionBodyPartial, VariableBodyPartial, DAGPatchBodyPartial - Use partial models in connections, dags, and variables PATCH endpoints - Add tests for make_partial_model and endpoint-level partial validation Co-Authored-By: Claude Opus 4.6 * Fix mypy and ruff CI failures in make_partial_model - Replace `Optional[ann]` with `ann | None` to satisfy ruff UP045/UP007 - Add type: ignore for mypy since runtime type manipulation can't be statically analyzed - Remove unused `Optional` import Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../src/airflow/api_fastapi/core_api/base.py | 24 +++++- .../core_api/datamodels/connections.py | 5 +- .../api_fastapi/core_api/datamodels/dags.py | 5 +- .../core_api/datamodels/variables.py | 5 +- .../core_api/routes/public/connections.py | 16 +++- .../core_api/routes/public/dags.py | 5 ++ .../core_api/services/public/variables.py | 16 +++- .../routes/public/test_connections.py | 28 +++++++ .../core_api/routes/public/test_variables.py | 17 ++++ .../unit/api_fastapi/core_api/test_base.py | 78 +++++++++++++++++++ 10 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 airflow-core/tests/unit/api_fastapi/core_api/test_base.py diff --git a/airflow-core/src/airflow/api_fastapi/core_api/base.py b/airflow-core/src/airflow/api_fastapi/core_api/base.py index 887f528f197ef..600a6d896e2f2 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/base.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/base.py @@ -17,9 +17,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Generic, TypeVar +from copy import deepcopy +from typing import TYPE_CHECKING, Generic, TypeVar, Union, get_args, get_origin -from pydantic import BaseModel as PydanticBaseModel, ConfigDict +from pydantic import BaseModel as PydanticBaseModel, ConfigDict, create_model if TYPE_CHECKING: from sqlalchemy.sql import Select @@ -49,6 +50,25 @@ class StrictBaseModel(BaseModel): model_config = ConfigDict(from_attributes=True, populate_by_name=True, extra="forbid") +def make_partial_model(model: type[PydanticBaseModel]) -> type[PydanticBaseModel]: + """Create a version of a Pydantic model where all fields are Optional with default=None.""" + field_overrides: dict = {} + for field_name, field_info in model.model_fields.items(): + new_info = deepcopy(field_info) + ann = field_info.annotation + origin = get_origin(ann) + if not (origin is Union and type(None) in get_args(ann)): + ann = ann | None # type: ignore[operator, assignment] + new_info.default = None + field_overrides[field_name] = (ann, new_info) + + return create_model( + f"{model.__name__}Partial", + __base__=model, + **field_overrides, + ) + + class OrmClause(Generic[T], ABC): """ Base class for filtering clauses with paginated_select. diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py index 58bf8ca8e0d00..f7cb944ebbf6b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/connections.py @@ -25,7 +25,7 @@ from pydantic_core.core_schema import ValidationInfo from airflow._shared.secrets_masker import redact, should_hide_value_for_key -from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel +from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model # Response Models @@ -193,3 +193,6 @@ def validate_extra(cls, v: str | None) -> str | None: "but encountered non-JSON in `extra` field" ) return v + + +ConnectionBodyPartial = make_partial_model(ConnectionBody) 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 5bda20af0bfd5..333349ad5e1a1 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 @@ -33,7 +33,7 @@ field_validator, ) -from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel +from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model from airflow.api_fastapi.core_api.datamodels.dag_tags import DagTagResponse from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse from airflow.configuration import conf @@ -146,6 +146,9 @@ class DAGPatchBody(StrictBaseModel): is_paused: bool +DAGPatchBodyPartial = make_partial_model(DAGPatchBody) + + class DAGCollectionResponse(BaseModel): """DAG Collection serializer for responses.""" diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py index bc19207c29cdf..f952a5a777082 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py @@ -23,7 +23,7 @@ from pydantic import Field, JsonValue, model_validator from airflow._shared.secrets_masker import redact -from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel +from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model from airflow.models.base import ID_LEN from airflow.typing_compat import Self @@ -61,6 +61,9 @@ class VariableBody(StrictBaseModel): team_name: str | None = Field(max_length=50, default=None) +VariableBodyPartial = make_partial_model(VariableBody) + + class VariableCollectionResponse(BaseModel): """Variable Collection serializer for responses.""" diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py index 744dff444fd3e..05ec6b642941d 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py @@ -38,6 +38,7 @@ ) from airflow.api_fastapi.core_api.datamodels.connections import ( ConnectionBody, + ConnectionBodyPartial, ConnectionCollectionResponse, ConnectionResponse, ConnectionTestResponse, @@ -203,10 +204,17 @@ def patch_connection( status.HTTP_404_NOT_FOUND, f"The Connection with connection_id: `{connection_id}` was not found" ) - try: - ConnectionBody(**patch_body.model_dump()) - except ValidationError as e: - raise RequestValidationError(errors=e.errors()) + if update_mask: + fields_to_update = patch_body.model_fields_set & set(update_mask) + try: + ConnectionBodyPartial(**patch_body.model_dump(include=fields_to_update)) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) + else: + try: + ConnectionBody(**patch_body.model_dump()) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) update_orm_from_pydantic(connection, patch_body, update_mask) return connection diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py index f4c9186cd2fca..6fbb7c831e9d9 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py @@ -62,6 +62,7 @@ DAGCollectionResponse, DAGDetailsResponse, DAGPatchBody, + DAGPatchBodyPartial, DAGResponse, ) from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc @@ -286,6 +287,10 @@ def patch_dag( status.HTTP_400_BAD_REQUEST, "Only `is_paused` field can be updated through the REST API" ) fields_to_update = fields_to_update.intersection(update_mask) + try: + DAGPatchBodyPartial(**patch_body.model_dump(include=fields_to_update)) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) else: try: DAGPatchBody(**patch_body.model_dump()) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py b/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py index a5011768cb0aa..e363565bb92a3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/public/variables.py @@ -35,6 +35,7 @@ ) from airflow.api_fastapi.core_api.datamodels.variables import ( VariableBody, + VariableBodyPartial, ) from airflow.api_fastapi.core_api.services.public.common import BulkService from airflow.models.variable import Variable @@ -65,10 +66,17 @@ def update_orm_from_pydantic( status.HTTP_404_NOT_FOUND, f"The Variable with key: `{variable_key}` was not found" ) - try: - VariableBody(**patch_body.model_dump()) - except ValidationError as e: - raise RequestValidationError(errors=e.errors()) + if update_mask: + fields_to_update = patch_body.model_fields_set & set(update_mask) + try: + VariableBodyPartial(**patch_body.model_dump(include=fields_to_update)) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) + else: + try: + VariableBody(**patch_body.model_dump()) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) non_update_fields = {"key"} if patch_body.key != old_variable.key: diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py index 2f8d43ab219e9..1f63247cfa9ab 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py @@ -938,6 +938,34 @@ def test_patch_should_response_200_redacted_password( assert response.json() == expected_response _check_last_log(session, dag_id=None, event="patch_connection", logical_date=None, check_masked=True) + def test_patch_with_update_mask_validates_extra_as_json(self, test_client): + """When update_mask includes 'extra', the extra field validator should still reject invalid JSON.""" + self.create_connection() + response = test_client.patch( + f"/connections/{TEST_CONN_ID}", + json={ + "connection_id": TEST_CONN_ID, + "conn_type": TEST_CONN_TYPE, + "extra": "not valid json", + }, + params={"update_mask": ["extra"]}, + ) + assert response.status_code == 422 + + def test_patch_with_update_mask_rejects_extra_fields(self, test_client): + """Partial model should still forbid unknown fields.""" + self.create_connection() + response = test_client.patch( + f"/connections/{TEST_CONN_ID}", + json={ + "connection_id": TEST_CONN_ID, + "conn_type": TEST_CONN_TYPE, + "unknown_field": "value", + }, + params={"update_mask": ["host"]}, + ) + assert response.status_code == 422 + class TestConnection(TestConnectionEndpoint): def setup_method(self): diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py index 0617fa184cca4..467b03554f0b1 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py @@ -494,6 +494,23 @@ def test_patch_should_respond_404(self, test_client): body = response.json() assert f"The Variable with key: `{TEST_VARIABLE_KEY}` was not found" == body["detail"] + @pytest.mark.enable_redact + def test_patch_with_update_mask_description_only(self, test_client, session): + """PATCH with update_mask=['description'] should only update description, keeping value unchanged.""" + self.create_variables() + response = test_client.patch( + f"/variables/{TEST_VARIABLE_KEY}", + json={ + "key": TEST_VARIABLE_KEY, + "value": "ignored_value", + "description": "updated description", + }, + params={"update_mask": ["description"]}, + ) + assert response.status_code == 200 + assert response.json()["description"] == "updated description" + assert response.json()["key"] == TEST_VARIABLE_KEY + class TestPostVariable(TestVariableEndpoint): @pytest.mark.enable_redact diff --git a/airflow-core/tests/unit/api_fastapi/core_api/test_base.py b/airflow-core/tests/unit/api_fastapi/core_api/test_base.py new file mode 100644 index 0000000000000..15a47efac11f7 --- /dev/null +++ b/airflow-core/tests/unit/api_fastapi/core_api/test_base.py @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +import pytest +from pydantic import Field, ValidationError, field_validator + +from airflow.api_fastapi.core_api.base import StrictBaseModel, make_partial_model + + +class SampleModel(StrictBaseModel): + """A sample model with required and optional fields for testing.""" + + name: str = Field(max_length=50) + age: int + email: str | None = Field(default=None) + + @field_validator("name") + @classmethod + def name_must_not_be_empty(cls, v: str) -> str: + if not v.strip(): + raise ValueError("name must not be empty") + return v + + +SampleModelPartial = make_partial_model(SampleModel) + + +class TestMakePartialModel: + def test_all_fields_become_optional(self): + instance = SampleModelPartial() + assert instance.name is None + assert instance.age is None + assert instance.email is None + + def test_partial_model_accepts_subset_of_fields(self): + instance = SampleModelPartial(name="Alice") + assert instance.name == "Alice" + assert instance.age is None + + def test_full_model_still_requires_fields(self): + with pytest.raises(ValidationError): + SampleModel(email="test@example.com") + + def test_validators_are_preserved(self): + with pytest.raises(ValidationError, match="name must not be empty"): + SampleModelPartial(name=" ") + + def test_field_metadata_preserved(self): + with pytest.raises(ValidationError): + SampleModelPartial(name="x" * 51) + + def test_extra_forbid_preserved(self): + with pytest.raises(ValidationError): + SampleModelPartial(unknown_field="test") + + def test_already_optional_fields_stay_optional(self): + instance = SampleModelPartial(email="test@example.com") + assert instance.email == "test@example.com" + assert instance.name is None + + def test_partial_model_name(self): + assert SampleModelPartial.__name__ == "SampleModelPartial" From 7606f821a347c62da670c8c5278a1873d5b9b09a Mon Sep 17 00:00:00 2001 From: Kunal Bhattacharya Date: Tue, 10 Mar 2026 22:21:10 +0530 Subject: [PATCH 050/595] Remove remaining session query usages (#62758) * Remove remaining session.query usages * Remove remaining session.query usages * Change as per review comment * Minor comment update in compat.sdk --- airflow-core/tests/unit/models/test_deadline.py | 2 +- .../compat/src/airflow/providers/common/compat/sdk.py | 2 +- .../standard/tests/unit/standard/utils/test_skipmixin.py | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/airflow-core/tests/unit/models/test_deadline.py b/airflow-core/tests/unit/models/test_deadline.py index 379998efbb88c..f4f435291ce09 100644 --- a/airflow-core/tests/unit/models/test_deadline.py +++ b/airflow-core/tests/unit/models/test_deadline.py @@ -166,7 +166,7 @@ def test_prune_deadlines(self, mock_session, conditions, dagrun): mock_session.execute.return_value.all.assert_called_once() mock_session.delete.assert_called_once_with(mock_deadline) else: - mock_session.query.assert_not_called() + mock_session.execute.assert_not_called() def test_repr_with_callback_kwargs(self, deadline_orm, dagrun): repr_str = repr(deadline_orm) diff --git a/providers/common/compat/src/airflow/providers/common/compat/sdk.py b/providers/common/compat/src/airflow/providers/common/compat/sdk.py index f64fc215cf3dd..93174df7b2a28 100644 --- a/providers/common/compat/src/airflow/providers/common/compat/sdk.py +++ b/providers/common/compat/src/airflow/providers/common/compat/sdk.py @@ -326,7 +326,7 @@ # Module map: module_name -> module_path(s) # For entire modules that have been moved (e.g., timezone) -# Usage: from airflow.providers.common.compat.lazy_compat import timezone +# Usage: from airflow.providers.common.compat.sdk import timezone _MODULE_MAP: dict[str, str | tuple[str, ...]] = { "timezone": ("airflow.sdk.timezone", "airflow.utils.timezone"), "io": ("airflow.sdk.io", "airflow.io"), diff --git a/providers/standard/tests/unit/standard/utils/test_skipmixin.py b/providers/standard/tests/unit/standard/utils/test_skipmixin.py index 58e9b8e9a202a..10b46f80a5e99 100644 --- a/providers/standard/tests/unit/standard/utils/test_skipmixin.py +++ b/providers/standard/tests/unit/standard/utils/test_skipmixin.py @@ -26,7 +26,11 @@ from airflow.models.taskinstance import TaskInstance as TI from airflow.providers.common.compat.sdk import AirflowException, SkipMixin from airflow.providers.standard.operators.empty import EmptyOperator -from airflow.utils import timezone + +try: + from airflow.providers.common.compat.sdk import timezone +except ImportError: # Fallback for Airflow < 3.1 + from airflow.utils import timezone # type: ignore[attr-defined,no-redef] from airflow.utils.state import State from airflow.utils.types import DagRunType @@ -104,7 +108,7 @@ def test_skip_none_tasks(self): else: session = Mock() assert SkipMixin().skip(dag_run=None, execution_date=None, tasks=[]) is None - assert not session.query.called + assert not session.scalars.called assert not session.commit.called @pytest.mark.skipif(not AIRFLOW_V_3_0_PLUS, reason="Airflow 2 had a different implementation") From a998e28e68ec822af45d622094b6c60395644b77 Mon Sep 17 00:00:00 2001 From: Yunho Jung <87285536+yunhobb@users.noreply.github.com> Date: Wed, 11 Mar 2026 02:09:18 +0900 Subject: [PATCH 051/595] Add skip_on_exit_code support to EcsRunTaskOperator (#63274) Allow users to specify exit codes that should raise an AirflowSkipException (marking the task as skipped) via the new `skip_on_exit_code` parameter. This is consistent with the existing behavior in DockerOperator and KubernetesPodOperator. --- .../providers/amazon/aws/operators/ecs.py | 25 +++++++++++-- .../unit/amazon/aws/operators/test_ecs.py | 37 ++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py index 5e4a4b04f900b..05f0102152d4a 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py @@ -18,7 +18,7 @@ from __future__ import annotations import re -from collections.abc import Sequence +from collections.abc import Container, Sequence from datetime import timedelta from functools import cached_property from time import sleep @@ -39,7 +39,7 @@ from airflow.providers.amazon.aws.utils.identifiers import generate_uuid from airflow.providers.amazon.aws.utils.mixins import aws_template_fields from airflow.providers.amazon.aws.utils.task_log_fetcher import AwsTaskLogFetcher -from airflow.providers.common.compat.sdk import AirflowException, conf +from airflow.providers.common.compat.sdk import AirflowException, AirflowSkipException, conf from airflow.utils.helpers import prune_dict if TYPE_CHECKING: @@ -394,6 +394,9 @@ class EcsRunTaskOperator(EcsBaseOperator): :param deferrable: If True, the operator will wait asynchronously for the job to complete. This implies waiting for completion. This mode requires aiobotocore module to be installed. (default: False) + :param skip_on_exit_code: If task exits with this exit code, leave the task + in ``skipped`` state (default: None). If set to ``None``, any non-zero + exit code will be treated as a failure. Can be an int or a container of ints. :param do_xcom_push: If True, the operator will push the ECS task ARN to XCom with key 'ecs_task_arn'. Additionally, if logs are fetched, the last log message will be pushed to XCom with the key 'return_value'. (default: False) :param stop_task_on_failure: If True, attempt to stop the ECS task if the Airflow task fails @@ -461,6 +464,7 @@ def __init__( # Set the default waiter duration to 70 days (attempts*delay) # Airflow execution_timeout handles task timeout deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False), + skip_on_exit_code: int | Container[int] | None = None, stop_task_on_failure: bool = True, **kwargs, ): @@ -500,6 +504,13 @@ def __init__( self.waiter_delay = waiter_delay self.waiter_max_attempts = waiter_max_attempts self.deferrable = deferrable + self.skip_on_exit_code = ( + skip_on_exit_code + if isinstance(skip_on_exit_code, Container) + else [skip_on_exit_code] + if skip_on_exit_code is not None + else [] + ) self.stop_task_on_failure = stop_task_on_failure if self._aws_logs_enabled() and not self.wait_for_completion: @@ -763,15 +774,21 @@ def _check_success_task(self) -> None: containers = task["containers"] for container in containers: if container.get("lastStatus") == "STOPPED" and container.get("exitCode", 1) != 0: + exit_code = container.get("exitCode", 1) + if exit_code in self.skip_on_exit_code: + exception_cls: type[AirflowException] = AirflowSkipException + else: + exception_cls = AirflowException + if self.task_log_fetcher: last_logs = "\n".join( self.task_log_fetcher.get_last_log_messages(self.number_logs_exception) ) - raise AirflowException( + raise exception_cls( f"This task is not in success state - last {self.number_logs_exception} " f"logs from Cloudwatch:\n{last_logs}" ) - raise AirflowException(f"This task is not in success state {task}") + raise exception_cls(f"This task is not in success state {task}") if container.get("lastStatus") == "PENDING": raise AirflowException(f"This task is still pending {task}") if "error" in container.get("reason", "").lower(): diff --git a/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py b/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py index 5a866e36e115e..946b086e0fe32 100644 --- a/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py +++ b/providers/amazon/tests/unit/amazon/aws/operators/test_ecs.py @@ -38,7 +38,7 @@ from airflow.providers.amazon.aws.triggers.ecs import TaskDoneTrigger from airflow.providers.amazon.aws.utils.task_log_fetcher import AwsTaskLogFetcher from airflow.providers.amazon.version_compat import NOTSET -from airflow.providers.common.compat.sdk import AirflowException, TaskDeferred +from airflow.providers.common.compat.sdk import AirflowException, AirflowSkipException, TaskDeferred from unit.amazon.aws.utils.test_template_fields import validate_template_fields @@ -603,6 +603,41 @@ def test_check_success_task_not_raises(self, client_mock): self.ecs._check_success_task() client_mock.describe_tasks.assert_called_once_with(cluster="c", tasks=["arn"]) + @mock.patch.object(EcsBaseOperator, "client") + def test_check_success_task_raises_skip_exception(self, client_mock): + self.ecs.arn = "arn" + self.ecs.skip_on_exit_code = [2] + client_mock.describe_tasks.return_value = { + "tasks": [{"containers": [{"name": "container-name", "lastStatus": "STOPPED", "exitCode": 2}]}] + } + with pytest.raises(AirflowSkipException): + self.ecs._check_success_task() + + @mock.patch.object(EcsBaseOperator, "client") + @mock.patch("airflow.providers.amazon.aws.utils.task_log_fetcher.AwsTaskLogFetcher") + def test_check_success_task_skip_exception_with_logs(self, log_fetcher_mock, client_mock): + self.ecs.arn = "arn" + self.ecs.skip_on_exit_code = [2] + self.ecs.task_log_fetcher = log_fetcher_mock + log_fetcher_mock.get_last_log_messages.return_value = ["log1", "log2"] + client_mock.describe_tasks.return_value = { + "tasks": [{"containers": [{"name": "container-name", "lastStatus": "STOPPED", "exitCode": 2}]}] + } + with pytest.raises(AirflowSkipException, match="This task is not in success state"): + self.ecs._check_success_task() + + @mock.patch.object(EcsBaseOperator, "client") + def test_check_success_task_unmatched_exit_code_raises_airflow_exception(self, client_mock): + """Exit codes not in skip_on_exit_code raise AirflowException.""" + self.ecs.arn = "arn" + self.ecs.skip_on_exit_code = [2] + client_mock.describe_tasks.return_value = { + "tasks": [{"containers": [{"name": "container-name", "lastStatus": "STOPPED", "exitCode": 1}]}] + } + with pytest.raises(AirflowException) as ctx: + self.ecs._check_success_task() + assert type(ctx.value) is AirflowException + @pytest.mark.parametrize( ("launch_type", "tags"), [ From 45248f56e78670824c2b966ffb4fbd470466fef6 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:23:41 -0700 Subject: [PATCH 052/595] fix: pass through user-provided password in HiveServer2Hook for all auth modes (#62888) * fix: pass through user-provided password in HiveServer2Hook regardless of auth mechanism Previously, HiveServer2Hook.get_conn() only set the password when auth_mechanism was LDAP, CUSTOM, or PLAIN. For all other auth modes (including the default NONE), user-provided passwords were silently dropped and pyhive would default to sending 'x' as the password. Now the password is passed through whenever the user has explicitly set one in the connection, regardless of the configured auth mechanism. Fixes apache/airflow#62338 * ci: retrigger CI --- .../providers/apache/hive/hooks/hive.py | 7 +++++-- .../tests/unit/apache/hive/hooks/test_hive.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py index efa0397c0d7ea..e5ce0cc604778 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py @@ -885,8 +885,11 @@ def get_conn(self, schema: str | None = None) -> Any: auth_mechanism = db.extra_dejson.get("auth_mechanism", "KERBEROS") kerberos_service_name = db.extra_dejson.get("kerberos_service_name", "hive") - # Password should be set if in LDAP, CUSTOM or PLAIN mode - if auth_mechanism in ("LDAP", "CUSTOM", "PLAIN"): + # Pass through the password whenever the user has explicitly set one. + # Previously this was restricted to LDAP/CUSTOM/PLAIN, which caused + # user-provided passwords to be silently dropped for other auth modes + # (pyhive defaults to sending "x" when password is None). + if db.password: password = db.password from pyhive.hive import connect diff --git a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py index 94a573a2259c8..1508b2030127f 100644 --- a/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py +++ b/providers/apache/hive/tests/unit/apache/hive/hooks/test_hive.py @@ -697,6 +697,27 @@ def test_get_conn_with_password_plain(self, mock_connect): database="default", ) + @mock.patch("pyhive.hive.connect") + def test_get_conn_with_password_none_auth(self, mock_connect): + """Test that password is passed through even when auth_mechanism is NONE.""" + conn_id = "conn_none_with_password" + conn_env = CONN_ENV_PREFIX + conn_id.upper() + + with mock.patch.dict( + "os.environ", + {conn_env: "jdbc+hive2://user:mypassword@localhost:10000/default"}, + ): + HiveServer2Hook(hiveserver2_conn_id=conn_id).get_conn() + mock_connect.assert_called_once_with( + host="localhost", + port=10000, + auth="NONE", + kerberos_service_name=None, + username="user", + password="mypassword", + database="default", + ) + @pytest.mark.parametrize( ("host", "port", "schema", "message"), [ From 19fdbe4fa0ed9339f30d35eed37b04b9d769d4d4 Mon Sep 17 00:00:00 2001 From: Eason09053360 Date: Wed, 11 Mar 2026 01:24:58 +0800 Subject: [PATCH 053/595] docs: add DAG documentation for example_bash_decorator (#62948) * add_DAG_documentation_for_example_bash_decorator * run prek --- .../standard/example_dags/example_bash_decorator.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py b/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py index e5b005ce40c48..1cd8d5b61ee7b 100644 --- a/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py +++ b/providers/standard/src/airflow/providers/standard/example_dags/example_bash_decorator.py @@ -28,6 +28,19 @@ @dag(schedule=None, start_date=pendulum.datetime(2023, 1, 1, tz="UTC"), catchup=False) def example_bash_decorator(): + """ + ### Bash TaskFlow Decorator Example + This DAG demonstrates the `@task.bash` decorator for running shell commands from TaskFlow tasks. + It includes: + - Basic bash tasks and loops using `override` + - Jinja templating and context variables + - Skip behavior via non-zero exit codes and conditional branching + - Parameterized environment variables and dynamic command construction + + For details, see the Bash decorator documentation + [here](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/bash.html). + """ + @task.bash def run_me(sleep_seconds: int, task_instance_key_str: str) -> str: return f"echo {task_instance_key_str} && sleep {sleep_seconds}" From aec3744e15fff587b9be623b6b35f0840139d632 Mon Sep 17 00:00:00 2001 From: Kushal Bohra Date: Tue, 10 Mar 2026 10:37:59 -0700 Subject: [PATCH 054/595] fix(fab): recover from first idle MySQL disconnect in token auth (#62919) * fix(fab): recover from first idle MySQL disconnect in token auth Retry user deserialization once after clearing the poisoned scoped session so the first request after a server-side idle timeout does not return 500. Add regression coverage for transient disconnect recovery and factorize deserialization lookup logic to avoid duplication. * test(fab): rename retry mock for clarity --- .../fab/auth_manager/fab_auth_manager.py | 21 +++++++++++++---- .../fab/auth_manager/test_fab_auth_manager.py | 23 +++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py index 99fcbeff134da..2ad86559a74ea 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py @@ -267,16 +267,29 @@ def session(self): @cachedmethod(lambda self: self.cache, key=lambda _, token: int(token["sub"])) def deserialize_user(self, token: dict[str, Any]) -> User: + user_id = int(token["sub"]) + + def _fetch_user() -> User: + try: + return self.session.scalars(select(User).where(User.id == user_id)).one() + except NoResultFound: + raise ValueError(f"User with id {token['sub']} not found") + try: - return self.session.scalars(select(User).where(User.id == int(token["sub"]))).one() - except NoResultFound: - raise ValueError(f"User with id {token['sub']} not found") + return _fetch_user() except SQLAlchemyError: # Discard the poisoned scoped session so the next request gets a # fresh connection from the pool instead of a PendingRollbackError. with suppress(Exception): self.session.remove() - raise + try: + return _fetch_user() + except SQLAlchemyError: + # If retry also fails, remove the scoped session again to keep + # future requests from reusing a broken transaction state. + with suppress(Exception): + self.session.remove() + raise def serialize_user(self, user: User) -> dict[str, Any]: return {"sub": str(user.id)} diff --git a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py index 3b3ebd4bbbe3e..99f7dc24edc23 100644 --- a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py +++ b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py @@ -996,7 +996,7 @@ def _patched_session(auth_manager, mock_session): ids=["operational_error", "pending_rollback_error"], ) def test_db_error_calls_session_remove(self, auth_manager_with_appbuilder, raised_exc): - """session.remove() is called on SQLAlchemy errors so the next request recovers.""" + """session.remove() is called on SQLAlchemy errors before and after retry.""" mock_session = MagicMock(spec=["scalars", "remove"]) mock_session.scalars.side_effect = raised_exc auth_manager_with_appbuilder.cache.pop(99997, None) @@ -1005,7 +1005,7 @@ def test_db_error_calls_session_remove(self, auth_manager_with_appbuilder, raise with pytest.raises(type(raised_exc)): auth_manager_with_appbuilder.deserialize_user({"sub": "99997"}) - mock_session.remove.assert_called_once() + assert mock_session.remove.call_count == 2 def test_db_error_propagates_when_session_remove_raises(self, auth_manager_with_appbuilder): """The original SQLAlchemyError propagates even if session.remove() itself raises.""" @@ -1021,6 +1021,25 @@ def test_db_error_propagates_when_session_remove_raises(self, auth_manager_with_ with pytest.raises(OperationalError): auth_manager_with_appbuilder.deserialize_user({"sub": "99997"}) + assert mock_session.remove.call_count == 2 + + def test_db_error_retries_once_and_recovers(self, auth_manager_with_appbuilder): + """A transient DB disconnect is recovered by removing session and retrying once.""" + user = Mock() + user.id = 99996 + original_exc = OperationalError("connection dropped", None, Exception()) + retry_query_result = Mock() + retry_query_result.one.return_value = user + + mock_session = MagicMock(spec=["scalars", "remove"]) + mock_session.scalars.side_effect = [original_exc, retry_query_result] + auth_manager_with_appbuilder.cache.pop(user.id, None) + + with self._patched_session(auth_manager_with_appbuilder, mock_session): + result = auth_manager_with_appbuilder.deserialize_user({"sub": str(user.id)}) + + assert result == user + assert mock_session.scalars.call_count == 2 mock_session.remove.assert_called_once() From 69257f04ee0d62e6e3a25996fb24d033143cc7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ahlert?= Date: Tue, 10 Mar 2026 14:49:42 -0300 Subject: [PATCH 055/595] Improve translation completeness checker (required keys, coverage, unused) (#62983) * Improve translation completeness checker: required keys, coverage, unused - Required keys: EN base + plural forms for locale when EN has {{count}} - Merge Extra and Unused into single Unused (keys not required) - Table: Required (base EN), Required (plural), Total required, Translated, Missing, Coverage (translated/total), TODOs, Unused - Coverage uses only real translations (exclude TODO placeholders) - Rename --remove-extra to --remove-unused; remove_unused_translations() - Update docs, SKILL, README, and help SVG * Add translation:pt-BR section to boring-cyborg config * Revert "Add translation:pt-BR section to boring-cyborg config" This reverts commit dda93a9ef391741157ce150d2bf2d1256da198d6. * Apply ruff format to ui_commands.py * Update breeze docs: regenerate check-translation-completeness output * Update ui_commands.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(breeze): translation checker row style (red for missing keys) Fix unreachable red branch in style logic: use red for missing keys, yellow for todos/unused, bold green when all clear. Also remove unused missing_counts param and rename extra_keys to unused_keys. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/airflow/ui/public/i18n/README.md | 6 +- dev/breeze/doc/10_ui_tasks.rst | 6 +- ...tput_ui_check-translation-completeness.svg | 6 +- ...tput_ui_check-translation-completeness.txt | 2 +- .../airflow_breeze/commands/ui_commands.py | 246 ++++++++++-------- .../commands/ui_commands_config.py | 2 +- dev/breeze/tests/test_ui_commands.py | 77 +++++- 7 files changed, 218 insertions(+), 127 deletions(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/README.md b/airflow-core/src/airflow/ui/public/i18n/README.md index 994c499481f48..c4a269bf8ed7c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/README.md +++ b/airflow-core/src/airflow/ui/public/i18n/README.md @@ -332,16 +332,16 @@ Adding missing translations (with `TODO: translate` prefix): breeze ui check-translation-completeness --language --add-missing ``` -You can also remove extra translations from the language of your choice: +You can also remove unused translations from the language of your choice: ```bash -breeze ui check-translation-completeness --language --remove-extra +breeze ui check-translation-completeness --language --remove-unused ``` Or from all languages: ```bash -breeze ui check-translation-completeness --remove-extra +breeze ui check-translation-completeness --remove-unused ``` diff --git a/dev/breeze/doc/10_ui_tasks.rst b/dev/breeze/doc/10_ui_tasks.rst index 974a21dfccf2f..ee98f3770e4da 100644 --- a/dev/breeze/doc/10_ui_tasks.rst +++ b/dev/breeze/doc/10_ui_tasks.rst @@ -80,11 +80,11 @@ Example usage: # Add missing translations with TODO markers breeze ui check-translation-completeness --add-missing - # Remove extra translations not present in English - breeze ui check-translation-completeness --remove-extra + # Remove unused translations (keys not required) + breeze ui check-translation-completeness --remove-unused # Fix translations for a specific language - breeze ui check-translation-completeness --language de --add-missing --remove-extra + breeze ui check-translation-completeness --language de --add-missing --remove-unused ----- diff --git a/dev/breeze/doc/images/output_ui_check-translation-completeness.svg b/dev/breeze/doc/images/output_ui_check-translation-completeness.svg index de811978b2f1e..7815ad487199e 100644 --- a/dev/breeze/doc/images/output_ui_check-translation-completeness.svg +++ b/dev/breeze/doc/images/output_ui_check-translation-completeness.svg @@ -105,9 +105,9 @@ Check completeness of UI translations. ╭─ Translation options ────────────────────────────────────────────────────────────────────────────────────────────────╮ ---language    -lShow summary for a single language (e.g. en, de, pl, etc.) (TEXT) ---add-missing Add missing translations for all languages except English, prefixed with 'TODO: translate:'. ---remove-extraRemove extra translations that are present in the language but missing in English. +--language     -lShow summary for a single language (e.g. en, de, pl, etc.) (TEXT) +--add-missing  Add missing translations for all languages except English, prefixed with 'TODO: translate:'. +--remove-unusedRemove unused translations (keys present in the language but not required). ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ --verbose-vPrint verbose information about performed steps. diff --git a/dev/breeze/doc/images/output_ui_check-translation-completeness.txt b/dev/breeze/doc/images/output_ui_check-translation-completeness.txt index 68771c83bbefa..acab51d294f70 100644 --- a/dev/breeze/doc/images/output_ui_check-translation-completeness.txt +++ b/dev/breeze/doc/images/output_ui_check-translation-completeness.txt @@ -1 +1 @@ -f9abfd3676b04ed17b81796c9bcd79a9 +4d8c7e8b2fe193a4b8e8cf25a429ff7e diff --git a/dev/breeze/src/airflow_breeze/commands/ui_commands.py b/dev/breeze/src/airflow_breeze/commands/ui_commands.py index 2051c64d571a2..eb1843c4d2a01 100644 --- a/dev/breeze/src/airflow_breeze/commands/ui_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/ui_commands.py @@ -98,15 +98,15 @@ def char_key(c: str) -> tuple: class LocaleSummary(NamedTuple): """ - Summary of missing and extra translation keys for a file, per locale. + Summary of missing and unused translation keys for a file, per locale. Attributes: - missing_keys: A dictionary mapping locale codes to lists of missing translation keys. - extra_keys: A dictionary mapping locale codes to lists of extra translation keys. + missing_keys: Required keys that the locale does not have. + unused_keys: Keys present in the locale that are not required (never used at runtime). """ missing_keys: dict[str, list[str]] - extra_keys: dict[str, list[str]] + unused_keys: dict[str, list[str]] class LocaleFiles(NamedTuple): @@ -142,9 +142,17 @@ def get_plural_base(key: str, suffixes: list[str]) -> str | None: return None -def expand_plural_keys(keys: set[str], lang: str) -> set[str]: +COUNT_PLACEHOLDER = "{{count}}" + + +def expand_plural_keys(keys: set[str], lang: str, en_key_to_value: dict[str, str] | None = None) -> set[str]: """ - For a set of keys, expand all plural bases to include all required suffixes for the language. + For a set of keys, expand plural bases to include required suffixes for the language. + + When en_key_to_value is provided, only expand a base to all plural forms when at least + one of the English values for that base contains {{count}}. Keys without {{count}} + (e.g. fixed "1 Error") are not expanded, so locales are not required to have + unused forms like error_other when the source only has error_one. """ console = get_console() suffixes = PLURAL_SUFFIXES.get(lang) @@ -162,6 +170,11 @@ def expand_plural_keys(keys: set[str], lang: str) -> set[str]: base_to_suffixes.setdefault(base, set()).add(key[len(base) :]) expanded = set(keys) for base in base_to_suffixes.keys(): + if en_key_to_value is not None: + en_keys_for_base = [k for k in keys if get_plural_base(k, suffixes) == base] + any_has_count = any(COUNT_PLACEHOLDER in en_key_to_value.get(k, "") for k in en_keys_for_base) + if not any_has_count: + continue for suffix in suffixes: expanded.add(base + suffix) return expanded @@ -192,6 +205,18 @@ def flatten_keys(d: dict, prefix: str = "") -> list[str]: return keys +def flatten_keys_to_values(d: dict, prefix: str = "") -> dict[str, str]: + """Build a flat key -> value map for leaf string values (for i18n JSON).""" + result: dict[str, str] = {} + for k, v in d.items(): + full_key = f"{prefix}.{k}" if prefix else k + if isinstance(v, dict): + result.update(flatten_keys_to_values(v, full_key)) + elif isinstance(v, str): + result[full_key] = v + return result + + def compare_keys( locale_files: list[LocaleFiles], ) -> tuple[dict[str, LocaleSummary], dict[str, dict[str, int]]]: @@ -199,7 +224,7 @@ def compare_keys( Compare all non-English locales with English locale only. Returns a tuple: - - summary dict: filename -> LocaleSummary(missing, extra) + - summary dict: filename -> LocaleSummary(missing_keys, unused_keys) - missing_counts dict: filename -> {locale: count_of_missing_keys} """ all_files: set[str] = set() @@ -221,25 +246,32 @@ def compare_keys( key_sets.append(LocaleKeySet(locale=lf.locale, keys=keys)) keys_by_locale = {ks.locale: ks.keys for ks in key_sets} en_keys = keys_by_locale.get("en", set()) or set() - # Expand English keys for all required plural forms in each language - expanded_en_keys = {lang: expand_plural_keys(en_keys, lang) for lang in keys_by_locale.keys()} + en_key_to_value: dict[str, str] = {} + en_path = LOCALES_DIR / "en" / filename + if en_path.exists(): + try: + en_data = load_json(en_path) + en_key_to_value = flatten_keys_to_values(en_data) + except Exception as e: + get_console().print(f"Error loading en values from {en_path}: {e}") + # Required = EN keys plus, for bases with {{count}} in EN, all plural forms for the locale missing_keys: dict[str, list[str]] = {} - extra_keys: dict[str, list[str]] = {} + unused_keys: dict[str, list[str]] = {} missing_counts[filename] = {} for ks in key_sets: if ks.locale == "en": continue - required_keys = expanded_en_keys.get(ks.locale, en_keys) + required_keys = expand_plural_keys(en_keys, ks.locale, en_key_to_value) if ks.keys is None: missing_keys[ks.locale] = list(required_keys) - extra_keys[ks.locale] = [] + unused_keys[ks.locale] = [] missing_counts[filename][ks.locale] = len(required_keys) else: missing = list(required_keys - ks.keys) missing_keys[ks.locale] = missing - extra_keys[ks.locale] = list(ks.keys - required_keys) + unused_keys[ks.locale] = sorted(ks.keys - required_keys) missing_counts[filename][ks.locale] = len(missing) - summary[filename] = LocaleSummary(missing_keys=missing_keys, extra_keys=extra_keys) + summary[filename] = LocaleSummary(missing_keys=missing_keys, unused_keys=unused_keys) return summary, missing_counts @@ -296,15 +328,15 @@ def print_language_summary(locale_files: list[LocaleFiles], summary: dict[str, L for lf in sorted(locale_files): locale = lf.locale file_missing: dict[str, list[str]] = {} - file_extra: dict[str, list[str]] = {} + file_unused: dict[str, list[str]] = {} for filename, diff in sorted(summary.items()): missing_keys = diff.missing_keys.get(locale, []) - extra_keys = diff.extra_keys.get(locale, []) + unused_keys = diff.unused_keys.get(locale, []) if missing_keys: file_missing[filename] = missing_keys - if extra_keys: - file_extra[filename] = extra_keys - if file_missing or file_extra: + if unused_keys: + file_unused[filename] = unused_keys + if file_missing or file_unused: if locale == "en": continue console.print(Panel(f"[bold yellow]{locale}[/bold yellow]", style="blue")) @@ -315,34 +347,38 @@ def print_language_summary(locale_files: list[LocaleFiles], summary: dict[str, L console.print(f" [magenta]{filename}[/magenta]:") for k in keys: console.print(f" [yellow]{k}[/yellow]") - if file_extra: - found_difference = True - console.print("[red] Extra keys (need to be removed/updated):[/red]") - for filename, keys in file_extra.items(): + if file_unused: + console.print("[yellow] Unused keys (not required, can be removed):[/yellow]") + for filename, keys in file_unused.items(): console.print(f" [magenta]{filename}[/magenta]:") for k in keys: - console.print(f" [yellow]{k}[/yellow]") + console.print(f" [dim]{k}[/dim]") return found_difference +def is_todo_value(s: str) -> bool: + """Return True if the string is a TODO placeholder (e.g. 'TODO: translate: ...').""" + return isinstance(s, str) and s.strip().startswith("TODO: translate") + + def count_todos(obj) -> int: """Count TODO: translate entries in a dict or list.""" if isinstance(obj, dict): return sum(count_todos(v) for v in obj.values()) if isinstance(obj, list): return sum(count_todos(v) for v in obj) - if isinstance(obj, str) and obj.strip().startswith("TODO: translate"): + if is_todo_value(obj): return 1 return 0 -def print_translation_progress(locale_files, missing_counts, summary): +def print_translation_progress(locale_files, summary): console = get_console() from rich.table import Table tables = defaultdict(lambda: Table(show_lines=True)) all_files = set() - coverage_per_language = {} # Collect total coverage per language + coverage_per_language = {} for lf in locale_files: all_files.update(lf.files) @@ -355,91 +391,94 @@ def print_translation_progress(locale_files, missing_counts, summary): table = tables[lang] table.title = f"Translation Progress: {lang}" table.add_column("File", style="bold cyan") - table.add_column("Missing", style="red") - table.add_column("Extra", style="yellow") - table.add_column("TODOs", style="magenta") + table.add_column("Required (base EN)", style="dim") + table.add_column("Required (plural)", style="dim") + table.add_column("Total required", style="bold") table.add_column("Translated", style="green") - table.add_column("Total", style="bold") + table.add_column("Missing", style="red") table.add_column("Coverage", style="bold") - table.add_column("Completed", style="bold") + table.add_column("TODOs", style="magenta") + table.add_column("Unused", style="yellow") + total_required_base = 0 + total_required_plural = 0 + total_required = 0 + total_translated = 0 total_missing = 0 - total_extra = 0 total_todos = 0 - total_translated = 0 - total_total = 0 + total_unused = 0 for filename in sorted(all_files): - file_path = Path(LOCALES_DIR / lang / filename) - # Always get total from English version en_path = Path(LOCALES_DIR / "en" / filename) - if en_path.exists(): - with open(en_path) as f: - en_data = json.load(f) - file_total = sum(1 for _ in flatten_keys(en_data)) - else: - file_total = 0 + file_path = Path(LOCALES_DIR / lang / filename) + diff = summary.get(filename, LocaleSummary({}, {})) + if not en_path.exists(): + continue + with open(en_path) as f: + en_data = json.load(f) + en_keys = set(flatten_keys(en_data)) + en_key_to_value = flatten_keys_to_values(en_data) + required_keys = expand_plural_keys(en_keys, lang, en_key_to_value) + required_base_en = len(en_keys) + required_plural = len(required_keys) - required_base_en + total_req = len(required_keys) + file_missing = len(diff.missing_keys.get(lang, [])) + file_unused = len(diff.unused_keys.get(lang, [])) if file_path.exists(): with open(file_path) as f: - data = json.load(f) - file_missing = missing_counts.get(filename, {}).get(lang, 0) - file_extra = len(summary.get(filename, LocaleSummary({}, {})).extra_keys.get(lang, [])) - - file_todos = count_todos(data) + lang_data = json.load(f) + lang_key_to_value = flatten_keys_to_values(lang_data) + file_translated = sum( + 1 + for k in required_keys + if k in lang_key_to_value and not is_todo_value(lang_key_to_value[k]) + ) + file_todos = sum( + 1 for k in required_keys if k in lang_key_to_value and is_todo_value(lang_key_to_value[k]) + ) if file_todos > 0: has_todos = True - file_translated = file_total - file_missing - # Coverage: translated / total - file_coverage_percent = 100 * file_translated / file_total if file_total else 100 - # Complete percent: (translated - todos) / translated - file_actual_translated = file_translated - file_todos - complete_percent = 100 * file_actual_translated / file_translated if file_translated else 100 - style = ( - "bold green" - if file_missing == 0 and file_extra == 0 and file_todos == 0 - else ( - "yellow" if file_missing < file_total or file_extra > 0 or file_todos > 0 else "red" - ) - ) else: - file_missing = file_total - file_extra = len(summary.get(filename, LocaleSummary({}, {})).extra_keys.get(lang, [])) - file_todos = 0 file_translated = 0 - file_coverage_percent = 0 - complete_percent = 0 + file_todos = 0 + file_coverage = 100 * file_translated / total_req if total_req else 100.0 + if file_missing > 0: style = "red" + elif file_todos > 0 or file_unused > 0: + style = "yellow" + else: + style = "bold green" table.add_row( f"[bold reverse]{filename}[/bold reverse]", + str(required_base_en), + str(required_plural), + str(total_req), + str(file_translated), str(file_missing), - str(file_extra), + f"{file_coverage:.1f}%", str(file_todos), - str(file_translated), - str(file_total), - f"{file_coverage_percent:.1f}%", - f"{complete_percent:.1f}%", + str(file_unused), style=style, ) + total_required_base += required_base_en + total_required_plural += required_plural + total_required += total_req + total_translated += file_translated total_missing += file_missing - total_extra += file_extra total_todos += file_todos - total_translated += file_translated - total_total += file_total - - # Calculate totals for this language - total_coverage_percent = 100 * total_translated / total_total if total_total else 100 - total_actual_translated = total_translated - total_todos - total_complete_percent = 100 * total_actual_translated / total_translated if total_translated else 100 + total_unused += file_unused - coverage_per_language[lang] = total_coverage_percent + total_coverage = 100 * total_translated / total_required if total_required else 100.0 + coverage_per_language[lang] = total_coverage table.add_row( "All files", + str(total_required_base), + str(total_required_plural), + str(total_required), + str(total_translated), str(total_missing), - str(total_extra), + f"{total_coverage:.1f}%", str(total_todos), - str(total_translated), - str(total_total), - f"{total_coverage_percent:.1f}%", - f"{total_complete_percent:.1f}%", - style="red" if total_complete_percent < 100 else "bold green", + str(total_unused), + style="red" if total_missing > 0 or total_todos > 0 or total_unused > 0 else "bold green", ) for _lang, table in tables.items(): @@ -512,16 +551,16 @@ def sort_dict_keys(obj): console.print(f"[green]Added missing translations to {lang_path}[/green]") -def remove_extra_translations(language: str, summary: dict[str, LocaleSummary]): +def remove_unused_translations(language: str, summary: dict[str, LocaleSummary]): """ - Remove extra translations for the selected language. + Remove unused translations for the selected language. - Removes keys that are present in the language file but missing in the English file. + Removes keys that are present in the language file but are not required. """ console = get_console() for filename, diff in summary.items(): - extra_keys = set(diff.extra_keys.get(language, [])) - if not extra_keys: + unused_keys = set(diff.unused_keys.get(language, [])) + if not unused_keys: continue lang_path = LOCALES_DIR / language / filename try: @@ -530,12 +569,12 @@ def remove_extra_translations(language: str, summary: dict[str, LocaleSummary]): console.print(f"[yellow]Failed to load {language} file {lang_path}: {e}[/yellow]") continue - # Helper to recursively remove extra keys + # Helper to recursively remove unused keys def remove_keys(dst, prefix=""): keys_to_remove = [] for k, v in list(dst.items()): full_key = f"{prefix}.{k}" if prefix else k - if full_key in extra_keys: + if full_key in unused_keys: keys_to_remove.append(k) elif isinstance(v, dict): remove_keys(v, full_key) @@ -556,7 +595,7 @@ def sort_dict_keys(obj): with open(lang_path, "w", encoding="utf-8") as f: json.dump(lang_data, f, ensure_ascii=False, indent=2) f.write("\n") # Ensure newline at the end of the file - console.print(f"[green]Removed {len(extra_keys)} extra translations from {lang_path}[/green]") + console.print(f"[green]Removed {len(unused_keys)} unused translations from {lang_path}[/green]") @click.group(cls=BreezeGroup, name="ui", help="Tools for UI development and maintenance") @@ -581,15 +620,15 @@ def ui_group(): help="Add missing translations for all languages except English, prefixed with 'TODO: translate:'.", ) @click.option( - "--remove-extra", + "--remove-unused", is_flag=True, default=False, - help="Remove extra translations that are present in the language but missing in English.", + help="Remove unused translations (keys present in the language but not required).", ) @option_verbose @option_dry_run def check_translation_completeness( - language: str | None = None, add_missing: bool = False, remove_extra: bool = False + language: str | None = None, add_missing: bool = False, remove_unused: bool = False ): locale_files = get_locale_files() console = get_console() @@ -608,13 +647,13 @@ def check_translation_completeness( for filename, diff in summary.items(): filtered_summary[filename] = LocaleSummary( missing_keys={lf.locale: diff.missing_keys.get(lf.locale, [])}, - extra_keys={lf.locale: diff.extra_keys.get(lf.locale, [])}, + unused_keys={lf.locale: diff.unused_keys.get(lf.locale, [])}, ) add_missing_translations(lf.locale, filtered_summary) # After adding, re-run the summary for all languages summary, missing_counts = compare_keys(get_locale_files()) - if remove_extra and language != "en": - # Loop through all languages except 'en' and remove extra translations + if remove_unused and language != "en": + # Loop through all languages except 'en' and remove unused translations if language: language_files = [lf for lf in locale_files if lf.locale == language] else: @@ -624,9 +663,9 @@ def check_translation_completeness( for filename, diff in summary.items(): filtered_summary[filename] = LocaleSummary( missing_keys={lf.locale: diff.missing_keys.get(lf.locale, [])}, - extra_keys={lf.locale: diff.extra_keys.get(lf.locale, [])}, + unused_keys={lf.locale: diff.unused_keys.get(lf.locale, [])}, ) - remove_extra_translations(lf.locale, filtered_summary) + remove_unused_translations(lf.locale, filtered_summary) # After removing, re-run the summary for all languages summary, missing_counts = compare_keys(get_locale_files()) if language: @@ -642,7 +681,7 @@ def check_translation_completeness( for filename, diff in summary.items(): filtered_summary[filename] = LocaleSummary( missing_keys={language: diff.missing_keys.get(language, [])}, - extra_keys={language: diff.extra_keys.get(language, [])}, + unused_keys={language: diff.unused_keys.get(language, [])}, ) lang_diff = print_language_summary( [lf for lf in locale_files if lf.locale == language], filtered_summary @@ -653,7 +692,6 @@ def check_translation_completeness( found_difference = found_difference or lang_diff has_todos, coverage_per_language = print_translation_progress( [lf for lf in locale_files if language is None or lf.locale == language], - missing_counts, summary, ) if not found_difference and not has_todos: diff --git a/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py b/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py index 83f9fd34fbfdc..e50e10e6fd6c1 100644 --- a/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py +++ b/dev/breeze/src/airflow_breeze/commands/ui_commands_config.py @@ -31,7 +31,7 @@ "options": [ "--language", "--add-missing", - "--remove-extra", + "--remove-unused", ], }, ], diff --git a/dev/breeze/tests/test_ui_commands.py b/dev/breeze/tests/test_ui_commands.py index 843d985ed0281..8f7b844be796a 100644 --- a/dev/breeze/tests/test_ui_commands.py +++ b/dev/breeze/tests/test_ui_commands.py @@ -61,6 +61,25 @@ def test_expand_plural_keys_polish(self): assert "message_many" in expanded assert "message_other" in expanded + def test_expand_plural_keys_only_expands_when_en_has_count_placeholder(self): + # When en has error_one without {{count}}, we must not require error_other + keys = {"error_one"} + en_key_to_value = {"error_one": "1 Error"} + expanded = expand_plural_keys(keys, "pt", en_key_to_value) + assert "error_one" in expanded + assert "error_other" not in expanded + + def test_expand_plural_keys_expands_when_en_has_count_placeholder(self): + # Polish has 4 plural forms (_one, _few, _many, _other). Input has only _one and _other; + # expansion must add _few and _many so the test verifies the expansion logic ran. + keys = {"warning_one", "warning_other"} + en_key_to_value = {"warning_one": "1 Warning", "warning_other": "{{count}} Warnings"} + expanded = expand_plural_keys(keys, "pl", en_key_to_value) + assert "warning_one" in expanded + assert "warning_other" in expanded + assert "warning_few" in expanded + assert "warning_many" in expanded + class TestFlattenKeys: def test_flatten_simple_dict(self): @@ -112,7 +131,7 @@ def test_compare_keys_identical(self, tmp_path): assert "test.json" in summary assert summary["test.json"].missing_keys.get("de", []) == [] - assert summary["test.json"].extra_keys.get("de", []) == [] + assert summary["test.json"].unused_keys.get("de", []) == [] finally: ui_commands.LOCALES_DIR = original_locales_dir @@ -171,16 +190,50 @@ def test_compare_keys_with_extra(self, tmp_path): summary, missing_counts = compare_keys(locale_files) assert "test.json" in summary - assert "extra" in summary["test.json"].extra_keys.get("de", []) + assert "extra" in summary["test.json"].unused_keys.get("de", []) + finally: + ui_commands.LOCALES_DIR = original_locales_dir + + def test_compare_keys_optional_plural_unused_when_no_count(self, tmp_path): + """Plural variant of EN base with no {{count}} in EN is reported as unused.""" + en_dir = tmp_path / "en" + en_dir.mkdir() + de_dir = tmp_path / "de" + de_dir.mkdir() + + # EN has only error_one (no {{count}}), so error_other is never used at runtime + en_data = {"dagWarnings": {"error_one": "1 Error"}} + de_data = {"dagWarnings": {"error_one": "1 Fehler", "error_other": "{{count}} Fehler"}} + + (en_dir / "test.json").write_text(json.dumps(en_data)) + (de_dir / "test.json").write_text(json.dumps(de_data)) + + import airflow_breeze.commands.ui_commands as ui_commands + + original_locales_dir = ui_commands.LOCALES_DIR + ui_commands.LOCALES_DIR = tmp_path + + try: + locale_files = [ + LocaleFiles(locale="en", files=["test.json"]), + LocaleFiles(locale="de", files=["test.json"]), + ] + summary, _ = compare_keys(locale_files) + + # EN base has no {{count}}, so error_other is not required and is unused + assert "dagWarnings.error_other" in summary["test.json"].unused_keys.get("de", []) finally: ui_commands.LOCALES_DIR = original_locales_dir class TestLocaleSummary: def test_locale_summary_creation(self): - summary = LocaleSummary(missing_keys={"de": ["key1", "key2"]}, extra_keys={"de": ["key3"]}) + summary = LocaleSummary( + missing_keys={"de": ["key1", "key2"]}, + unused_keys={"de": ["key3"]}, + ) assert summary.missing_keys == {"de": ["key1", "key2"]} - assert summary.extra_keys == {"de": ["key3"]} + assert summary.unused_keys == {"de": ["key3"]} class TestLocaleFiles: @@ -255,7 +308,7 @@ def test_add_missing_translations(self, tmp_path): try: summary = LocaleSummary( missing_keys={"de": ["farewell"]}, - extra_keys={"de": []}, + unused_keys={"de": []}, ) add_missing_translations("de", {"test.json": summary}) @@ -267,9 +320,9 @@ def test_add_missing_translations(self, tmp_path): ui_commands.LOCALES_DIR = original_locales_dir -class TestRemoveExtraTranslations: - def test_remove_extra_translations(self, tmp_path): - from airflow_breeze.commands.ui_commands import remove_extra_translations +class TestRemoveUnusedTranslations: + def test_remove_unused_translations(self, tmp_path): + from airflow_breeze.commands.ui_commands import remove_unused_translations de_dir = tmp_path / "de" de_dir.mkdir() @@ -285,11 +338,11 @@ def test_remove_extra_translations(self, tmp_path): try: summary = LocaleSummary( missing_keys={"de": []}, - extra_keys={"de": ["extra"]}, + unused_keys={"de": ["extra"]}, ) - remove_extra_translations("de", {"test.json": summary}) + remove_unused_translations("de", {"test.json": summary}) - # Check that the extra key was removed + # Check that the unused key was removed de_data_updated = json.loads((de_dir / "test.json").read_text()) assert "extra" not in de_data_updated assert "greeting" in de_data_updated @@ -330,7 +383,7 @@ def test_natural_sort_matches_eslint(self, tmp_path): try: summary = LocaleSummary( missing_keys={"de": list(en_data.keys())}, - extra_keys={"de": []}, + unused_keys={"de": []}, ) add_missing_translations("de", {"test.json": summary}) From 838e8e8acfd668cbc726ef3776cb15a2606d0d86 Mon Sep 17 00:00:00 2001 From: Nitochkin <62333822+Crowiant@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:11:05 +0100 Subject: [PATCH 056/595] Add operators for Google BidManager API (#62521) Co-authored-by: Anton Nitochkin --- .../marketing_platform/bid_manager.rst | 119 ++++++ .../marketing_platform/display_video.rst | 1 - providers/google/provider.yaml | 15 + .../providers/google/get_provider_info.py | 21 ++ .../marketing_platform/hooks/bid_manager.py | 126 +++++++ .../operators/bid_manager.py | 347 ++++++++++++++++++ .../marketing_platform/sensors/bid_manager.py | 88 +++++ .../sensors/display_video.py | 2 +- .../marketing_platform/example_bid_manager.py | 214 +++++++++++ .../hooks/test_bid_manager.py | 145 ++++++++ .../operators/test_bid_manager.py | 257 +++++++++++++ .../sensors/test_bid_manager.py | 44 +++ 12 files changed, 1377 insertions(+), 2 deletions(-) create mode 100644 providers/google/docs/operators/marketing_platform/bid_manager.rst create mode 100644 providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py create mode 100644 providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py create mode 100644 providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py create mode 100644 providers/google/tests/system/google/marketing_platform/example_bid_manager.py create mode 100644 providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py create mode 100644 providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py create mode 100644 providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py diff --git a/providers/google/docs/operators/marketing_platform/bid_manager.rst b/providers/google/docs/operators/marketing_platform/bid_manager.rst new file mode 100644 index 0000000000000..c29e2687ed521 --- /dev/null +++ b/providers/google/docs/operators/marketing_platform/bid_manager.rst @@ -0,0 +1,119 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +Google Bid Manager API Operators +======================================= +`Google Bid Manager API `__ is a programmatic interface for the Display & Video 360 reporting feature. +It lets users build and run report queries, and download the resulting report file. + +Prerequisite Tasks +^^^^^^^^^^^^^^^^^^ + +.. include:: /operators/_partials/prerequisite_tasks.rst + +.. _howto/operator:GoogleBidManagerCreateQueryOperator: + +Creating a Query +^^^^^^^^^^^^^^^^ + +To create a query using Bid Manager, use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerCreateQueryOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_create_query_operator] + :end-before: [END howto_google_bid_manager_create_query_operator] + +Use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerCreateQueryOperator` +parameters which allow you to dynamically determine values. You can provide body definition using ``.json`` file +as this operator supports this template extension. +The result is saved to :ref:`XCom `, which allows the result to be used by other operators. + +.. _howto/operator:GoogleBidManagerRunQueryOperator: + +Run Query +^^^^^^^^^ + +To run a query using Bid Manager, use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerRunQueryOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_run_query_report_operator] + :end-before: [END howto_google_bid_manager_run_query_report_operator] + +You can use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerRunQueryOperator` +parameters which allow you to dynamically determine values. +The result is saved to :ref:`XCom `, which allows the result to be used by other operators. + +.. _howto/operator:GoogleBidManagerDeleteQueryOperator: + +Deleting a Query +^^^^^^^^^^^^^^^^ + +To delete a query using Bid Manager, use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDeleteQueryOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_delete_query_operator] + :end-before: [END howto_google_bid_manager_delete_query_operator] + +You can use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDeleteQueryOperator` +parameters which allow you to dynamically determine values. + +.. _howto/operator:GoogleBidManagerRunQuerySensor: + +Waiting for query +^^^^^^^^^^^^^^^^^ + +To wait for the report use +:class:`~airflow.providers.google.marketing_platform.sensors.bid_manager.GoogleBidManagerRunQuerySensor`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_wait_run_query_sensor] + :end-before: [END howto_google_bid_manager_wait_run_query_sensor] + +Use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.sensors.bid_manager.GoogleBidManagerRunQuerySensor` +parameters which allow you to dynamically determine values. + +.. _howto/operator:GoogleBidManagerDownloadReportOperator: + +Downloading a report +^^^^^^^^^^^^^^^^^^^^ + +To download a report to GCS bucket use +:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDownloadReportOperator`. + +.. exampleinclude:: /../../google/tests/system/google/marketing_platform/example_bid_manager.py + :language: python + :dedent: 4 + :start-after: [START howto_google_bid_manager_get_report_operator] + :end-before: [END howto_google_bid_manager_get_report_operator] + +Use :ref:`Jinja templating ` with +:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDownloadReportOperator` +parameters which allow you to dynamically determine values. diff --git a/providers/google/docs/operators/marketing_platform/display_video.rst b/providers/google/docs/operators/marketing_platform/display_video.rst index 04cc9acbd04ac..7ce014303aba0 100644 --- a/providers/google/docs/operators/marketing_platform/display_video.rst +++ b/providers/google/docs/operators/marketing_platform/display_video.rst @@ -43,7 +43,6 @@ Use :ref:`Jinja templating ` with :template-fields:`airflow.providers.google.marketing_platform.operators.display_video.GoogleDisplayVideo360CreateSDFDownloadTaskOperator` parameters which allow you to dynamically determine values. - .. _howto/operator:GoogleDisplayVideo360SDFtoGCSOperator: Save SDF files in the Google Cloud Storage diff --git a/providers/google/provider.yaml b/providers/google/provider.yaml index c12c3e80d2993..04541d5569d92 100644 --- a/providers/google/provider.yaml +++ b/providers/google/provider.yaml @@ -464,6 +464,12 @@ integrations: how-to-guide: - /docs/apache-airflow-providers-google/operators/cloud/ray.rst tags: [gcp] + - integration-name: Google Bid Manager API + external-doc-url: https://developers.google.com/bid-manager + logo: /docs/integration-logos/Google-Search-Ads360.png + how-to-guide: + - /docs/apache-airflow-providers-google/operators/marketing_platform/bid_manager.rst + tags: [gmp] operators: - integration-name: Google Ads @@ -629,6 +635,9 @@ operators: - integration-name: Google Ray python-modules: - airflow.providers.google.cloud.operators.ray + - integration-name: Google Bid Manager API + python-modules: + - airflow.providers.google.marketing_platform.operators.bid_manager sensors: - integration-name: Google BigQuery @@ -694,6 +703,9 @@ sensors: - integration-name: Google Cloud Tasks python-modules: - airflow.providers.google.cloud.sensors.tasks + - integration-name: Google Bid Manager API + python-modules: + - airflow.providers.google.marketing_platform.sensors.bid_manager filesystems: - airflow.providers.google.cloud.fs.gcs @@ -913,6 +925,9 @@ hooks: - integration-name: Google Ray python-modules: - airflow.providers.google.cloud.hooks.ray + - integration-name: Google Bid Manager API + python-modules: + - airflow.providers.google.marketing_platform.hooks.bid_manager bundles: - integration-name: Google Cloud Storage (GCS) diff --git a/providers/google/src/airflow/providers/google/get_provider_info.py b/providers/google/src/airflow/providers/google/get_provider_info.py index cab93aa57c9e1..fec76eb9d5f3b 100644 --- a/providers/google/src/airflow/providers/google/get_provider_info.py +++ b/providers/google/src/airflow/providers/google/get_provider_info.py @@ -473,6 +473,15 @@ def get_provider_info(): "how-to-guide": ["/docs/apache-airflow-providers-google/operators/cloud/ray.rst"], "tags": ["gcp"], }, + { + "integration-name": "Google Bid Manager API", + "external-doc-url": "https://developers.google.com/bid-manager", + "logo": "/docs/integration-logos/Google-Search-Ads360.png", + "how-to-guide": [ + "/docs/apache-airflow-providers-google/operators/marketing_platform/bid_manager.rst" + ], + "tags": ["gmp"], + }, ], "operators": [ { @@ -694,6 +703,10 @@ def get_provider_info(): "integration-name": "Google Ray", "python-modules": ["airflow.providers.google.cloud.operators.ray"], }, + { + "integration-name": "Google Bid Manager API", + "python-modules": ["airflow.providers.google.marketing_platform.operators.bid_manager"], + }, ], "sensors": [ { @@ -780,6 +793,10 @@ def get_provider_info(): "integration-name": "Google Cloud Tasks", "python-modules": ["airflow.providers.google.cloud.sensors.tasks"], }, + { + "integration-name": "Google Bid Manager API", + "python-modules": ["airflow.providers.google.marketing_platform.sensors.bid_manager"], + }, ], "filesystems": ["airflow.providers.google.cloud.fs.gcs"], "asset-uris": [ @@ -1062,6 +1079,10 @@ def get_provider_info(): "integration-name": "Google Ray", "python-modules": ["airflow.providers.google.cloud.hooks.ray"], }, + { + "integration-name": "Google Bid Manager API", + "python-modules": ["airflow.providers.google.marketing_platform.hooks.bid_manager"], + }, ], "bundles": [ { diff --git a/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py b/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py new file mode 100644 index 0000000000000..4722a7720ae84 --- /dev/null +++ b/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py @@ -0,0 +1,126 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""This module contains Google Bid Manager API hook.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from googleapiclient.discovery import Resource, build + +from airflow.providers.google.common.hooks.base_google import GoogleBaseHook + + +class GoogleBidManagerHook(GoogleBaseHook): + """Hook for Google Bid Manager API.""" + + _conn: Resource | None = None + + def __init__( + self, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__( + gcp_conn_id=gcp_conn_id, + impersonation_chain=impersonation_chain, + **kwargs, + ) + self.api_version = api_version + + def get_conn(self) -> Resource: + """Retrieve connection to Bid Manager API.""" + if not self._conn: + http_authorized = self._authorize() + self._conn = build( + "doubleclickbidmanager", + self.api_version, + http=http_authorized, + cache_discovery=False, + ) + return self._conn + + def create_query(self, query: dict[str, Any]) -> dict: + """ + Create a query. + + :param query: Query object to be passed to request body. + """ + response = self.get_conn().queries().create(body=query).execute(num_retries=self.num_retries) + return response + + def delete_query(self, query_id: str) -> None: + """ + Delete a stored query as well as the associated stored reports. + + :param query_id: Query ID to delete. + """ + self.get_conn().queries().delete(queryId=query_id).execute(num_retries=self.num_retries) + + def get_query(self, query_id: str) -> dict: + """ + Retrieve a stored query. + + :param query_id: Query ID to retrieve. + """ + response = self.get_conn().queries().get(queryId=query_id).execute(num_retries=self.num_retries) + return response + + def list_queries(self) -> list[dict]: + """Retrieve stored queries.""" + response = self.get_conn().queries().list().execute(num_retries=self.num_retries) + return response.get("queries", []) + + def run_query(self, query_id: str, params: dict[str, Any] | None) -> dict: + """ + Run a stored query to generate a report. + + :param query_id: Query ID to run. + :param params: Parameters for the report. + """ + return ( + self.get_conn().queries().run(queryId=query_id, body=params).execute(num_retries=self.num_retries) + ) + + def get_report(self, query_id: str, report_id: str) -> dict: + """ + Retrieve a report. + + :param query_id: Query ID for which report was generated. + :param report_id: Report ID to retrieve. + """ + return ( + self.get_conn() + .queries() + .reports() + .get(queryId=query_id, reportId=report_id) + .execute(num_retries=self.num_retries) + ) + + def list_reports(self, query_id: str) -> dict: + """ + Retrieve a list of reports. + + :param query_id: Query ID for which report was generated. + """ + return ( + self.get_conn().queries().reports().list(queryId=query_id).execute(num_retries=self.num_retries) + ) diff --git a/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py b/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py new file mode 100644 index 0000000000000..2f4a43ea4b02d --- /dev/null +++ b/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py @@ -0,0 +1,347 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""This module contains operators for Bid Manager API part of the Google Display & Video 360.""" + +from __future__ import annotations + +import json +import shutil +import tempfile +import urllib.request +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any +from urllib.parse import urlsplit + +from airflow.exceptions import AirflowException +from airflow.providers.google.cloud.hooks.gcs import GCSHook +from airflow.providers.google.marketing_platform.hooks.bid_manager import GoogleBidManagerHook +from airflow.providers.google.version_compat import BaseOperator + +if TYPE_CHECKING: + from airflow.providers.common.compat.sdk import Context + + +class GoogleBidManagerCreateQueryOperator(BaseOperator): + """ + Creates a query. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerCreateQueryOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/create` + + :param body: Report object passed to the request's body as described here: + https://developers.google.com/bid-manager/v2/queries#Query + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with the first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "body", + "impersonation_chain", + ) + template_ext: Sequence[str] = (".json",) + + def __init__( + self, + *, + body: dict[str, Any], + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.body = body + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + + def prepare_template(self) -> None: + # If .json is passed then we have to read the file + if isinstance(self.body, str) and self.body.endswith(".json"): + with open(self.body) as file: + self.body = json.load(file) + + def execute(self, context: Context) -> dict: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + self.log.info("Creating Bid Manager API query.") + response = hook.create_query(query=self.body) + query_id = response["queryId"] + context["task_instance"].xcom_push(key="query_id", value=query_id) + self.log.info("Created query with ID: %s", query_id) + return response + + +class GoogleBidManagerRunQueryOperator(BaseOperator): + """ + Runs a stored query to generate a report. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerRunQueryOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/run` + + :param query_id: Query ID to run. + :param parameters: Parameters for running a report as described here: + https://developers.google.com/bid-manager/v2/queries/run + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "parameters", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + parameters: dict[str, Any] | None = None, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.query_id = query_id + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.parameters = parameters + self.impersonation_chain = impersonation_chain + + def execute(self, context: Context) -> dict: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + self.log.info( + "Running query %s with the following parameters:\n %s", + self.query_id, + self.parameters, + ) + response = hook.run_query(query_id=self.query_id, params=self.parameters) + context["task_instance"].xcom_push(key="query_id", value=response["key"]["queryId"]) + context["task_instance"].xcom_push(key="report_id", value=response["key"]["reportId"]) + return response + + +class GoogleBidManagerDeleteQueryOperator(BaseOperator): + """ + Deletes a stored query as well as the associated stored reports. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerDeleteQueryOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/delete` + + :param query_id: Query ID to delete. + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + self.query_id = query_id + + def execute(self, context: Context) -> None: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + self.log.info("Deleting query with id: %s and all connected reports", self.query_id) + hook.delete_query(query_id=self.query_id) + self.log.info("Report deleted.") + + +class GoogleBidManagerDownloadReportOperator(BaseOperator): + """ + Retrieves a stored query. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerDownloadReportOperator` + + .. seealso:: + Check also the official API docs: + `https://developers.google.com/bid-manager/v2/queries/get` + + :param report_id: Report ID to retrieve. + :param query_id: Query ID for which report was generated.. + :param bucket_name: The bucket to upload to. + :param report_name: The report name to set when uploading the local file. + :param chunk_size: File will be downloaded in chunks of this many bytes. + :param gzip: Option to compress local file or file data for upload + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "report_id", + "bucket_name", + "report_name", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + report_id: str, + bucket_name: str, + report_name: str | None = None, + gzip: bool = True, + chunk_size: int = 10 * 1024 * 1024, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.query_id = query_id + self.report_id = report_id + self.chunk_size = chunk_size + self.gzip = gzip + self.bucket_name = bucket_name + self.report_name = report_name + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + + def _resolve_file_name(self, name: str) -> str: + new_name = name if name.endswith(".csv") else f"{name}.csv" + new_name = f"{new_name}.gz" if self.gzip else new_name + return new_name + + @staticmethod + def _set_bucket_name(name: str) -> str: + bucket = name if not name.startswith("gs://") else name[5:] + return bucket.strip("/") + + def execute(self, context: Context): + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + gcs_hook = GCSHook( + gcp_conn_id=self.gcp_conn_id, + impersonation_chain=self.impersonation_chain, + ) + + resource = hook.get_report(query_id=self.query_id, report_id=self.report_id) + status = resource.get("metadata", {}).get("status", {}).get("state") + if resource and status not in ["DONE", "FAILED"]: + raise AirflowException(f"Report {self.report_id} for query {self.query_id} is still running") + + # If no custom report_name provided, use Bid Manager name + file_url = resource["metadata"]["googleCloudStoragePath"] + if urllib.parse.urlparse(file_url).scheme == "file": + raise AirflowException("Accessing local file is not allowed in this operator") + report_name = self.report_name or urlsplit(file_url).path.split("/")[-1] + report_name = self._resolve_file_name(report_name) + + # Download the report + self.log.info("Starting downloading report %s", self.report_id) + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + with urllib.request.urlopen(file_url) as response: # nosec + shutil.copyfileobj(response, temp_file, length=self.chunk_size) + + temp_file.flush() + # Upload the local file to bucket + bucket_name = self._set_bucket_name(self.bucket_name) + gcs_hook.upload( + bucket_name=bucket_name, + object_name=report_name, + gzip=self.gzip, + filename=temp_file.name, + mime_type="text/csv", + ) + self.log.info( + "Report %s was saved in bucket %s as %s.", + self.report_id, + self.bucket_name, + report_name, + ) + context["task_instance"].xcom_push(key="report_name", value=report_name) diff --git a/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py b/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py new file mode 100644 index 0000000000000..75c806c6b622b --- /dev/null +++ b/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py @@ -0,0 +1,88 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Sensor for detecting the completion of DV360 Bid Manager reports.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from airflow.providers.common.compat.sdk import BaseSensorOperator +from airflow.providers.google.marketing_platform.hooks.bid_manager import GoogleBidManagerHook + +if TYPE_CHECKING: + from airflow.providers.common.compat.sdk import Context + + +class GoogleBidManagerRunQuerySensor(BaseSensorOperator): + """ + Sensor for detecting the completion of DV360 Bid Manager reports for API v2. + + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:GoogleBidManagerRunQuerySensor` + + :param query_id: Query ID for which report was generated + :param report_id: Report ID for which you want to wait + :param api_version: The version of the api that will be requested for example 'v3'. + :param gcp_conn_id: The connection ID to use when fetching connection info. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ( + "query_id", + "report_id", + "impersonation_chain", + ) + + def __init__( + self, + *, + query_id: str, + report_id: str, + api_version: str = "v2", + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.query_id = query_id + self.report_id = report_id + self.api_version = api_version + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + + def poke(self, context: Context) -> bool: + hook = GoogleBidManagerHook( + gcp_conn_id=self.gcp_conn_id, + api_version=self.api_version, + impersonation_chain=self.impersonation_chain, + ) + + response = hook.get_report(query_id=self.query_id, report_id=self.report_id) + status = response.get("metadata", {}).get("status", {}).get("state") + self.log.info("STATUS OF THE REPORT %s FOR QUERY %s: %s", self.report_id, self.query_id, status) + if response and status in ["DONE", "FAILED"]: + return True + return False diff --git a/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py b/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py index 433262c7698c9..83ab3df4f99e8 100644 --- a/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py +++ b/providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -"""Sensor for detecting the completion of DV360 reports.""" +"""Sensor for detecting the completion of DV360 SDF operations.""" from __future__ import annotations diff --git a/providers/google/tests/system/google/marketing_platform/example_bid_manager.py b/providers/google/tests/system/google/marketing_platform/example_bid_manager.py new file mode 100644 index 0000000000000..9315258fa12d0 --- /dev/null +++ b/providers/google/tests/system/google/marketing_platform/example_bid_manager.py @@ -0,0 +1,214 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Example Airflow DAG that shows how to use Bid Manager API from GoogleDisplayVideo360. +""" + +from __future__ import annotations + +import json +import os +from datetime import datetime +from typing import cast + +from airflow.models.dag import DAG +from airflow.models.xcom_arg import XComArg +from airflow.providers.google.cloud.operators.gcs import GCSCreateBucketOperator, GCSDeleteBucketOperator +from airflow.providers.google.marketing_platform.operators.bid_manager import ( + GoogleBidManagerCreateQueryOperator, + GoogleBidManagerDeleteQueryOperator, + GoogleBidManagerDownloadReportOperator, + GoogleBidManagerRunQueryOperator, +) +from airflow.providers.google.marketing_platform.sensors.bid_manager import ( + GoogleBidManagerRunQuerySensor, +) + +try: + from airflow.sdk import TriggerRule +except ImportError: + # Compatibility for Airflow < 3.1 + from airflow.utils.trigger_rule import TriggerRule # type: ignore[no-redef,attr-defined] +from google.cloud.exceptions import NotFound + +try: + from airflow.sdk import task +except ImportError: + # Airflow 2 path + from airflow.decorators import task # type: ignore[attr-defined,no-redef] +from airflow.providers.google.cloud.hooks.secret_manager import ( + GoogleCloudSecretManagerHook, +) + +from system.google.gcp_api_client_helpers import create_airflow_connection, delete_airflow_connection + +DAG_ID = "bid_manager" +ENV_ID = os.environ.get("SYSTEM_TESTS_ENV_ID", "default") +CONNECTION_TYPE = "google_cloud_platform" +CONN_ID = "google_display_video_default" +DISPLAY_VIDEO_SERVICE_ACCOUNT_KEY = "google_display_video_service_account_key" +IS_COMPOSER = bool(os.environ.get("COMPOSER_ENVIRONMENT", "")) + + +PROJECT_ID = os.environ.get("SYSTEM_TESTS_GCP_PROJECT", "default") +BUCKET_NAME = f"bucket_{DAG_ID}_{ENV_ID}" +ADVERTISER_ID = os.environ.get("GMP_ADVERTISER_ID", "1234567") + +REPORT = { + "metadata": { + "title": "Airflow Test Report", + "dataRange": {"range": "LAST_7_DAYS"}, + "format": "CSV", + "sendNotification": False, + }, + "params": { + "type": "STANDARD", + "groupBys": ["FILTER_DATE", "FILTER_PARTNER"], + "filters": [{"type": "FILTER_PARTNER", "value": ADVERTISER_ID}], + "metrics": ["METRIC_IMPRESSIONS", "METRIC_CLICKS"], + }, + "schedule": {"frequency": "ONE_TIME"}, +} + +PARAMETERS = { + "dataRange": {"range": "LAST_7_DAYS"}, +} + + +def get_secret(secret_id: str) -> str: + hook = GoogleCloudSecretManagerHook() + if hook.secret_exists(secret_id=secret_id): + return hook.access_secret(secret_id=secret_id).payload.data.decode() + raise NotFound(f"The secret {secret_id} not found") + + +with DAG( + DAG_ID, + start_date=datetime(2021, 1, 1), + catchup=False, + tags=["example", "bid_manager"], + schedule="@once", +) as dag: + + @task + def get_display_video_service_account_key(): + return get_secret(secret_id=DISPLAY_VIDEO_SERVICE_ACCOUNT_KEY) + + get_display_video_service_account_key_task = get_display_video_service_account_key() + + @task + def create_connection_display_video(connection_id: str, key) -> None: + conn_extra_json = json.dumps( + { + "keyfile_dict": key, + "project": PROJECT_ID, + "scope": "https://www.googleapis.com/auth/display-video, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/doubleclickbidmanager", + } + ) + create_airflow_connection( + connection_id=connection_id, + connection_conf={"conn_type": CONNECTION_TYPE, "extra": conn_extra_json}, + is_composer=IS_COMPOSER, + ) + + create_connection_display_video_task = create_connection_display_video( + connection_id=CONN_ID, key=get_display_video_service_account_key_task + ) + + @task(task_id="delete_connection_task") + def delete_connection_display_video(connection_id: str) -> None: + delete_airflow_connection(connection_id=connection_id, is_composer=IS_COMPOSER) + + delete_connection_task = delete_connection_display_video(connection_id=CONN_ID) + + create_bucket = GCSCreateBucketOperator( + task_id="create_bucket", bucket_name=BUCKET_NAME, project_id=PROJECT_ID, gcp_conn_id=CONN_ID + ) + # [START howto_google_bid_manager_create_query_operator] + create_query = GoogleBidManagerCreateQueryOperator( + body=REPORT, task_id="create_query", gcp_conn_id=CONN_ID + ) + + query_id = cast("str", XComArg(create_query, key="query_id")) + # [END howto_google_bid_manager_create_query_operator] + + # [START howto_google_bid_manager_run_query_report_operator] + run_query = GoogleBidManagerRunQueryOperator( + query_id=query_id, parameters=PARAMETERS, task_id="run_report", gcp_conn_id=CONN_ID + ) + + query_id = cast("str", XComArg(run_query, key="query_id")) + report_id = cast("str", XComArg(run_query, key="report_id")) + # [END howto_google_bid_manager_run_query_report_operator] + + # [START howto_google_bid_manager_wait_run_query_sensor] + wait_for_query = GoogleBidManagerRunQuerySensor( + task_id="wait_for_query", + query_id=query_id, + report_id=report_id, + gcp_conn_id=CONN_ID, + ) + # [END howto_google_bid_manager_wait_run_query_sensor] + + # [START howto_google_bid_manager_get_report_operator] + get_report = GoogleBidManagerDownloadReportOperator( + query_id=query_id, + report_id=report_id, + task_id="get_report", + bucket_name=BUCKET_NAME, + report_name="test1.csv", + gcp_conn_id=CONN_ID, + ) + + delete_bucket = GCSDeleteBucketOperator( + task_id="delete_bucket", + bucket_name=BUCKET_NAME, + gcp_conn_id=CONN_ID, + trigger_rule=TriggerRule.ALL_DONE, + ) + # [END howto_google_bid_manager_get_report_operator] + + # [START howto_google_bid_manager_delete_query_operator] + delete_query = GoogleBidManagerDeleteQueryOperator( + query_id=query_id, task_id="delete_query", trigger_rule=TriggerRule.ALL_DONE, gcp_conn_id=CONN_ID + ) + # [END howto_google_bid_manager_delete_query_operator] + + ( + get_display_video_service_account_key_task + >> create_connection_display_video_task # type: ignore + >> create_bucket + >> create_query + >> run_query + >> wait_for_query + >> get_report + >> delete_query + >> delete_bucket + >> delete_connection_task + ) + + from tests_common.test_utils.watcher import watcher + + # This test needs watcher in order to properly mark success/failure + # when "tearDown" task with trigger rule is part of the DAG + list(dag.tasks) >> watcher() + +from tests_common.test_utils.system_tests import get_test_run # noqa: E402 + +# Needed to run the example DAG with pytest (see: tests/system/README.md#run_via_pytest) +test_run = get_test_run(dag) diff --git a/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py b/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py new file mode 100644 index 0000000000000..d3cd91419aa90 --- /dev/null +++ b/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py @@ -0,0 +1,145 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from unittest import mock + +from airflow.providers.google.marketing_platform.hooks.bid_manager import GoogleBidManagerHook + +from unit.google.cloud.utils.base_gcp_mock import mock_base_gcp_hook_default_project_id + +API_VERSION = "v2" +GCP_CONN_ID = "google_cloud_default" + + +class TestGoogleBidManagerHook: + def setup_method(self): + with mock.patch( + "airflow.providers.google.common.hooks.base_google.GoogleBaseHook.__init__", + new=mock_base_gcp_hook_default_project_id, + ): + self.hook = GoogleBidManagerHook(api_version=API_VERSION, gcp_conn_id=GCP_CONN_ID) + + @mock.patch( + "airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook._authorize" + ) + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.build") + def test_gen_conn(self, mock_build, mock_authorize): + result = self.hook.get_conn() + mock_build.assert_called_once_with( + "doubleclickbidmanager", + API_VERSION, + http=mock_authorize.return_value, + cache_discovery=False, + ) + assert mock_build.return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_create_query(self, get_conn_mock): + body = {"body": "test"} + + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.create.return_value.execute.return_value = ( + return_value + ) + result = self.hook.create_query(query=body) + + get_conn_mock.return_value.queries.return_value.create.assert_called_once_with(body=body) + + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_delete_query(self, get_conn_mock): + query_id = "QUERY_ID" + + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.delete.return_value.execute.return_value = ( + return_value + ) + self.hook.delete_query(query_id=query_id) + + get_conn_mock.return_value.queries.return_value.delete.assert_called_once_with(queryId=query_id) + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_get_query(self, get_conn_mock): + query_id = "QUERY_ID" + + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.get.return_value.execute.return_value = return_value + result = self.hook.get_query(query_id=query_id) + + get_conn_mock.return_value.queries.return_value.get.assert_called_once_with(queryId=query_id) + + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_list_queries(self, get_conn_mock): + queries = ["test"] + return_value = {"queries": queries} + get_conn_mock.return_value.queries.return_value.list.return_value.execute.return_value = return_value + result = self.hook.list_queries() + + get_conn_mock.return_value.queries.return_value.list.assert_called_once_with() + + assert queries == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_run_query(self, get_conn_mock): + query_id = "QUERY_ID" + params = {"params": "test"} + return_value = "TEST" + get_conn_mock.return_value.queries.return_value.run.return_value.execute.return_value = return_value + + result = self.hook.run_query(query_id=query_id, params=params) + + get_conn_mock.return_value.queries.return_value.run.assert_called_once_with( + queryId=query_id, body=params + ) + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_get_report(self, get_conn_mock): + query_id = "QUERY_ID" + report_id = "REPORT_ID" + return_value = "TEST_REPORT" + ( + get_conn_mock.return_value.queries.return_value.reports.return_value.get.return_value.execute.return_value + ) = return_value + + result = self.hook.get_report(query_id=query_id, report_id=report_id) + + get_conn_mock.return_value.queries.return_value.reports.return_value.get.assert_called_once_with( + queryId=query_id, reportId=report_id + ) + assert return_value == result + + @mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn") + def test_list_reports(self, get_conn_mock): + query_id = "QUERY_ID" + reports = ["report1", "report2"] + return_value = {"reports": reports} + ( + get_conn_mock.return_value.queries.return_value.reports.return_value.list.return_value.execute.return_value + ) = return_value + + result = self.hook.list_reports(query_id=query_id) + + get_conn_mock.return_value.queries.return_value.reports.return_value.list.assert_called_once_with( + queryId=query_id + ) + assert reports == result["reports"] diff --git a/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py b/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py new file mode 100644 index 0000000000000..20c323e57c00d --- /dev/null +++ b/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py @@ -0,0 +1,257 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import json +from tempfile import NamedTemporaryFile +from unittest import mock + +import pytest +from sqlalchemy import delete + +from airflow.exceptions import AirflowException +from airflow.models import TaskInstance as TI +from airflow.providers.google.marketing_platform.operators.bid_manager import ( + GoogleBidManagerCreateQueryOperator, + GoogleBidManagerDeleteQueryOperator, + GoogleBidManagerDownloadReportOperator, + GoogleBidManagerRunQueryOperator, +) +from airflow.utils import timezone +from airflow.utils.session import create_session + +from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS + +API_VERSION = "v2" +GCP_CONN_ID = "google_cloud_default" +IMPERSONATION_CHAIN = ["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"] + +DEFAULT_DATE = timezone.datetime(2021, 1, 1) +REPORT_ID = "report_id" +BUCKET_NAME = "test_bucket" +REPORT_NAME = "test_report.csv" +QUERY_ID = FILENAME = "test.csv" + + +class TestGoogleBidManagerDeleteQueryOperator: + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute(self, hook_mock): + op = GoogleBidManagerDeleteQueryOperator( + query_id=QUERY_ID, api_version=API_VERSION, task_id="test_task" + ) + op.execute(context=None) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version=API_VERSION, + impersonation_chain=None, + ) + hook_mock.return_value.delete_query.assert_called_once_with(query_id=QUERY_ID) + + +@pytest.mark.db_test +class TestGoogleBidManagerDownloadReportOperator: + def setup_method(self): + with create_session() as session: + session.execute(delete(TI)) + + def teardown_method(self): + with create_session() as session: + session.execute(delete(TI)) + + @pytest.mark.parametrize( + ("file_path", "should_except"), [("https://host/path", False), ("file:/path/to/file", True)] + ) + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.shutil") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.urllib.request") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.tempfile") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GCSHook") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute( + self, + mock_hook, + mock_gcs_hook, + mock_temp, + mock_request, + mock_shutil, + file_path, + should_except, + ): + mock_temp.NamedTemporaryFile.return_value.__enter__.return_value.name = FILENAME + mock_hook.return_value.get_report.return_value = { + "metadata": { + "status": {"state": "DONE"}, + "googleCloudStoragePath": file_path, + } + } + # Create mock context with task_instance + mock_context = {"task_instance": mock.Mock()} + + op = GoogleBidManagerDownloadReportOperator( + query_id=QUERY_ID, + report_id=REPORT_ID, + bucket_name=BUCKET_NAME, + report_name=REPORT_NAME, + task_id="test_task", + ) + if should_except: + with pytest.raises(AirflowException): + op.execute(context=mock_context) + return + op.execute(context=mock_context) + mock_hook.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version="v2", + impersonation_chain=None, + ) + mock_hook.return_value.get_report.assert_called_once_with(report_id=REPORT_ID, query_id=QUERY_ID) + + mock_gcs_hook.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + impersonation_chain=None, + ) + mock_gcs_hook.return_value.upload.assert_called_once_with( + bucket_name=BUCKET_NAME, + filename=FILENAME, + gzip=True, + mime_type="text/csv", + object_name=REPORT_NAME + ".gz", + ) + mock_context["task_instance"].xcom_push.assert_called_once_with( + key="report_name", value=REPORT_NAME + ".gz" + ) + + @pytest.mark.parametrize( + "test_bucket_name", + [BUCKET_NAME, f"gs://{BUCKET_NAME}", "XComArg", "{{ ti.xcom_pull(task_ids='taskflow_op') }}"], + ) + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.shutil") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.urllib.request") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.tempfile") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GCSHook") + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_set_bucket_name( + self, + mock_hook, + mock_gcs_hook, + mock_temp, + mock_request, + mock_shutil, + test_bucket_name, + dag_maker, + ): + mock_temp.NamedTemporaryFile.return_value.__enter__.return_value.name = FILENAME + mock_hook.return_value.get_report.return_value = { + "metadata": {"status": {"state": "DONE"}, "googleCloudStoragePath": "TEST"} + } + with dag_maker(dag_id="test_set_bucket_name", start_date=DEFAULT_DATE) as dag: + if BUCKET_NAME not in test_bucket_name: + + @dag.task(task_id="taskflow_op") + def f(): + return BUCKET_NAME + + taskflow_op = f() + + op = GoogleBidManagerDownloadReportOperator( + query_id=QUERY_ID, + report_id=REPORT_ID, + bucket_name=test_bucket_name if test_bucket_name != "XComArg" else taskflow_op, + report_name=REPORT_NAME, + task_id="test_task", + ) + + if test_bucket_name == "{{ ti.xcom_pull(task_ids='taskflow_op') }}": + taskflow_op >> op + + if AIRFLOW_V_3_0_PLUS: + dag.test() + else: + dr = dag_maker.create_dagrun() + for ti in dr.get_task_instances(): + ti.run() + + mock_gcs_hook.return_value.upload.assert_called_once_with( + bucket_name=BUCKET_NAME, + filename=FILENAME, + gzip=True, + mime_type="text/csv", + object_name=REPORT_NAME + ".gz", + ) + + +class TestGoogleBidManagerRunQueryOperator: + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute(self, hook_mock): + parameters = {"param": "test"} + + # Create mock context with task_instance + mock_context = {"task_instance": mock.Mock()} + + hook_mock.return_value.run_query.return_value = { + "key": { + "queryId": QUERY_ID, + "reportId": REPORT_ID, + } + } + op = GoogleBidManagerRunQueryOperator( + query_id=QUERY_ID, + parameters=parameters, + api_version=API_VERSION, + task_id="test_task", + ) + op.execute(context=mock_context) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version=API_VERSION, + impersonation_chain=None, + ) + + mock_context["task_instance"].xcom_push.assert_any_call(key="query_id", value=QUERY_ID) + mock_context["task_instance"].xcom_push.assert_any_call(key="report_id", value=REPORT_ID) + hook_mock.return_value.run_query.assert_called_once_with(query_id=QUERY_ID, params=parameters) + + +class TestGoogleBidManagerCreateQueryOperator: + @mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook") + def test_execute(self, hook_mock): + body = {"body": "test"} + + # Create mock context with task_instance + mock_context = {"task_instance": mock.Mock()} + + hook_mock.return_value.create_query.return_value = {"queryId": QUERY_ID} + op = GoogleBidManagerCreateQueryOperator(body=body, task_id="test_task") + op.execute(context=mock_context) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version="v2", + impersonation_chain=None, + ) + hook_mock.return_value.create_query.assert_called_once_with(query=body) + mock_context["task_instance"].xcom_push.assert_called_once_with(key="query_id", value=QUERY_ID) + + def test_prepare_template(self): + body = {"key": "value"} + with NamedTemporaryFile("w+", suffix=".json") as f: + f.write(json.dumps(body)) + f.flush() + op = GoogleBidManagerCreateQueryOperator(body=body, task_id="test_task") + op.prepare_template() + + assert isinstance(op.body, dict) + assert op.body == body diff --git a/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py b/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py new file mode 100644 index 0000000000000..f48810e60dea7 --- /dev/null +++ b/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py @@ -0,0 +1,44 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from unittest import mock + +from airflow.providers.google.marketing_platform.sensors.bid_manager import ( + GoogleBidManagerRunQuerySensor, +) + +MODULE_NAME = "airflow.providers.google.marketing_platform.sensors.bid_manager" + +GCP_CONN_ID = "google_cloud_default" + + +class TestGoogleBidManagerRunQuerySensor: + @mock.patch(f"{MODULE_NAME}.GoogleBidManagerHook") + @mock.patch(f"{MODULE_NAME}.BaseSensorOperator") + def test_poke(self, mock_base_op, hook_mock): + query_id = "QUERY_ID" + report_id = "REPORT_ID" + op = GoogleBidManagerRunQuerySensor(query_id=query_id, report_id=report_id, task_id="test_task") + op.poke(context=None) + hook_mock.assert_called_once_with( + gcp_conn_id=GCP_CONN_ID, + api_version="v2", + impersonation_chain=None, + ) + hook_mock.return_value.get_report.assert_called_once_with(query_id=query_id, report_id=report_id) From 9217499bbdbd65783d59e8652dbeee6102ef6ab6 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:34:12 +0000 Subject: [PATCH 057/595] Handle concurrent create race in DatabricksReposCreateOperator when ignore_existing_repo=True. If create_repo() fails, the operator now re-checks repository existence and proceeds if the repository was created concurrently; otherwise, the original exception is re-raised. Add unit tests covering recovery and failure propagation under concurrent create scenarios. (#62422) Co-authored-by: Sameer Mesiah --- .../databricks/operators/databricks_repos.py | 29 ++++++++- .../operators/test_databricks_repos.py | 62 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py index d77a9dc640dfd..cb513ea8873ba 100644 --- a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py +++ b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py @@ -142,15 +142,40 @@ def execute(self, context: Context): ) payload["path"] = self.repo_path existing_repo_id = None + if self.repo_path is not None: existing_repo_id = self._hook.get_repo_by_path(self.repo_path) if existing_repo_id is not None and not self.ignore_existing_repo: raise AirflowException(f"Repo with path '{self.repo_path}' already exists") + if existing_repo_id is None: - result = self._hook.create_repo(payload) - repo_id = result["id"] + try: + result = self._hook.create_repo(payload) + repo_id = result["id"] + except Exception: + # If ignore_existing_repo is False, preserve existing behavior + # and propagate the original create failure. + if not self.ignore_existing_repo: + raise + + # When ignore_existing_repo=True, attempt to recover from a possible + # create-time conflict (e.g., repo created concurrently). + if self.repo_path is not None: + repo_id = self._hook.get_repo_by_path(self.repo_path) + + # Only treat this as success if the repo now exists. + # If it still does not exist, re-raise the original exception + # to avoid masking genuine create failures. + if repo_id is None: + raise + + self.log.info( + "Repository at path '%s' already exists; continuing because ignore_existing_repo=True.", + self.repo_path, + ) else: repo_id = existing_repo_id + # update repo if necessary if self.branch is not None: self._hook.update_repo(str(repo_id), {"branch": str(self.branch)}) diff --git a/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py b/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py index dea5155cd0d1d..398050cf1ff8f 100644 --- a/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py +++ b/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py @@ -210,6 +210,68 @@ def test_create_ignore_existing_plus_checkout(self, db_mock_class): db_mock.get_repo_by_path.assert_called_once_with(repo_path) db_mock.update_repo.assert_called_once_with("123", {"branch": "releases"}) + @mock.patch("airflow.providers.databricks.operators.databricks_repos.DatabricksHook") + def test_create_conflict_recovered_when_ignore_flag_set(self, db_mock_class): + """ + Test that create-time conflict is recovered when ignore_existing_repo=True. + """ + git_url = "https://github.com/test/test" + repo_path = "/Repos/Project1/test-repo" + + op = DatabricksReposCreateOperator( + task_id=TASK_ID, + git_url=git_url, + repo_path=repo_path, + ignore_existing_repo=True, + ) + + db_mock = db_mock_class.return_value + + # After first existence check, repo is not found. + # After second existence check, repo exists (created concurrently). + db_mock.get_repo_by_path.side_effect = [None, "123"] + + db_mock.create_repo.side_effect = Exception("Conflict") + + result = op.execute(None) + + db_mock_class.assert_called_once_with( + DEFAULT_CONN_ID, + retry_limit=op.databricks_retry_limit, + retry_delay=op.databricks_retry_delay, + caller="DatabricksReposCreateOperator", + ) + + db_mock.create_repo.assert_called_once_with({"url": git_url, "provider": "gitHub", "path": repo_path}) + + assert result == "123" + + @mock.patch("airflow.providers.databricks.operators.databricks_repos.DatabricksHook") + def test_create_conflict_not_recovered_when_repo_still_missing(self, db_mock_class): + """ + Test that create failure is re-raised if repo does not exist after failure. + """ + git_url = "https://github.com/test/test" + repo_path = "/Repos/Project1/test-repo" + + op = DatabricksReposCreateOperator( + task_id=TASK_ID, + git_url=git_url, + repo_path=repo_path, + ignore_existing_repo=True, + ) + + db_mock = db_mock_class.return_value + + # Repo not found before or after create failure. + db_mock.get_repo_by_path.side_effect = [None, None] + db_mock.create_repo.side_effect = Exception("Create failure") + + with pytest.raises(Exception, match="Create failure"): + op.execute(None) + + db_mock.create_repo.assert_called_once() + def test_init_exception(self): """ Tests handling of incorrect parameters passed to ``__init__`` From af80c491a8cabbc9d3697b7ebf243cecb48816a7 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:35:13 +0000 Subject: [PATCH 058/595] Refactor timeout handling in DatabricksSqlHook to use explicit signaling (#62623) Replace implicit timeout detection based on Timer.is_alive() with explicit timeout signaling via threading.Event. Timeout classification now checks an explicit signal set by the timeout callback instead of inferring state from thread lifecycle behavior. Preserves existing cancellation semantics and exception types. Unit tests have been adjusted accordingly. Co-authored-by: Sameer Mesiah --- .../databricks/hooks/databricks_sql.py | 48 ++++++++++++------- .../databricks/hooks/test_databricks_sql.py | 39 +++++++++------ 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py b/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py index 127f6b71c708a..2c2164bd9c78b 100644 --- a/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py +++ b/providers/databricks/src/airflow/providers/databricks/hooks/databricks_sql.py @@ -52,14 +52,23 @@ T = TypeVar("T") -def create_timeout_thread(cur, execution_timeout: timedelta | None) -> threading.Timer | None: - if execution_timeout is not None: - seconds_to_timeout = execution_timeout.total_seconds() - t = threading.Timer(seconds_to_timeout, cur.connection.cancel) - else: - t = None +def create_timeout_thread( + cur, execution_timeout: timedelta | None +) -> tuple[threading.Timer | None, threading.Event | None]: + """Create a timeout timer that cancels the connection and sets a timeout flag.""" + if not execution_timeout: + return None, None - return t + timeout_event = threading.Event() + + def _cancel(): + timeout_event.set() + cur.connection.cancel() + + timer = threading.Timer(execution_timeout.total_seconds(), _cancel) + timer.start() + + return timer, timeout_event class DatabricksSqlHook(BaseDatabricksHook, DbApiHook): @@ -290,22 +299,25 @@ def run( self.set_autocommit(conn, autocommit) with closing(conn.cursor()) as cur: - t = create_timeout_thread(cur, execution_timeout) + timer, timeout_event = create_timeout_thread(cur, execution_timeout) - # TODO: adjust this to make testing easier try: self._run_command(cur, sql_statement, parameters) + except Exception as e: - if t is None or t.is_alive(): - raise DatabricksSqlExecutionError( - f"Error running SQL statement: {sql_statement}. {str(e)}" - ) - raise DatabricksSqlExecutionTimeout( - f"Timeout threshold exceeded for SQL statement: {sql_statement} was cancelled." - ) + if timeout_event and timeout_event.is_set(): + raise DatabricksSqlExecutionTimeout( + f"Timeout threshold exceeded for SQL statement: " + f"{sql_statement} was cancelled." + ) from e + + raise DatabricksSqlExecutionError( + f"Error running SQL statement: {sql_statement}. {str(e)}" + ) from e + finally: - if t is not None: - t.cancel() + if timer: + timer.cancel() if query_id := cur.query_id: self.log.info("Databricks query id: %s", query_id) diff --git a/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py b/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py index 98ea7e1d34779..d661f5b0714ec 100644 --- a/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py +++ b/providers/databricks/tests/unit/databricks/hooks/test_databricks_sql.py @@ -18,7 +18,6 @@ # from __future__ import annotations -import threading from collections import namedtuple from datetime import timedelta from unittest import mock @@ -509,8 +508,12 @@ def test_execution_timeout_exceeded( description=get_cursor_descriptions(cursor_descriptions), ) - # Simulate a timeout - mock_create_timeout_thread.return_value = threading.Timer(cur, execution_timeout) + mock_event = mock.MagicMock() + mock_event.is_set.return_value = True # simulate timeout + + mock_timer = mock.MagicMock() + + mock_create_timeout_thread.return_value = (mock_timer, mock_event) mock_run_command.side_effect = Exception("Mocked exception") @@ -532,20 +535,22 @@ def test_execution_timeout_exceeded( "cursor_descriptions", [(("id", "value"),)], ) -def test_create_timeout_thread( - mock_get_conn, - mock_get_requests, - mock_timer, - cursor_descriptions, -): +def test_create_timeout_thread(mock_get_conn, mock_get_requests, cursor_descriptions): + cur = mock.MagicMock( rowcount=1, description=get_cursor_descriptions(cursor_descriptions), ) + timeout = timedelta(seconds=1) - thread = create_timeout_thread(cur=cur, execution_timeout=timeout) - mock_timer.assert_called_once_with(timeout.total_seconds(), cur.connection.cancel) - assert thread is not None + + timer, event = create_timeout_thread(cur=cur, execution_timeout=timeout) + + assert timer is not None + assert event is not None + assert not event.is_set() + + timer.cancel() @pytest.mark.parametrize( @@ -562,9 +567,15 @@ def test_create_timeout_thread_no_timeout( rowcount=1, description=get_cursor_descriptions(cursor_descriptions), ) - thread = create_timeout_thread(cur=cur, execution_timeout=None) + + timer, timeout_event = create_timeout_thread( + cur=cur, + execution_timeout=None, + ) + mock_timer.assert_not_called() - assert thread is None + assert timer is None + assert timeout_event is None def test_get_openlineage_default_schema_with_no_schema_set(): From b78ba6db4726e95bb9cd5f05407077c05ba01043 Mon Sep 17 00:00:00 2001 From: ANKIT KUMAR Date: Wed, 11 Mar 2026 00:09:06 +0530 Subject: [PATCH 059/595] Fix Breeze unit tests for milestone tagging (#63031) The GITHUB_REPOSITORY environment variable is set in the CI pipeline to the repository fork (e.g. Ironankit525/airflow). This overrides the default value of 'apache/airflow' in the option_github_repository of the Breeze CLI. This causes the test assertions to fail when they expect the comment URL to use 'apache/airflow' but it actually uses the fork repository. This commit explicitly passes '--github-repository apache/airflow' in the test arguments to ensure they are unaffected by this environment variable. --- dev/breeze/tests/test_set_milestone.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/breeze/tests/test_set_milestone.py b/dev/breeze/tests/test_set_milestone.py index e7c1c29153404..78aa8f19325f6 100644 --- a/dev/breeze/tests/test_set_milestone.py +++ b/dev/breeze/tests/test_set_milestone.py @@ -446,6 +446,8 @@ def test_find_milestone_should_set_and_comment( "testuser", "--github-token", "fake-token", + "--github-repository", + "apache/airflow", ], ) @@ -550,6 +552,8 @@ def test_not_find_milestone_should_comment_warning( "testuser", "--github-token", "fake-token", + "--github-repository", + "apache/airflow", ], ) From 6a05ea550fd62b0fe1273cccdad2a2dfb9d56749 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:55:18 -0400 Subject: [PATCH 060/595] chore(deps-dev): bump babel-loader (#63283) Bumps the fab-ui-package-updates group with 1 update in the /providers/fab/src/airflow/providers/fab/www directory: [babel-loader](https://github.com/babel/babel-loader). Updates `babel-loader` from 10.0.0 to 10.1.0 - [Release notes](https://github.com/babel/babel-loader/releases) - [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel-loader/compare/v10.0.0...v10.1.0) --- updated-dependencies: - dependency-name: babel-loader dependency-version: 10.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: fab-ui-package-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../airflow/providers/fab/www/package.json | 2 +- .../airflow/providers/fab/www/pnpm-lock.yaml | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/providers/fab/src/airflow/providers/fab/www/package.json b/providers/fab/src/airflow/providers/fab/www/package.json index b3106458723b7..3ea9e9319137e 100644 --- a/providers/fab/src/airflow/providers/fab/www/package.json +++ b/providers/fab/src/airflow/providers/fab/www/package.json @@ -43,7 +43,7 @@ "@babel/eslint-parser": "^7.28.6", "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.0", - "babel-loader": "^10.0.0", + "babel-loader": "^10.1.0", "copy-webpack-plugin": "^14.0.0", "css-loader": "7.1.4", "css-minimizer-webpack-plugin": "^8.0.0", diff --git a/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml b/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml index 8bfe6c3596fa5..d40f3100cef54 100644 --- a/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml +++ b/providers/fab/src/airflow/providers/fab/www/pnpm-lock.yaml @@ -34,8 +34,8 @@ importers: specifier: ^7.29.0 version: 7.29.0(@babel/core@7.29.0) babel-loader: - specifier: ^10.0.0 - version: 10.0.0(@babel/core@7.29.0)(webpack@5.105.4) + specifier: ^10.1.0 + version: 10.1.0(@babel/core@7.29.0)(webpack@5.105.4) copy-webpack-plugin: specifier: ^14.0.0 version: 14.0.0(webpack@5.105.4) @@ -919,12 +919,18 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - babel-loader@10.0.0: - resolution: {integrity: sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==} + babel-loader@10.1.0: + resolution: {integrity: sha512-5HTUZa013O4SWEYlJDHexrqSIYkWatfA9w/ZZQa7V2nMc0dRWkfu/0pmioC7XMYm8M7Z/3+q42NWj6e+fAT0MQ==} engines: {node: ^18.20.0 || ^20.10.0 || >=22.0.0} peerDependencies: - '@babel/core': ^7.12.0 + '@babel/core': ^7.12.0 || ^8.0.0-beta.1 + '@rspack/core': ^1.0.0 || ^2.0.0-0 webpack: '>=5.61.0' + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true babel-plugin-polyfill-corejs2@0.4.15: resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==} @@ -3329,10 +3335,11 @@ snapshots: astral-regex@2.0.0: {} - babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.4): + babel-loader@10.1.0(@babel/core@7.29.0)(webpack@5.105.4): dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 + optionalDependencies: webpack: 5.105.4(webpack-cli@6.0.1) babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0): From 60f6efca5b5169ad89eda9993c12687715413d69 Mon Sep 17 00:00:00 2001 From: Aritra Basu <24430013+aritra24@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:28:28 +0530 Subject: [PATCH 061/595] Updates exception to hide sql statements on constraint failure (#63028) * Updates exception to hide sql statements on constraint failure The exception handler now hides the sql statement when the expose stacktrace flag is false. * Fixes failing UT * Adds ignore type on pop from dict Since pytest returns a starlette HTTPException it annotates details as a string, we make use of FastApi's HTTPException which returns details as a dict. Due to this mypy get's confused about some of the operations we're performing on a dict. * Cleaned up UTs to remove generate_test_cases_parametrize --- .../airflow/api_fastapi/common/exceptions.py | 10 +- .../api_fastapi/common/test_exceptions.py | 119 ++++++++++++++++-- 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/common/exceptions.py b/airflow-core/src/airflow/api_fastapi/common/exceptions.py index 2495a64dd288c..12d2486253c11 100644 --- a/airflow-core/src/airflow/api_fastapi/common/exceptions.py +++ b/airflow-core/src/airflow/api_fastapi/common/exceptions.py @@ -74,22 +74,26 @@ def exception_handler(self, request: Request, exc: IntegrityError): for tb in traceback.format_tb(exc.__traceback__): stacktrace += tb - log_message = f"Error with id {exception_id}\n{stacktrace}" + log_message = f"Error with id {exception_id}, statement: {exc.statement}\n{stacktrace}" log.error(log_message) if conf.get("api", "expose_stacktrace") == "True": message = log_message + statement = str(exc.statement) + orig_error = str(exc.orig) else: message = ( "Serious error when handling your request. Check logs for more details - " f"you will find it in api server when you look for ID {exception_id}" ) + statement = "hidden" + orig_error = "hidden" raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ "reason": "Unique constraint violation", - "statement": str(exc.statement), - "orig_error": str(exc.orig), + "statement": statement, + "orig_error": orig_error, "message": message, }, ) diff --git a/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py b/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py index d21e5a3b01b81..c3958f0297a36 100644 --- a/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py +++ b/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py @@ -119,6 +119,67 @@ def teardown_method(self) -> None: clear_db_runs() clear_db_dags() + @pytest.mark.parametrize( + ("table", "expected_exception"), + [ + [ + "Pool", + HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "reason": "Unique constraint violation", + "statement": "hidden", + "orig_error": "hidden", + "message": MESSAGE, + }, + ), + ], + [ + "Variable", + HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "reason": "Unique constraint violation", + "statement": "hidden", + "orig_error": "hidden", + "message": MESSAGE, + }, + ), + ], + ], + ) + @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) + @conf_vars({("api", "expose_stacktrace"): "False"}) + @provide_session + def test_handle_single_column_unique_constraint_error_without_stacktrace( + self, + mock_get_random_string, + session, + table, + expected_exception, + ) -> None: + # Take Pool and Variable tables as test cases + # Note: SQLA2 uses a more optimized bulk insert strategy when multiple objects are added to the + # session. Instead of individual INSERT statements, a single INSERT with the SELECT FROM VALUES + # pattern is used. + if table == "Pool": + session.add(Pool(pool=TEST_POOL, slots=1, description="test pool", include_deferred=False)) + session.flush() # Avoid SQLA2.0 bulk insert optimization + session.add(Pool(pool=TEST_POOL, slots=1, description="test pool", include_deferred=False)) + elif table == "Variable": + session.add(Variable(key=TEST_VARIABLE_KEY, val="test_val")) + session.flush() + session.add(Variable(key=TEST_VARIABLE_KEY, val="test_val")) + + with pytest.raises(IntegrityError) as exeinfo_integrity_error: + session.commit() + + with pytest.raises(HTTPException) as exeinfo_response_error: + self.unique_constraint_error_handler.exception_handler(None, exeinfo_integrity_error.value) # type: ignore + + assert exeinfo_response_error.value.status_code == expected_exception.status_code + assert exeinfo_response_error.value.detail == expected_exception.detail + @pytest.mark.parametrize( ("table", "expected_exception"), generate_test_cases_parametrize( @@ -131,7 +192,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO slot_pool (pool, slots, description, include_deferred, team_name) VALUES (?, ?, ?, ?, ?)", "orig_error": "UNIQUE constraint failed: slot_pool.pool", - "message": MESSAGE, }, ), HTTPException( @@ -140,7 +200,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO slot_pool (pool, slots, description, include_deferred, team_name) VALUES (%s, %s, %s, %s, %s)", "orig_error": "(1062, \"Duplicate entry 'test_pool' for key 'slot_pool.slot_pool_pool_uq'\")", - "message": MESSAGE, }, ), HTTPException( @@ -149,7 +208,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO slot_pool (pool, slots, description, include_deferred, team_name) VALUES (%(pool)s, %(slots)s, %(description)s, %(include_deferred)s, %(team_name)s) RETURNING slot_pool.id", "orig_error": 'duplicate key value violates unique constraint "slot_pool_pool_uq"\nDETAIL: Key (pool)=(test_pool) already exists.\n', - "message": MESSAGE, }, ), ], @@ -160,7 +218,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": 'INSERT INTO variable ("key", val, description, is_encrypted, team_name) VALUES (?, ?, ?, ?, ?)', "orig_error": "UNIQUE constraint failed: variable.key", - "message": MESSAGE, }, ), HTTPException( @@ -169,7 +226,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO variable (`key`, val, description, is_encrypted, team_name) VALUES (%s, %s, %s, %s, %s)", "orig_error": "(1062, \"Duplicate entry 'test_key' for key 'variable.variable_key_uq'\")", - "message": MESSAGE, }, ), HTTPException( @@ -178,7 +234,6 @@ def teardown_method(self) -> None: "reason": "Unique constraint violation", "statement": "INSERT INTO variable (key, val, description, is_encrypted, team_name) VALUES (%(key)s, %(val)s, %(description)s, %(is_encrypted)s, %(team_name)s) RETURNING variable.id", "orig_error": 'duplicate key value violates unique constraint "variable_key_uq"\nDETAIL: Key (key)=(test_key) already exists.\n', - "message": MESSAGE, }, ), ], @@ -186,9 +241,9 @@ def teardown_method(self) -> None: ), ) @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) - @conf_vars({("api", "expose_stacktrace"): "False"}) + @conf_vars({("api", "expose_stacktrace"): "True"}) @provide_session - def test_handle_single_column_unique_constraint_error( + def test_handle_single_column_unique_constraint_error_with_stacktrace( self, mock_get_random_string, session, @@ -214,7 +269,46 @@ def test_handle_single_column_unique_constraint_error( with pytest.raises(HTTPException) as exeinfo_response_error: self.unique_constraint_error_handler.exception_handler(None, exeinfo_integrity_error.value) # type: ignore + exeinfo_response_error.value.detail.pop("message", None) # type: ignore[attr-defined] + assert exeinfo_response_error.value.status_code == expected_exception.status_code + assert exeinfo_response_error.value.detail == expected_exception.detail + + @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) + @conf_vars({("api", "expose_stacktrace"): "False"}) + @provide_session + def test_handle_multiple_columns_unique_constraint_error_without_stacktrace( + self, + mock_get_random_string, + session, + ) -> None: + expected_exception = HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "reason": "Unique constraint violation", + "statement": "hidden", + "orig_error": "hidden", + "message": MESSAGE, + }, + ) + session.add( + DagRun(dag_id="test_dag_id", run_id="test_run_id", run_type="manual", state=DagRunState.RUNNING) + ) + session.add( + DagRun(dag_id="test_dag_id", run_id="test_run_id", run_type="manual", state=DagRunState.RUNNING) + ) + with pytest.raises(IntegrityError) as exeinfo_integrity_error: + session.commit() + + with pytest.raises(HTTPException) as exeinfo_response_error: + self.unique_constraint_error_handler.exception_handler(None, exeinfo_integrity_error.value) # type: ignore + assert exeinfo_response_error.value.status_code == expected_exception.status_code + # The SQL statement is an implementation detail, so we match on the statement pattern (contains + # the table name and is an INSERT) instead of insisting on an exact match. + response_detail = exeinfo_response_error.value.detail + expected_detail = expected_exception.detail + + assert response_detail == expected_detail assert exeinfo_response_error.value.detail == expected_exception.detail @pytest.mark.parametrize( @@ -229,7 +323,6 @@ def test_handle_single_column_unique_constraint_error( "reason": "Unique constraint violation", "statement": "INSERT INTO dag_run (dag_id, queued_at, logical_date, start_date, end_date, state, run_id, creating_job_id, run_type, triggered_by, triggering_user_name, conf, data_interval_start, data_interval_end, run_after, last_scheduling_decision, log_template_id, updated_at, clear_number, backfill_id, bundle_version, scheduled_by_job_id, context_carrier, created_dag_version_id, partition_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT max(log_template.id) AS max_1 \nFROM log_template), ?, ?, ?, ?, ?, ?, ?, ?)", "orig_error": "UNIQUE constraint failed: dag_run.dag_id, dag_run.run_id", - "message": MESSAGE, }, ), HTTPException( @@ -238,7 +331,6 @@ def test_handle_single_column_unique_constraint_error( "reason": "Unique constraint violation", "statement": "INSERT INTO dag_run (dag_id, queued_at, logical_date, start_date, end_date, state, run_id, creating_job_id, run_type, triggered_by, triggering_user_name, conf, data_interval_start, data_interval_end, run_after, last_scheduling_decision, log_template_id, updated_at, clear_number, backfill_id, bundle_version, scheduled_by_job_id, context_carrier, created_dag_version_id, partition_key) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, (SELECT max(log_template.id) AS max_1 \nFROM log_template), %s, %s, %s, %s, %s, %s, %s, %s)", "orig_error": "(1062, \"Duplicate entry 'test_dag_id-test_run_id' for key 'dag_run.dag_run_dag_id_run_id_key'\")", - "message": MESSAGE, }, ), HTTPException( @@ -247,7 +339,6 @@ def test_handle_single_column_unique_constraint_error( "reason": "Unique constraint violation", "statement": "INSERT INTO dag_run (dag_id, queued_at, logical_date, start_date, end_date, state, run_id, creating_job_id, run_type, triggered_by, triggering_user_name, conf, data_interval_start, data_interval_end, run_after, last_scheduling_decision, log_template_id, updated_at, clear_number, backfill_id, bundle_version, scheduled_by_job_id, context_carrier, created_dag_version_id, partition_key) VALUES (%(dag_id)s, %(queued_at)s, %(logical_date)s, %(start_date)s, %(end_date)s, %(state)s, %(run_id)s, %(creating_job_id)s, %(run_type)s, %(triggered_by)s, %(triggering_user_name)s, %(conf)s, %(data_interval_start)s, %(data_interval_end)s, %(run_after)s, %(last_scheduling_decision)s, (SELECT max(log_template.id) AS max_1 \nFROM log_template), %(updated_at)s, %(clear_number)s, %(backfill_id)s, %(bundle_version)s, %(scheduled_by_job_id)s, %(context_carrier)s, %(created_dag_version_id)s, %(partition_key)s) RETURNING dag_run.id", "orig_error": 'duplicate key value violates unique constraint "dag_run_dag_id_run_id_key"\nDETAIL: Key (dag_id, run_id)=(test_dag_id, test_run_id) already exists.\n', - "message": MESSAGE, }, ), ], @@ -255,9 +346,9 @@ def test_handle_single_column_unique_constraint_error( ), ) @patch("airflow.api_fastapi.common.exceptions.get_random_string", return_value=MOCKED_ID) - @conf_vars({("api", "expose_stacktrace"): "False"}) + @conf_vars({("api", "expose_stacktrace"): "True"}) @provide_session - def test_handle_multiple_columns_unique_constraint_error( + def test_handle_multiple_columns_unique_constraint_error_with_stacktrace( self, mock_get_random_string, session, @@ -290,6 +381,8 @@ def test_handle_multiple_columns_unique_constraint_error( actual_statement = response_detail.pop("statement", None) # type: ignore[attr-defined] expected_detail.pop("statement", None) + # Removes the stacktrace from response to remove during comparison. + response_detail.pop("message", None) # type: ignore[attr-defined] assert response_detail == expected_detail assert "INSERT INTO dag_run" in actual_statement assert exeinfo_response_error.value.detail == expected_exception.detail From 0decb3cbd1ecce2a6e55d6148f517f4cde6cefdd Mon Sep 17 00:00:00 2001 From: Daniel Standish <15932138+dstandish@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:12:42 -0700 Subject: [PATCH 062/595] Simplify approach for creating dag run and task spans (#62554) Previously we had a very high touch way of creating long-running spans for dag runs and tasks and keeping them alive across scheduler handoffs etc. We figured out a simpler way to do that. Additionally, we determined that there was really no need to use the custom airflow tracing interfaces and resolved instead to do OTEL in a way where we can just use the OTEL interfaces directly. We pushed the task span creation down to the tasks, so that task spans run in the task process. Replace the custom base tracer class and scattered OTEL logic with a focused helper module in `airflow/observability/traces/`. Remove the large blocks of tracing code from `scheduler_job_runner.py` and `dagrun.py` and consolidate span creation for dag runs and task execution into clean, reusable functions. Add configurable flush timeout and improve span naming. --- .../src/airflow/config_templates/config.yml | 12 + .../src/airflow/executors/base_executor.py | 44 --- .../src/airflow/executors/workloads/task.py | 2 +- .../src/airflow/jobs/scheduler_job_runner.py | 262 ---------------- .../src/airflow/jobs/triggerer_job_runner.py | 10 - airflow-core/src/airflow/models/dagrun.py | 174 ++-------- .../airflow/observability/traces/__init__.py | 134 ++++++++ airflow-core/src/airflow/settings.py | 4 +- .../integration/otel/dags/otel_test_dag.py | 57 +--- .../otel_test_dag_with_pause_between_tasks.py | 158 ---------- .../dags/otel_test_dag_with_pause_in_task.py | 151 --------- .../tests/integration/otel/test_otel.py | 20 +- .../tests/unit/jobs/test_scheduler_job.py | 187 ----------- airflow-core/tests/unit/models/test_dagrun.py | 296 ++++++++++-------- docs/spelling_wordlist.txt | 1 + .../ci/docker-compose/integration-otel.yml | 2 +- task-sdk/src/airflow/sdk/definitions/dag.py | 4 - .../airflow/sdk/execution_time/task_runner.py | 119 ++++--- .../execution_time/test_task_runner.py | 117 ++++++- 19 files changed, 549 insertions(+), 1205 deletions(-) delete mode 100644 airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py delete mode 100644 airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index 8558a71236eb7..2d70e625c1544 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1414,9 +1414,21 @@ traces: description: | If True, then traces from Airflow internal methods are exported. Defaults to False. version_added: 3.1.0 + version_deprecated: 3.2.0 + deprecation_reason: | + This parameter is no longer used. type: string example: ~ default: "False" + task_runner_flush_timeout_milliseconds: + description: | + Timeout in milliseconds to wait for the OpenTelemetry span exporter to flush pending spans + when a task runner process exits. If the exporter does not finish within this time, any + buffered spans may be dropped. + version_added: 3.2.0 + type: integer + example: ~ + default: "30000" secrets: description: ~ options: diff --git a/airflow-core/src/airflow/executors/base_executor.py b/airflow-core/src/airflow/executors/base_executor.py index 2997d55d8bb3b..d67c25c7bafaa 100644 --- a/airflow-core/src/airflow/executors/base_executor.py +++ b/airflow-core/src/airflow/executors/base_executor.py @@ -32,14 +32,11 @@ from airflow.configuration import conf from airflow.executors import workloads from airflow.executors.executor_loader import ExecutorLoader -from airflow.executors.workloads.task import TaskInstanceDTO from airflow.models import Log from airflow.models.callback import CallbackKey from airflow.observability.metrics import stats_utils -from airflow.observability.trace import Trace from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.state import TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict PARALLELISM: int = conf.getint("core", "PARALLELISM") @@ -143,8 +140,6 @@ class BaseExecutor(LoggingMixin): :param parallelism: how many jobs should run at one time. """ - active_spans = ThreadSafeDict() - supports_ad_hoc_ti_run: bool = False supports_callbacks: bool = False supports_multi_team: bool = False @@ -217,10 +212,6 @@ def __repr__(self): _repr += ")" return _repr - @classmethod - def set_active_spans(cls, active_spans: ThreadSafeDict): - cls.active_spans = active_spans - def start(self): # pragma: no cover """Executors may need to get things started.""" @@ -340,17 +331,6 @@ def _emit_metrics(self, open_slots, num_running_tasks, num_queued_tasks): queued_tasks_metric_name = self._get_metric_name("executor.queued_tasks") running_tasks_metric_name = self._get_metric_name("executor.running_tasks") - span = Trace.get_current_span() - if span.is_recording(): - span.add_event( - name="executor", - attributes={ - open_slots_metric_name: open_slots, - queued_tasks_metric_name: num_queued_tasks, - running_tasks_metric_name: num_running_tasks, - }, - ) - self.log.debug("%s running task instances for executor %s", num_running_tasks, name) self.log.debug("%s in queue for executor %s", num_queued_tasks, name) if open_slots == 0: @@ -415,30 +395,6 @@ def trigger_tasks(self, open_slots: int) -> None: if key in self.attempts: del self.attempts[key] - if isinstance(workload, workloads.ExecuteTask) and hasattr(workload, "ti"): - ti = workload.ti - - # If it's None, then the span for the current id hasn't been started. - if self.active_spans is not None and self.active_spans.get("ti:" + str(ti.id)) is None: - if isinstance(ti, TaskInstanceDTO): - parent_context = Trace.extract(ti.parent_context_carrier) - else: - parent_context = Trace.extract(ti.dag_run.context_carrier) - # Start a new span using the context from the parent. - # Attributes will be set once the task has finished so that all - # values will be available (end_time, duration, etc.). - - span = Trace.start_child_span( - span_name=f"{ti.task_id}", - parent_context=parent_context, - component="task", - start_as_current=False, - ) - self.active_spans.set("ti:" + str(ti.id), span) - # Inject the current context into the carrier. - carrier = Trace.inject() - ti.context_carrier = carrier - workload_list.append(workload) if workload_list: diff --git a/airflow-core/src/airflow/executors/workloads/task.py b/airflow-core/src/airflow/executors/workloads/task.py index d691dcb6f0968..a5939cf424412 100644 --- a/airflow-core/src/airflow/executors/workloads/task.py +++ b/airflow-core/src/airflow/executors/workloads/task.py @@ -86,7 +86,7 @@ def make( from airflow.utils.helpers import log_filename_template_renderer ser_ti = TaskInstanceDTO.model_validate(ti, from_attributes=True) - ser_ti.parent_context_carrier = ti.dag_run.context_carrier + ser_ti.context_carrier = ti.dag_run.context_carrier if not bundle_info: bundle_info = BundleInfo( name=ti.dag_model.bundle_name, diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index 2d58f295c6ecc..c667631756edc 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -32,7 +32,6 @@ from functools import lru_cache, partial from itertools import groupby from typing import TYPE_CHECKING, Any -from uuid import UUID from sqlalchemy import ( and_, @@ -98,17 +97,14 @@ from airflow.models.team import Team from airflow.models.trigger import TRIGGER_FAIL_REPR, Trigger, TriggerFailureReason from airflow.observability.metrics import stats_utils -from airflow.observability.trace import Trace from airflow.serialization.definitions.assets import SerializedAssetUniqueKey from airflow.serialization.definitions.notset import NOTSET from airflow.ti_deps.dependencies_states import EXECUTION_STATES from airflow.timetables.simple import AssetTriggeredTimetable -from airflow.utils.dates import datetime_to_nano from airflow.utils.event_scheduler import EventScheduler from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.retries import MAX_DB_RETRIES, retry_db_transaction, run_with_db_retries from airflow.utils.session import NEW_SESSION, create_session, provide_session -from airflow.utils.span_status import SpanStatus from airflow.utils.sqlalchemy import ( get_dialect_name, is_lock_not_available_error, @@ -116,7 +112,6 @@ with_row_locks, ) from airflow.utils.state import CallbackState, DagRunState, State, TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType if TYPE_CHECKING: @@ -273,14 +268,6 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin): job_type = "SchedulerJob" - # For a dagrun span - # - key: dag_run.run_id | value: span - # - dagrun keys will be prefixed with 'dr:'. - # For a ti span - # - key: ti.id | value: span - # - taskinstance keys will be prefixed with 'ti:'. - active_spans = ThreadSafeDict() - def __init__( self, job: Job, @@ -434,9 +421,6 @@ def _get_workload_team_name(self, workload: SchedulerWorkload, session: Session) def _exit_gracefully(self, signum: int, frame: FrameType | None) -> None: """Clean up processor_agent to avoid leaving orphan processes.""" - if self._is_tracing_enabled(): - self._end_active_spans() - if not _is_parent_process(): # Only the parent process should perform the cleanup. return @@ -1311,18 +1295,6 @@ def process_executor_events( ti.pid, ) - if (active_ti_span := cls.active_spans.get("ti:" + str(ti.id))) is not None: - cls.set_ti_span_attrs(span=active_ti_span, state=state, ti=ti) - # End the span and remove it from the active_spans dict. - active_ti_span.end(end_time=datetime_to_nano(ti.end_date)) - cls.active_spans.delete("ti:" + str(ti.id)) - ti.span_status = SpanStatus.ENDED - else: - if ti.span_status == SpanStatus.ACTIVE: - # Another scheduler has started the span. - # Update the SpanStatus to let the process know that it must end it. - ti.span_status = SpanStatus.SHOULD_END - # There are two scenarios why the same TI with the same try_number is queued # after executor is finished with it: # 1) the TI was killed externally and it had no time to mark itself failed @@ -1459,39 +1431,6 @@ def process_executor_events( return len(event_buffer) - @classmethod - def set_ti_span_attrs(cls, span, state, ti): - span.set_attributes( - { - "airflow.category": "scheduler", - "airflow.task.id": ti.id, - "airflow.task.task_id": ti.task_id, - "airflow.task.dag_id": ti.dag_id, - "airflow.task.state": ti.state, - "airflow.task.error": state == TaskInstanceState.FAILED, - "airflow.task.start_date": str(ti.start_date), - "airflow.task.end_date": str(ti.end_date), - "airflow.task.duration": ti.duration, - "airflow.task.executor_config": str(ti.executor_config), - "airflow.task.logical_date": str(ti.logical_date), - "airflow.task.hostname": ti.hostname, - "airflow.task.log_url": ti.log_url, - "airflow.task.operator": str(ti.operator), - "airflow.task.try_number": ti.try_number, - "airflow.task.executor_state": state, - "airflow.task.pool": ti.pool, - "airflow.task.queue": ti.queue, - "airflow.task.priority_weight": ti.priority_weight, - "airflow.task.queued_dttm": str(ti.queued_dttm), - "airflow.task.queued_by_job_id": ti.queued_by_job_id, - "airflow.task.pid": ti.pid, - } - ) - if span.is_recording(): - span.add_event(name="airflow.task.queued", timestamp=datetime_to_nano(ti.queued_dttm)) - span.add_event(name="airflow.task.started", timestamp=datetime_to_nano(ti.start_date)) - span.add_event(name="airflow.task.ended", timestamp=datetime_to_nano(ti.end_date)) - def _execute(self) -> int | None: import os @@ -1515,12 +1454,6 @@ def _execute(self) -> int | None: executor.start() # local import due to type_checking. - from airflow.executors.base_executor import BaseExecutor - - # Pass a reference to the dictionary. - # Any changes made by a dag_run instance, will be reflected to the dictionary of this class. - DagRun.set_active_spans(active_spans=self.active_spans) - BaseExecutor.set_active_spans(active_spans=self.active_spans) stats_factory = stats_utils.get_stats_factory(Stats) Stats.initialize(factory=stats_factory) @@ -1571,162 +1504,6 @@ def _update_dag_run_state_for_paused_dags(self, session: Session = NEW_SESSION) except Exception as e: # should not fail the scheduler self.log.exception("Failed to update dag run state for paused dags due to %s", e) - @provide_session - def _end_active_spans(self, session: Session = NEW_SESSION): - # No need to do a commit for every update. The annotation will commit all of them once at the end. - for prefixed_key, span in self.active_spans.get_all().items(): - # Use partition to split on the first occurrence of ':'. - prefix, sep, key = prefixed_key.partition(":") - - if prefix == "ti": - ti_result = session.get(TaskInstance, UUID(key)) - if ti_result is None: - continue - ti: TaskInstance = ti_result - - if ti.state in State.finished: - self.set_ti_span_attrs(span=span, state=ti.state, ti=ti) - span.end(end_time=datetime_to_nano(ti.end_date)) - ti.span_status = SpanStatus.ENDED - else: - span.end() - ti.span_status = SpanStatus.NEEDS_CONTINUANCE - elif prefix == "dr": - dag_run: DagRun | None = session.scalars( - select(DagRun).where(DagRun.id == int(key)) - ).one_or_none() - if dag_run is None: - continue - if dag_run.state in State.finished_dr_states: - dag_run.set_dagrun_span_attrs(span=span) - - span.end(end_time=datetime_to_nano(dag_run.end_date)) - dag_run.span_status = SpanStatus.ENDED - else: - span.end() - dag_run.span_status = SpanStatus.NEEDS_CONTINUANCE - initial_dag_run_context = Trace.extract(dag_run.context_carrier) - with Trace.start_child_span( - span_name="current_scheduler_exited", parent_context=initial_dag_run_context - ) as s: - s.set_attribute("trace_status", "needs continuance") - else: - self.log.error("Found key with unknown prefix: '%s'", prefixed_key) - - # Even if there is a key with an unknown prefix, clear the dict. - # If this method has been called, the scheduler is exiting. - self.active_spans.clear() - - def _end_spans_of_externally_ended_ops(self, session: Session): - # The scheduler that starts a dag_run or a task is also the one that starts the spans. - # Each scheduler should end the spans that it has started. - # - # Otel spans are implemented in a certain way so that the objects - # can't be shared between processes or get recreated. - # It is done so that the process that starts a span, is also the one that ends it. - # - # If another scheduler has finished processing a dag_run or a task and there is a reference - # on the active_spans dictionary, then the current scheduler started the span, - # and therefore must end it. - dag_runs_should_end: list[DagRun] = list( - session.scalars(select(DagRun).where(DagRun.span_status == SpanStatus.SHOULD_END)) - ) - tis_should_end: list[TaskInstance] = list( - session.scalars(select(TaskInstance).where(TaskInstance.span_status == SpanStatus.SHOULD_END)) - ) - - for dag_run in dag_runs_should_end: - active_dagrun_span = self.active_spans.get("dr:" + str(dag_run.id)) - if active_dagrun_span is not None: - if dag_run.state in State.finished_dr_states: - dag_run.set_dagrun_span_attrs(span=active_dagrun_span) - - active_dagrun_span.end(end_time=datetime_to_nano(dag_run.end_date)) - else: - active_dagrun_span.end() - self.active_spans.delete("dr:" + str(dag_run.id)) - dag_run.span_status = SpanStatus.ENDED - - for ti in tis_should_end: - active_ti_span = self.active_spans.get(f"ti:{ti.id}") - if active_ti_span is not None: - if ti.state in State.finished: - self.set_ti_span_attrs(span=active_ti_span, state=ti.state, ti=ti) - active_ti_span.end(end_time=datetime_to_nano(ti.end_date)) - else: - active_ti_span.end() - self.active_spans.delete(f"ti:{ti.id}") - ti.span_status = SpanStatus.ENDED - - def _recreate_unhealthy_scheduler_spans_if_needed(self, dag_run: DagRun, session: Session): - # There are two scenarios: - # 1. scheduler is unhealthy but managed to update span_status - # 2. scheduler is unhealthy and didn't manage to make any updates - # Check the span_status first, in case the 2nd db query can be avoided (scenario 1). - - # If the dag_run is scheduled by a different scheduler, and it's still running and the span is active, - # then check the Job table to determine if the initial scheduler is still healthy. - if ( - dag_run.scheduled_by_job_id != self.job.id - and dag_run.state in State.unfinished_dr_states - and dag_run.span_status == SpanStatus.ACTIVE - ): - initial_scheduler_id = dag_run.scheduled_by_job_id - job: Job | None = session.scalars( - select(Job).where( - Job.id == initial_scheduler_id, - Job.job_type == "SchedulerJob", - ) - ).one_or_none() - if job is None: - return - - if not job.is_alive(): - # Start a new span for the dag_run. - dr_span = Trace.start_root_span( - span_name=f"{dag_run.dag_id}_recreated", - component="dag", - start_time=dag_run.queued_at, - start_as_current=False, - ) - carrier = Trace.inject() - # Update the context_carrier and leave the SpanStatus as ACTIVE. - dag_run.context_carrier = carrier - self.active_spans.set("dr:" + str(dag_run.id), dr_span) - - tis = dag_run.get_task_instances(session=session) - - # At this point, any tis will have been adopted by the current scheduler, - # and ti.queued_by_job_id will point to the current id. - # Any tis that have been executed by the unhealthy scheduler, will need a new span - # so that it can be associated with the new dag_run span. - tis_needing_spans = [ - ti - for ti in tis - # If it has started and there is a reference on the active_spans dict, - # then it was started by the current scheduler. - if ti.start_date is not None and self.active_spans.get(f"ti:{ti.id}") is None - ] - - dr_context = Trace.extract(dag_run.context_carrier) - for ti in tis_needing_spans: - ti_span = Trace.start_child_span( - span_name=f"{ti.task_id}_recreated", - parent_context=dr_context, - start_time=ti.queued_dttm, - start_as_current=False, - ) - ti_carrier = Trace.inject() - ti.context_carrier = ti_carrier - - if ti.state in State.finished: - self.set_ti_span_attrs(span=ti_span, state=ti.state, ti=ti) - ti_span.end(end_time=datetime_to_nano(ti.end_date)) - ti.span_status = SpanStatus.ENDED - else: - ti.span_status = SpanStatus.ACTIVE - self.active_spans.set(f"ti:{ti.id}", ti_span) - def _run_scheduler_loop(self) -> None: """ Harvest DAG parsing results, queue tasks, and perform executor heartbeat; the actual scheduler loop. @@ -1819,9 +1596,6 @@ def _run_scheduler_loop(self) -> None: for loop_count in itertools.count(start=1): with Stats.timer("scheduler.scheduler_loop_duration") as timer: with create_session() as session: - if self._is_tracing_enabled(): - self._end_spans_of_externally_ended_ops(session) - # This will schedule for as many executors as possible. num_queued_tis = self._do_scheduling(session) # Don't keep any objects alive -- we've possibly just looked at 500+ ORM objects! @@ -2357,16 +2131,6 @@ def _start_queued_dagruns(self, session: Session) -> None: active_runs_of_dags = Counter({(dag_id, br_id): num for dag_id, br_id, num in session.execute(query)}) def _update_state(dag: SerializedDAG, dag_run: DagRun): - span = Trace.get_current_span() - span.set_attributes( - { - "state": str(DagRunState.RUNNING), - "run_id": dag_run.run_id, - "type": dag_run.run_type, - "dag_id": dag_run.dag_id, - } - ) - dag_run.state = DagRunState.RUNNING dag_run.start_date = timezone.utcnow() if ( @@ -2383,18 +2147,12 @@ def _update_state(dag: SerializedDAG, dag_run: DagRun): tags={}, extra_tags={"dag_id": dag.dag_id}, ) - if span.is_recording(): - span.add_event( - name="schedule_delay", - attributes={"dag_id": dag.dag_id, "schedule_delay": str(schedule_delay)}, - ) # cache saves time during scheduling of many dag_runs for same dag cached_get_dag: Callable[[DagRun], SerializedDAG | None] = lru_cache()( partial(self.scheduler_dag_bag.get_dag_for_run, session=session) ) - span = Trace.get_current_span() for dag_run in dag_runs: dag_id = dag_run.dag_id run_id = dag_run.run_id @@ -2434,15 +2192,6 @@ def _update_state(dag: SerializedDAG, dag_run: DagRun): dag_run.run_id, ) continue - if span.is_recording(): - span.add_event( - name="dag_run", - attributes={ - "run_id": dag_run.run_id, - "dag_id": dag_run.dag_id, - "conf": str(dag_run.conf), - }, - ) active_runs_of_dags[(dag_run.dag_id, backfill_id)] += 1 _update_state(dag, dag_run) dag_run.notify_dagrun_state_changed(msg="started") @@ -2554,17 +2303,6 @@ def _schedule_dag_run( self.log.warning("The DAG disappeared before verifying integrity: %s. Skipping.", dag_run.dag_id) return callback - if ( - self._is_tracing_enabled() - and dag_run.scheduled_by_job_id is not None - and dag_run.scheduled_by_job_id != self.job.id - and self.active_spans.get("dr:" + str(dag_run.id)) is None - ): - # If the dag_run has been previously scheduled by another job and there is no active span, - # then check if the job is still healthy. - # If it's not healthy, then recreate the spans. - self._recreate_unhealthy_scheduler_spans_if_needed(dag_run, session) - dag_run.scheduled_by_job_id = self.job.id # TODO[HA]: Rename update_state -> schedule_dag_run, ?? something else? diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py b/airflow-core/src/airflow/jobs/triggerer_job_runner.py index ca11647811099..1406283c05cb3 100644 --- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py +++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py @@ -50,7 +50,6 @@ from airflow.jobs.job import perform_heartbeat from airflow.models.trigger import Trigger from airflow.observability.metrics import stats_utils -from airflow.observability.trace import Trace from airflow.sdk.api.datamodels._generated import HITLDetailResponse from airflow.sdk.execution_time.comms import ( CommsDecoder, @@ -627,15 +626,6 @@ def emit_metrics(self): extra_tags={"hostname": self.job.hostname}, ) - span = Trace.get_current_span() - span.set_attributes( - { - "trigger host": self.job.hostname, - "triggers running": len(self.running_triggers), - "capacity left": capacity_left, - } - ) - def update_triggers(self, requested_trigger_ids: set[int]): """ Request that we update what triggers we're running. diff --git a/airflow-core/src/airflow/models/dagrun.py b/airflow-core/src/airflow/models/dagrun.py index 4bc47dddea7ac..61242e45390d6 100644 --- a/airflow-core/src/airflow/models/dagrun.py +++ b/airflow-core/src/airflow/models/dagrun.py @@ -28,6 +28,9 @@ from uuid import UUID import structlog +from opentelemetry import context, trace +from opentelemetry.trace import StatusCode +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from sqlalchemy import ( JSON, Enum, @@ -72,12 +75,11 @@ from airflow.models.taskinstancehistory import TaskInstanceHistory as TIH from airflow.models.tasklog import LogTemplate from airflow.models.taskmap import TaskMap -from airflow.observability.trace import Trace +from airflow.observability.traces import new_dagrun_trace_carrier, override_ids from airflow.serialization.definitions.deadline import SerializedReferenceModels from airflow.serialization.definitions.notset import NOTSET, ArgNotSet, is_arg_set from airflow.ti_deps.dep_context import DepContext from airflow.ti_deps.dependencies_states import SCHEDULEABLE_STATES -from airflow.utils.dates import datetime_to_nano from airflow.utils.helpers import chunks, is_container, prune_dict from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.retries import retry_db_transaction @@ -92,19 +94,16 @@ ) from airflow.utils.state import DagRunState, State, TaskInstanceState from airflow.utils.strings import get_random_string -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType if TYPE_CHECKING: from typing import Literal, TypeAlias - from opentelemetry.sdk.trace import Span from pydantic import NonNegativeInt from sqlalchemy.engine import ScalarResult from sqlalchemy.orm import Session from sqlalchemy.sql.elements import Case, ColumnElement - from airflow._shared.observability.traces.base_tracer import EmptySpan from airflow.models.dag_version import DagVersion from airflow.models.taskinstancekey import TaskInstanceKey from airflow.sdk import DAG as SDKDAG @@ -120,6 +119,8 @@ log = structlog.get_logger(__name__) +tracer = trace.get_tracer(__name__) + class TISchedulingDecision(NamedTuple): """Type of return for DagRun.task_instance_scheduling_decisions.""" @@ -153,8 +154,6 @@ class DagRun(Base, LoggingMixin): external trigger (i.e. manual runs). """ - active_spans = ThreadSafeDict() - __tablename__ = "dag_run" id: Mapped[int] = mapped_column(Integer, primary_key=True) @@ -368,7 +367,8 @@ def __init__( self.triggered_by = triggered_by self.triggering_user_name = triggering_user_name self.scheduled_by_job_id = None - self.context_carrier = {} + self.context_carrier: dict[str, str] = new_dagrun_trace_carrier() + if not isinstance(partition_key, str | None): raise ValueError( f"Expected partition_key to be a `str` or `None` but got `{partition_key.__class__.__name__}`" @@ -461,10 +461,6 @@ def check_version_id_exists_in_dr(self, dag_version_id: UUID, session: Session = def stats_tags(self) -> dict[str, str]: return prune_dict({"dag_id": self.dag_id, "run_type": self.run_type}) - @classmethod - def set_active_spans(cls, active_spans: ThreadSafeDict): - cls.active_spans = active_spans - def get_state(self): return self._state @@ -1019,131 +1015,28 @@ def is_effective_leaf(task): leaf_tis = {ti for ti in tis if ti.task_id in leaf_task_ids if ti.state != TaskInstanceState.REMOVED} return leaf_tis - def set_dagrun_span_attrs(self, span: Span | EmptySpan): - if self._state == DagRunState.FAILED: - span.set_attribute("airflow.dag_run.error", True) - - # Explicitly set the value type to Union[...] to avoid a mypy error. - attributes: dict[str, AttributeValueType] = { - "airflow.category": "DAG runs", - "airflow.dag_run.dag_id": str(self.dag_id), - "airflow.dag_run.logical_date": str(self.logical_date), - "airflow.dag_run.run_id": str(self.run_id), - "airflow.dag_run.queued_at": str(self.queued_at), - "airflow.dag_run.run_start_date": str(self.start_date), - "airflow.dag_run.run_end_date": str(self.end_date), - "airflow.dag_run.run_duration": str( - (self.end_date - self.start_date).total_seconds() if self.start_date and self.end_date else 0 - ), - "airflow.dag_run.state": str(self._state), - "airflow.dag_run.run_type": str(self.run_type), - "airflow.dag_run.data_interval_start": str(self.data_interval_start), - "airflow.dag_run.data_interval_end": str(self.data_interval_end), - "airflow.dag_run.conf": str(self.conf), - } - if span.is_recording(): - span.add_event(name="airflow.dag_run.queued", timestamp=datetime_to_nano(self.queued_at)) - span.add_event(name="airflow.dag_run.started", timestamp=datetime_to_nano(self.start_date)) - span.add_event(name="airflow.dag_run.ended", timestamp=datetime_to_nano(self.end_date)) - span.set_attributes(attributes) - - def start_dr_spans_if_needed(self, tis: list[TI]): - # If there is no value in active_spans, then the span hasn't already been started. - if self.active_spans is not None and self.active_spans.get("dr:" + str(self.id)) is None: - if self.span_status == SpanStatus.NOT_STARTED or self.span_status == SpanStatus.NEEDS_CONTINUANCE: - dr_span = None - continue_ti_spans = False - if self.span_status == SpanStatus.NOT_STARTED: - dr_span = Trace.start_root_span( - span_name=f"{self.dag_id}", - component="dag", - start_time=self.queued_at, # This is later converted to nano. - start_as_current=False, - ) - elif self.span_status == SpanStatus.NEEDS_CONTINUANCE: - # Use the existing context_carrier to set the initial dag_run span as the parent. - parent_context = Trace.extract(self.context_carrier) - with Trace.start_child_span( - span_name="new_scheduler", parent_context=parent_context - ) as s: - s.set_attribute("trace_status", "continued") - - dr_span = Trace.start_child_span( - span_name=f"{self.dag_id}_continued", - parent_context=parent_context, - component="dag", - # No start time - start_as_current=False, - ) - # After this span is started, the context_carrier will be replaced by the new one. - # New task span will use this span as the parent. - continue_ti_spans = True - carrier = Trace.inject() - self.context_carrier = carrier - self.span_status = SpanStatus.ACTIVE - # Set the span in a synchronized dictionary, so that the variable can be used to end the span. - self.active_spans.set("dr:" + str(self.id), dr_span) - self.log.debug( - "DagRun span has been started and the injected context_carrier is: %s", - self.context_carrier, - ) - # Start TI spans that also need continuance. - if continue_ti_spans: - new_dagrun_context = Trace.extract(self.context_carrier) - for ti in tis: - if ti.span_status == SpanStatus.NEEDS_CONTINUANCE: - ti_span = Trace.start_child_span( - span_name=f"{ti.task_id}_continued", - parent_context=new_dagrun_context, - start_as_current=False, - ) - ti_carrier = Trace.inject() - ti.context_carrier = ti_carrier - ti.span_status = SpanStatus.ACTIVE - self.active_spans.set(f"ti:{ti.id}", ti_span) - else: - self.log.debug( - "Found span_status '%s', while updating state for dag_run '%s'", - self.span_status, - self.run_id, - ) - - def end_dr_span_if_needed(self): - if self.active_spans is not None: - active_span = self.active_spans.get("dr:" + str(self.id)) - if active_span is not None: - self.log.debug( - "Found active span with span_id: %s, for dag_id: %s, run_id: %s, state: %s", - active_span.get_span_context().span_id, - self.dag_id, - self.run_id, - self.state, - ) - - self.set_dagrun_span_attrs(span=active_span) - active_span.end(end_time=datetime_to_nano(self.end_date)) - # Remove the span from the dict. - self.active_spans.delete("dr:" + str(self.id)) - self.span_status = SpanStatus.ENDED - else: - if self.span_status == SpanStatus.ACTIVE: - # Another scheduler has started the span. - # Update the DB SpanStatus to notify the owner to end it. - self.span_status = SpanStatus.SHOULD_END - elif self.span_status == SpanStatus.NEEDS_CONTINUANCE: - # This is a corner case where the scheduler exited gracefully - # while the dag_run was almost done. - # Since it reached this point, the dag has finished but there has been no time - # to create a new span for the current scheduler. - # There is no need for more spans, update the status on the db. - self.span_status = SpanStatus.ENDED - else: - self.log.debug( - "No active span has been found for dag_id: %s, run_id: %s, state: %s", - self.dag_id, - self.run_id, - self.state, - ) + def _emit_dagrun_span(self, state: DagRunState): + ctx = TraceContextTextMapPropagator().extract(self.context_carrier) + span = trace.get_current_span(context=ctx) + span_context = span.get_span_context() + with override_ids(span_context.trace_id, span_context.span_id): + attributes = { + "airflow.dag_id": str(self.dag_id), + "airflow.dag_run.run_id": self.run_id, + } + if self.logical_date: + attributes["airflow.dag_run.logical_date"] = str(self.logical_date) + if self.partition_key: + attributes["airflow.dag_run.partition_key"] = str(self.partition_key) + span = tracer.start_span( + name=f"dag_run.{self.dag_id}", + start_time=int((self.start_date or timezone.utcnow()).timestamp() * 1e9), + attributes=attributes, + context=context.Context(), + ) + status_code = StatusCode.OK if state == DagRunState.SUCCESS else StatusCode.ERROR + span.set_status(status_code) + span.end() @provide_session def update_state( @@ -1302,9 +1195,6 @@ def recalculate(self) -> _UnfinishedStates: # finally, if the leaves aren't done, the dag is still running else: - # It might need to start TI spans as well. - self.start_dr_spans_if_needed(tis=tis) - self.set_state(DagRunState.RUNNING) if self._state == DagRunState.FAILED or self._state == DagRunState.SUCCESS: @@ -1331,10 +1221,8 @@ def recalculate(self) -> _UnfinishedStates: self.data_interval_start, self.data_interval_end, ) - - self.end_dr_span_if_needed() - session.flush() + self._emit_dagrun_span(state=self.state) self._emit_true_scheduling_delay_stats_for_finished_state(finished_tis) self._emit_duration_stats_for_finished_state() diff --git a/airflow-core/src/airflow/observability/traces/__init__.py b/airflow-core/src/airflow/observability/traces/__init__.py index 217e5db960782..6bf0019f74708 100644 --- a/airflow-core/src/airflow/observability/traces/__init__.py +++ b/airflow-core/src/airflow/observability/traces/__init__.py @@ -15,3 +15,137 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from __future__ import annotations + +import logging +import os +from contextlib import contextmanager +from importlib.metadata import entry_points + +from opentelemetry import context, trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter +from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + +from airflow.configuration import conf + +log = logging.getLogger(__name__) + +OVERRIDE_SPAN_ID_KEY = context.create_key("override_span_id") +OVERRIDE_TRACE_ID_KEY = context.create_key("override_trace_id") + + +class OverrideableRandomIdGenerator(RandomIdGenerator): + """Lets you override the span id.""" + + def generate_span_id(self): + override = context.get_value(OVERRIDE_SPAN_ID_KEY) + if override is not None: + return override + return super().generate_span_id() + + def generate_trace_id(self): + override = context.get_value(OVERRIDE_TRACE_ID_KEY) + if override is not None: + return override + return super().generate_trace_id() + + +def new_dagrun_trace_carrier() -> dict[str, str]: + """Generate a fresh W3C traceparent carrier without creating a recordable span.""" + gen = RandomIdGenerator() + span_ctx = SpanContext( + trace_id=gen.generate_trace_id(), + span_id=gen.generate_span_id(), + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + ctx = trace.set_span_in_context(NonRecordingSpan(span_ctx)) + carrier: dict[str, str] = {} + TraceContextTextMapPropagator().inject(carrier, context=ctx) + return carrier + + +@contextmanager +def override_ids(trace_id, span_id, ctx=None): + ctx = context.set_value(OVERRIDE_TRACE_ID_KEY, trace_id, context=ctx) + ctx = context.set_value(OVERRIDE_SPAN_ID_KEY, span_id, context=ctx) + token = context.attach(ctx) + try: + yield + finally: + context.detach(token) + + +def _get_backcompat_config() -> tuple[str | None, Resource | None]: + """ + Possibly get deprecated Airflow configs for otel. + + Ideally we return (None, None) here. But if the old configuration is there, + then we will use it. + """ + resource = None + if not os.environ.get("OTEL_SERVICE_NAME") and not os.environ.get("OTEL_RESOURCE_ATTRIBUTES"): + service_name = conf.get("traces", "otel_service", fallback=None) + if service_name: + resource = Resource({"service.name": service_name}) + + endpoint = None + if not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") and not os.environ.get( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" + ): + # this is only for backcompat! + host = conf.get("traces", "otel_host", fallback=None) + port = conf.get("traces", "otel_port", fallback=None) + ssl_active = conf.getboolean("traces", "otel_ssl_active", fallback=False) + if host and port: + scheme = "https" if ssl_active else "http" + endpoint = f"{scheme}://{host}:{port}/v1/traces" + return endpoint, resource + + +def _load_exporter_from_env() -> SpanExporter: + """ + Load a span exporter using the OTEL_TRACES_EXPORTER env var. + + Mirrors the entry-point mechanism used by the OTEL SDK auto-instrumentation + configurator. Supported values (from installed packages): + - ``otlp`` (default) — OTLP/gRPC + - ``otlp_proto_http`` — OTLP/HTTP + - ``console`` — stdout (useful for debugging) + """ + exporter_name = os.environ.get("OTEL_TRACES_EXPORTER", "otlp") + eps = entry_points(group="opentelemetry_traces_exporter", name=exporter_name) + ep = next(iter(eps), None) + if ep is None: + raise RuntimeError( + f"No span exporter found for OTEL_TRACES_EXPORTER={exporter_name!r}. " + f"Available: {[e.name for e in entry_points(group='opentelemetry_traces_exporter')]}" + ) + return ep.load()() + + +def configure_otel(): + otel_on = conf.getboolean("traces", "otel_on", fallback=False) + if not otel_on: + return + + # ideally both endpoint and resource are None here + # they would only be something other than None if user is using deprecated + # Airflow-defined otel configs + backcompat_endpoint, resource = _get_backcompat_config() + + # backcompat: if old-style host/port config provided an endpoint, set the + # env var so the exporter (loaded below) picks it up automatically + + otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") + otlp_traces_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") + if backcompat_endpoint and not (otlp_endpoint or otlp_traces_endpoint): + os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = backcompat_endpoint + + provider = TracerProvider(id_generator=OverrideableRandomIdGenerator(), resource=resource) + provider.add_span_processor(BatchSpanProcessor(_load_exporter_from_env())) + trace.set_tracer_provider(provider) diff --git a/airflow-core/src/airflow/settings.py b/airflow-core/src/airflow/settings.py index b8bc480ef156f..49d46f652c6fb 100644 --- a/airflow-core/src/airflow/settings.py +++ b/airflow-core/src/airflow/settings.py @@ -38,6 +38,8 @@ ) from sqlalchemy.orm import scoped_session, sessionmaker +from airflow.observability.traces import configure_otel + try: from sqlalchemy.ext.asyncio import async_sessionmaker except ImportError: @@ -722,7 +724,7 @@ def initialize(): load_policy_plugins(policy_mgr) import_local_settings() configure_logging() - + configure_otel() configure_adapters() # The webservers import this file from models.py with the default settings. diff --git a/airflow-core/tests/integration/otel/dags/otel_test_dag.py b/airflow-core/tests/integration/otel/dags/otel_test_dag.py index 6c005a9927ee9..25861c8f622ae 100644 --- a/airflow-core/tests/integration/otel/dags/otel_test_dag.py +++ b/airflow-core/tests/integration/otel/dags/otel_test_dag.py @@ -22,12 +22,12 @@ from opentelemetry import trace from airflow import DAG -from airflow.sdk import chain, task -from airflow.sdk.observability.trace import Trace -from airflow.sdk.observability.traces import otel_tracer +from airflow.sdk import task logger = logging.getLogger("airflow.otel_test_dag") +tracer = trace.get_tracer(__name__) + args = { "owner": "airflow", "start_date": datetime(2024, 9, 1), @@ -36,52 +36,13 @@ @task -def task1(ti): - logger.info("Starting Task_1.") - - context_carrier = ti.context_carrier - - otel_task_tracer = otel_tracer.get_otel_tracer_for_task(Trace) - tracer_provider = otel_task_tracer.get_otel_tracer_provider() - - if context_carrier is not None: - logger.info("Found ti.context_carrier: %s.", str(context_carrier)) - logger.info("Extracting the span context from the context_carrier.") - parent_context = otel_task_tracer.extract(context_carrier) - with otel_task_tracer.start_child_span( - span_name="task1_sub_span1", - parent_context=parent_context, - component="dag", - ) as s1: - s1.set_attribute("attr1", "val1") - logger.info("From task sub_span1.") - - with otel_task_tracer.start_child_span("task1_sub_span2") as s2: - s2.set_attribute("attr2", "val2") - logger.info("From task sub_span2.") +def task1(): + logger.info("starting task1") - tracer = trace.get_tracer("trace_test.tracer", tracer_provider=tracer_provider) - with tracer.start_as_current_span(name="task1_sub_span3") as s3: - s3.set_attribute("attr3", "val3") - logger.info("From task sub_span3.") + with tracer.start_as_current_span("sub_span1") as s1: + s1.set_attribute("attr1", "val1") - with otel_task_tracer.start_child_span( - span_name="task1_sub_span4", - parent_context=parent_context, - component="dag", - ) as s4: - s4.set_attribute("attr4", "val4") - logger.info("From task sub_span4.") - - logger.info("Task_1 finished.") - - -@task -def task2(): - logger.info("Starting Task_2.") - for i in range(3): - logger.info("Task_2, iteration '%d'.", i) - logger.info("Task_2 finished.") + logger.info("task1 finished.") with DAG( @@ -90,4 +51,4 @@ def task2(): schedule=None, catchup=False, ) as dag: - chain(task1(), task2()) # type: ignore + task1() diff --git a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py b/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py deleted file mode 100644 index 72fb9148a40e5..0000000000000 --- a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_between_tasks.py +++ /dev/null @@ -1,158 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import logging -import os -import time -from datetime import datetime - -from opentelemetry import trace -from sqlalchemy import select - -from airflow import DAG -from airflow.models import TaskInstance -from airflow.providers.standard.version_compat import AIRFLOW_V_3_0_PLUS -from airflow.sdk import chain, task -from airflow.sdk.observability.trace import Trace -from airflow.sdk.observability.traces import otel_tracer -from airflow.utils.session import create_session - -logger = logging.getLogger("airflow.otel_test_dag_with_pause") - -args = { - "owner": "airflow", - "start_date": datetime(2024, 9, 2), - "retries": 0, -} - - -@task -def task1(ti): - logger.info("Starting Task_1.") - - context_carrier = ti.context_carrier - - otel_task_tracer = otel_tracer.get_otel_tracer_for_task(Trace) - tracer_provider = otel_task_tracer.get_otel_tracer_provider() - - if context_carrier is not None: - logger.info("Found ti.context_carrier: %s.", context_carrier) - logger.info("Extracting the span context from the context_carrier.") - - # If the task takes too long to execute, then the ti should be read from the db - # to make sure that the initial context_carrier is the same. - # Since Airflow 3, direct db access has been removed entirely. - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - - parent_context = Trace.extract(context_carrier) - with otel_task_tracer.start_child_span( - span_name="task1_sub_span1", - parent_context=parent_context, - component="dag", - ) as s1: - s1.set_attribute("attr1", "val1") - logger.info("From task sub_span1.") - - with otel_task_tracer.start_child_span("task1_sub_span2") as s2: - s2.set_attribute("attr2", "val2") - logger.info("From task sub_span2.") - - tracer = trace.get_tracer("trace_test.tracer", tracer_provider=tracer_provider) - with tracer.start_as_current_span(name="task1_sub_span3") as s3: - s3.set_attribute("attr3", "val3") - logger.info("From task sub_span3.") - - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - parent_context = Trace.extract(context_carrier) - - with otel_task_tracer.start_child_span( - span_name="task1_sub_span4", - parent_context=parent_context, - component="dag", - ) as s4: - s4.set_attribute("attr4", "val4") - logger.info("From task sub_span4.") - - logger.info("Task_1 finished.") - - -@task -def paused_task(): - logger.info("Starting Paused_task.") - - dag_folder = os.path.dirname(os.path.abspath(__file__)) - control_file = os.path.join(dag_folder, "dag_control.txt") - - # Create the file and write 'pause' to it. - with open(control_file, "w") as file: - file.write("pause") - - # Pause execution until the word 'pause' is replaced on the file. - while True: - # If there is an exception, then writing to the file failed. Let it exit. - file_contents = None - with open(control_file) as file: - file_contents = file.read() - - if "pause" in file_contents: - logger.info("Task has been paused.") - time.sleep(1) - continue - logger.info("Resuming task execution.") - # Break the loop and finish with the task execution. - break - - # Cleanup the control file. - if os.path.exists(control_file): - os.remove(control_file) - print("Control file has been cleaned up.") - - logger.info("Paused_task finished.") - - -@task -def task2(): - logger.info("Starting Task_2.") - for i in range(3): - logger.info("Task_2, iteration '%d'.", i) - logger.info("Task_2 finished.") - - -with DAG( - "otel_test_dag_with_pause_between_tasks", - default_args=args, - schedule=None, - catchup=False, -) as dag: - chain(task1(), paused_task(), task2()) # type: ignore diff --git a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py b/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py deleted file mode 100644 index dfc5c30243f08..0000000000000 --- a/airflow-core/tests/integration/otel/dags/otel_test_dag_with_pause_in_task.py +++ /dev/null @@ -1,151 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import logging -import os -import time -from datetime import datetime - -from opentelemetry import trace -from sqlalchemy import select - -from airflow import DAG -from airflow.models import TaskInstance -from airflow.providers.standard.version_compat import AIRFLOW_V_3_0_PLUS -from airflow.sdk import chain, task -from airflow.sdk.observability.trace import Trace -from airflow.sdk.observability.traces import otel_tracer -from airflow.utils.session import create_session - -logger = logging.getLogger("airflow.otel_test_dag_with_pause_in_task") - -args = { - "owner": "airflow", - "start_date": datetime(2024, 9, 2), - "retries": 0, -} - - -@task -def task1(ti): - logger.info("Starting Task_1.") - - context_carrier = ti.context_carrier - - dag_folder = os.path.dirname(os.path.abspath(__file__)) - control_file = os.path.join(dag_folder, "dag_control.txt") - - # Create the file and write 'pause' to it. - with open(control_file, "w") as file: - file.write("pause") - - # Pause execution until the word 'pause' is replaced on the file. - while True: - # If there is an exception, then writing to the file failed. Let it exit. - file_contents = None - with open(control_file) as file: - file_contents = file.read() - - if "pause" in file_contents: - logger.info("Task has been paused.") - time.sleep(1) - continue - logger.info("Resuming task execution.") - # Break the loop and finish with the task execution. - break - - otel_task_tracer = otel_tracer.get_otel_tracer_for_task(Trace) - tracer_provider = otel_task_tracer.get_otel_tracer_provider() - - if context_carrier is not None: - logger.info("Found ti.context_carrier: %s.", context_carrier) - logger.info("Extracting the span context from the context_carrier.") - - # If the task takes too long to execute, then the ti should be read from the db - # to make sure that the initial context_carrier is the same. - # Since Airflow 3, direct db access has been removed entirely. - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - - parent_context = Trace.extract(context_carrier) - with otel_task_tracer.start_child_span( - span_name="task1_sub_span1", - parent_context=parent_context, - component="dag", - ) as s1: - s1.set_attribute("attr1", "val1") - logger.info("From task sub_span1.") - - with otel_task_tracer.start_child_span("task1_sub_span2") as s2: - s2.set_attribute("attr2", "val2") - logger.info("From task sub_span2.") - - tracer = trace.get_tracer("trace_test.tracer", tracer_provider=tracer_provider) - with tracer.start_as_current_span(name="task1_sub_span3") as s3: - s3.set_attribute("attr3", "val3") - logger.info("From task sub_span3.") - - if not AIRFLOW_V_3_0_PLUS: - with create_session() as session: - session_ti: TaskInstance = session.scalars( - select(TaskInstance).where( - TaskInstance.task_id == ti.task_id, - TaskInstance.run_id == ti.run_id, - ) - ).one() - context_carrier = session_ti.context_carrier - parent_context = Trace.extract(context_carrier) - - with otel_task_tracer.start_child_span( - span_name="task1_sub_span4", - parent_context=parent_context, - component="dag", - ) as s4: - s4.set_attribute("attr4", "val4") - logger.info("From task sub_span4.") - - # Cleanup the control file. - if os.path.exists(control_file): - os.remove(control_file) - print("Control file has been cleaned up.") - - logger.info("Task_1 finished.") - - -@task -def task2(): - logger.info("Starting Task_2.") - for i in range(3): - logger.info("Task_2, iteration '%d'.", i) - logger.info("Task_2 finished.") - - -with DAG( - "otel_test_dag_with_pause_in_task", - default_args=args, - schedule=None, - catchup=False, -) as dag: - chain(task1(), task2()) # type: ignore diff --git a/airflow-core/tests/integration/otel/test_otel.py b/airflow-core/tests/integration/otel/test_otel.py index 0e4546e301d77..60af1060ce12a 100644 --- a/airflow-core/tests/integration/otel/test_otel.py +++ b/airflow-core/tests/integration/otel/test_otel.py @@ -250,7 +250,7 @@ def serialize_and_get_dags(cls) -> dict[str, SerializedDAG]: dag_bag = DagBag(dag_folder=cls.dag_folder, include_examples=False) dag_ids = dag_bag.dag_ids - assert len(dag_ids) == 3 + assert len(dag_ids) == 1 dag_dict: dict[str, SerializedDAG] = {} with create_session() as session: @@ -317,7 +317,7 @@ def dag_execution_for_testing_metrics(self, capfd): try: # Start the processes here and not as fixtures or in a common setup, # so that the test can capture their output. - scheduler_process, apiserver_process = self.start_worker_and_scheduler() + scheduler_process, apiserver_process = self.start_scheduler() dag_id = "otel_test_dag" @@ -441,7 +441,7 @@ def test_dag_execution_succeeds(self, capfd): try: # Start the processes here and not as fixtures or in a common setup, # so that the test can capture their output. - scheduler_process, apiserver_process = self.start_worker_and_scheduler() + scheduler_process, apiserver_process = self.start_scheduler() dag_id = "otel_test_dag" @@ -486,10 +486,8 @@ def test_dag_execution_succeeds(self, capfd): log.info("out-start --\n%s\n-- out-end", out) log.info("err-start --\n%s\n-- err-end", err) - # host = "host.docker.internal" host = "jaeger" service_name = os.environ.get("OTEL_SERVICE_NAME", "test") - # service_name ``= "my-service-name" r = requests.get(f"http://{host}:16686/api/traces?service={service_name}") data = r.json() @@ -510,16 +508,12 @@ def get_parent_span_id(span): nested = get_span_hierarchy() assert nested == { - "otel_test_dag": None, - "task1": None, - "task1_sub_span1": None, - "task1_sub_span2": None, - "task1_sub_span3": "task1_sub_span2", - "task1_sub_span4": None, - "task2": None, + "sub_span1": "task_run.task1", + "task_run.task1": "dag_run.otel_test_dag", + "dag_run.otel_test_dag": None, } - def start_worker_and_scheduler(self): + def start_scheduler(self): scheduler_process = subprocess.Popen( self.scheduler_command_args, env=os.environ.copy(), diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py b/airflow-core/tests/unit/jobs/test_scheduler_job.py index 3aeebcedbdc2c..c23f180f3a479 100644 --- a/airflow-core/tests/unit/jobs/test_scheduler_job.py +++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py @@ -81,7 +81,6 @@ from airflow.models.taskinstance import TaskInstance from airflow.models.team import Team from airflow.models.trigger import Trigger -from airflow.observability.trace import Trace from airflow.partition_mappers.base import PartitionMapper as CorePartitionMapper from airflow.providers.standard.operators.bash import BashOperator from airflow.providers.standard.operators.empty import EmptyOperator @@ -93,9 +92,7 @@ from airflow.serialization.serialized_objects import LazyDeserializedDAG from airflow.timetables.base import DagRunInfo, DataInterval from airflow.utils.session import create_session, provide_session -from airflow.utils.span_status import SpanStatus from airflow.utils.state import CallbackState, DagRunState, State, TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType from tests_common.pytest_plugin import AIRFLOW_ROOT_PATH @@ -3283,190 +3280,6 @@ def test_runs_are_created_after_max_active_runs_was_reached(self, dag_maker, ses dag_runs = DagRun.find(dag_id=dag.dag_id, session=session) assert len(dag_runs) == 2 - @pytest.mark.parametrize( - ("ti_state", "final_ti_span_status"), - [ - pytest.param(State.SUCCESS, SpanStatus.ENDED, id="dr_ended_successfully"), - pytest.param(State.RUNNING, SpanStatus.ACTIVE, id="dr_still_running"), - ], - ) - def test_recreate_unhealthy_scheduler_spans_if_needed(self, ti_state, final_ti_span_status, dag_maker): - with dag_maker( - dag_id="test_recreate_unhealthy_scheduler_spans_if_needed", - start_date=DEFAULT_DATE, - max_active_runs=1, - dagrun_timeout=datetime.timedelta(seconds=60), - ): - EmptyOperator(task_id="dummy") - - session = settings.Session() - - old_job = Job() - old_job.job_type = SchedulerJobRunner.job_type - - session.add(old_job) - session.commit() - - assert old_job.is_alive() is False - - new_job = Job() - new_job.job_type = SchedulerJobRunner.job_type - session.add(new_job) - session.flush() - - self.job_runner = SchedulerJobRunner(job=new_job) - self.job_runner.active_spans = ThreadSafeDict() - assert len(self.job_runner.active_spans.get_all()) == 0 - - dr = dag_maker.create_dagrun() - dr.state = State.RUNNING - dr.span_status = SpanStatus.ACTIVE - dr.scheduled_by_job_id = old_job.id - - ti = dr.get_task_instances(session=session)[0] - ti.state = ti_state - ti.start_date = timezone.utcnow() - ti.span_status = SpanStatus.ACTIVE - ti.queued_by_job_id = old_job.id - session.merge(ti) - session.merge(dr) - session.commit() - - assert dr.scheduled_by_job_id != self.job_runner.job.id - assert dr.scheduled_by_job_id == old_job.id - assert dr.run_id is not None - assert dr.state == State.RUNNING - assert dr.span_status == SpanStatus.ACTIVE - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is None - - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - assert ti.state == ti_state - assert ti.span_status == SpanStatus.ACTIVE - - self.job_runner._recreate_unhealthy_scheduler_spans_if_needed(dr, session) - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is not None - - if final_ti_span_status == SpanStatus.ACTIVE: - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is not None - assert len(self.job_runner.active_spans.get_all()) == 2 - else: - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - assert len(self.job_runner.active_spans.get_all()) == 1 - - assert dr.span_status == SpanStatus.ACTIVE - assert ti.span_status == final_ti_span_status - - def test_end_spans_of_externally_ended_ops(self, dag_maker): - with dag_maker( - dag_id="test_end_spans_of_externally_ended_ops", - start_date=DEFAULT_DATE, - max_active_runs=1, - dagrun_timeout=datetime.timedelta(seconds=60), - ): - EmptyOperator(task_id="dummy") - - session = settings.Session() - - job = Job() - job.job_type = SchedulerJobRunner.job_type - session.add(job) - - self.job_runner = SchedulerJobRunner(job=job) - self.job_runner.active_spans = ThreadSafeDict() - assert len(self.job_runner.active_spans.get_all()) == 0 - - dr = dag_maker.create_dagrun() - dr.state = State.SUCCESS - dr.span_status = SpanStatus.SHOULD_END - - ti = dr.get_task_instances(session=session)[0] - ti.state = State.SUCCESS - ti.span_status = SpanStatus.SHOULD_END - ti.context_carrier = {} - session.merge(ti) - session.merge(dr) - session.commit() - - dr_span = Trace.start_root_span(span_name="dag_run_span", start_as_current=False) - ti_span = Trace.start_child_span(span_name="ti_span", start_as_current=False) - - self.job_runner.active_spans.set("dr:" + str(dr.id), dr_span) - self.job_runner.active_spans.set(f"ti:{ti.id}", ti_span) - - assert dr.span_status == SpanStatus.SHOULD_END - assert ti.span_status == SpanStatus.SHOULD_END - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is not None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is not None - - self.job_runner._end_spans_of_externally_ended_ops(session) - - assert dr.span_status == SpanStatus.ENDED - assert ti.span_status == SpanStatus.ENDED - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - - @pytest.mark.parametrize( - ("state", "final_span_status"), - [ - pytest.param(State.SUCCESS, SpanStatus.ENDED, id="dr_ended_successfully"), - pytest.param(State.RUNNING, SpanStatus.NEEDS_CONTINUANCE, id="dr_still_running"), - ], - ) - def test_end_active_spans(self, state, final_span_status, dag_maker): - with dag_maker( - dag_id="test_end_active_spans", - start_date=DEFAULT_DATE, - max_active_runs=1, - dagrun_timeout=datetime.timedelta(seconds=60), - ): - EmptyOperator(task_id="dummy") - - session = settings.Session() - - job = Job() - job.job_type = SchedulerJobRunner.job_type - - self.job_runner = SchedulerJobRunner(job=job) - self.job_runner.active_spans = ThreadSafeDict() - assert len(self.job_runner.active_spans.get_all()) == 0 - - dr = dag_maker.create_dagrun() - dr.state = state - dr.span_status = SpanStatus.ACTIVE - - ti = dr.get_task_instances(session=session)[0] - ti.state = state - ti.span_status = SpanStatus.ACTIVE - ti.context_carrier = {} - session.merge(ti) - session.merge(dr) - session.commit() - - dr_span = Trace.start_root_span(span_name="dag_run_span", start_as_current=False) - ti_span = Trace.start_child_span(span_name="ti_span", start_as_current=False) - - self.job_runner.active_spans.set("dr:" + str(dr.id), dr_span) - self.job_runner.active_spans.set(f"ti:{ti.id}", ti_span) - - assert dr.span_status == SpanStatus.ACTIVE - assert ti.span_status == SpanStatus.ACTIVE - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is not None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is not None - assert len(self.job_runner.active_spans.get_all()) == 2 - - self.job_runner._end_active_spans(session) - - assert dr.span_status == final_span_status - assert ti.span_status == final_span_status - - assert self.job_runner.active_spans.get("dr:" + str(dr.id)) is None - assert self.job_runner.active_spans.get(f"ti:{ti.id}") is None - assert len(self.job_runner.active_spans.get_all()) == 0 - def test_dagrun_timeout_verify_max_active_runs(self, dag_maker, session): """ Test if a dagrun will not be scheduled if max_dag_runs diff --git a/airflow-core/tests/unit/models/test_dagrun.py b/airflow-core/tests/unit/models/test_dagrun.py index f3de13422fac3..14722f83b0cce 100644 --- a/airflow-core/tests/unit/models/test_dagrun.py +++ b/airflow-core/tests/unit/models/test_dagrun.py @@ -27,6 +27,7 @@ import pendulum import pytest +from opentelemetry.sdk.trace import TracerProvider from sqlalchemy import func, select from sqlalchemy.orm import joinedload @@ -54,9 +55,7 @@ from airflow.settings import get_policy_plugin_manager from airflow.task.trigger_rule import TriggerRule from airflow.triggers.base import StartTriggerArgs -from airflow.utils.span_status import SpanStatus from airflow.utils.state import DagRunState, State, TaskInstanceState -from airflow.utils.thread_safe_dict import ThreadSafeDict from airflow.utils.types import DagRunTriggeredByType, DagRunType from tests_common.test_utils import db @@ -560,142 +559,6 @@ def test_on_success_callback_when_task_skipped(self, session, testing_dag_bundle assert dag_run.state == DagRunState.SUCCESS mock_on_success.assert_called_once() - def test_start_dr_spans_if_needed_new_span(self, dag_maker, session): - with dag_maker( - dag_id="test_start_dr_spans_if_needed_new_span", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.QUEUED, - "test_task2": TaskInstanceState.QUEUED, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - tis = dag_run.get_task_instances() - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - assert dag_run.span_status == SpanStatus.NOT_STARTED - - dag_run.start_dr_spans_if_needed(tis=tis) - - assert dag_run.span_status == SpanStatus.ACTIVE - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is not None - - def test_start_dr_spans_if_needed_span_with_continuance(self, dag_maker, session): - with dag_maker( - dag_id="test_start_dr_spans_if_needed_span_with_continuance", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.RUNNING, - "test_task2": TaskInstanceState.QUEUED, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - dag_run.span_status = SpanStatus.NEEDS_CONTINUANCE - - tis = dag_run.get_task_instances() - - first_ti = tis[0] - first_ti.span_status = SpanStatus.NEEDS_CONTINUANCE - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - assert dag_run.active_spans.get(f"ti:{first_ti.id}") is None - assert dag_run.span_status == SpanStatus.NEEDS_CONTINUANCE - assert first_ti.span_status == SpanStatus.NEEDS_CONTINUANCE - - dag_run.start_dr_spans_if_needed(tis=tis) - - assert dag_run.span_status == SpanStatus.ACTIVE - assert first_ti.span_status == SpanStatus.ACTIVE - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is not None - assert dag_run.active_spans.get(f"ti:{first_ti.id}") is not None - - def test_end_dr_span_if_needed(self, testing_dag_bundle, dag_maker, session): - with dag_maker( - dag_id="test_end_dr_span_if_needed", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.SUCCESS, - "test_task2": TaskInstanceState.SUCCESS, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - from airflow.observability.trace import Trace - - dr_span = Trace.start_root_span(span_name="test_span", start_as_current=False) - - active_spans.set("dr:" + str(dag_run.id), dr_span) - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is not None - - dag_run.end_dr_span_if_needed() - - assert dag_run.span_status == SpanStatus.ENDED - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - - def test_end_dr_span_if_needed_with_span_from_another_scheduler( - self, testing_dag_bundle, dag_maker, session - ): - with dag_maker( - dag_id="test_end_dr_span_if_needed_with_span_from_another_scheduler", - schedule=datetime.timedelta(days=1), - start_date=datetime.datetime(2017, 1, 1), - ) as dag: - dag_task1 = EmptyOperator(task_id="test_task1") - dag_task2 = EmptyOperator(task_id="test_task2") - dag_task1.set_downstream(dag_task2) - - initial_task_states = { - "test_task1": TaskInstanceState.SUCCESS, - "test_task2": TaskInstanceState.SUCCESS, - } - - dag_run = self.create_dag_run(dag=dag, task_states=initial_task_states, session=session) - - active_spans = ThreadSafeDict() - dag_run.set_active_spans(active_spans) - - dag_run.span_status = SpanStatus.ACTIVE - - assert dag_run.active_spans is not None - assert dag_run.active_spans.get("dr:" + str(dag_run.id)) is None - - dag_run.end_dr_span_if_needed() - - assert dag_run.span_status == SpanStatus.SHOULD_END - def test_dagrun_update_state_with_handle_callback_success(self, testing_dag_bundle, dag_maker, session): def on_success_callable(context): assert context["dag_run"].dag_id == "test_dagrun_update_state_with_handle_callback_success" @@ -744,7 +607,6 @@ def on_success_callable(context): ) def test_dagrun_update_state_with_handle_callback_failure(self, testing_dag_bundle, dag_maker, session): - def on_failure_callable(context): assert context["dag_run"].dag_id == "test_dagrun_update_state_with_handle_callback_failure" @@ -3292,3 +3154,159 @@ def on_failure(context): assert context_received["ti"].task_id == "test_task" assert context_received["ti"].dag_id == "test_dag" assert context_received["ti"].run_id == dr.run_id + + +class TestDagRunTracing: + """Tests for DagRun OpenTelemetry span behavior.""" + + @pytest.fixture(autouse=True) + def sdk_tracer_provider(self): + """Patch the module-level tracer with one backed by a real SDK provider so spans have valid IDs.""" + provider = TracerProvider() + real_tracer = provider.get_tracer("airflow.models.dagrun") + with mock.patch("airflow.models.dagrun.tracer", real_tracer): + yield + + def test_context_carrier_set_on_init(self, dag_maker): + """DagRun.__init__ should populate context_carrier with a W3C traceparent.""" + with dag_maker("test_tracing_init"): + EmptyOperator(task_id="t1") + dr = dag_maker.create_dagrun() + + assert dr.context_carrier is not None + assert isinstance(dr.context_carrier, dict) + assert "traceparent" in dr.context_carrier + + def test_context_carrier_unique_per_dagrun(self, dag_maker): + """Each DagRun should get a distinct trace context.""" + with dag_maker("test_tracing_unique1"): + EmptyOperator(task_id="t1") + dr1 = dag_maker.create_dagrun() + + with dag_maker("test_tracing_unique2"): + EmptyOperator(task_id="t1") + dr2 = dag_maker.create_dagrun() + + assert dr1.context_carrier["traceparent"] != dr2.context_carrier["traceparent"] + + @pytest.mark.parametrize("final_state", [DagRunState.SUCCESS, DagRunState.FAILED]) + def test_emit_dagrun_span_called_on_completion(self, dag_maker, session, final_state): + """_emit_dagrun_span should be called exactly once when a dag run finishes.""" + with dag_maker("test_tracing_emit", session=session) as dag: + EmptyOperator(task_id="t1") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + ti = dr.get_task_instance("t1", session=session) + ti.state = ( + TaskInstanceState.SUCCESS if final_state == DagRunState.SUCCESS else TaskInstanceState.FAILED + ) + session.flush() + + dr.dag = dag + + with mock.patch.object(dr, "_emit_dagrun_span") as mock_emit: + dr.update_state(session=session) + + mock_emit.assert_called_once_with(state=final_state) + + def test_emit_dagrun_span_not_called_while_running(self, dag_maker, session): + """_emit_dagrun_span should not be called while the dag run is still running.""" + with dag_maker("test_tracing_no_emit_running", session=session) as dag: + EmptyOperator(task_id="t1") + EmptyOperator(task_id="t2") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + tis = dr.get_task_instances(session=session) + for ti in tis: + if ti.task_id == "t1": + ti.state = TaskInstanceState.SUCCESS + else: + ti.state = TaskInstanceState.RUNNING + session.flush() + + dr.dag = dag + + with mock.patch.object(dr, "_emit_dagrun_span") as mock_emit: + dr.update_state(session=session) + + mock_emit.assert_not_called() + + def test_emit_dagrun_span_uses_context_carrier_ids(self, dag_maker, session): + """The emitted span should inherit trace_id/span_id from the context_carrier.""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + + from airflow.observability.traces import OverrideableRandomIdGenerator + + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider(id_generator=OverrideableRandomIdGenerator()) + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + test_tracer = provider.get_tracer("test") + + with dag_maker("test_tracing_ids", session=session) as dag: + EmptyOperator(task_id="t1") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + ti = dr.get_task_instance("t1", session=session) + ti.state = TaskInstanceState.SUCCESS + session.flush() + dr.dag = dag + + with mock.patch("airflow.models.dagrun.tracer", test_tracer): + dr.update_state(session=session) + + spans = in_mem_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + # Decode the expected trace_id/span_id from the stored context_carrier + ctx = TraceContextTextMapPropagator().extract(dr.context_carrier) + from opentelemetry import trace as otel_trace + + stored_span = otel_trace.get_current_span(context=ctx) + stored_ctx = stored_span.get_span_context() + + assert span.context.trace_id == stored_ctx.trace_id + assert span.context.span_id == stored_ctx.span_id + + @pytest.mark.parametrize("final_state", [DagRunState.SUCCESS, DagRunState.FAILED]) + def test_emit_dagrun_span_attributes_and_status(self, dag_maker, session, final_state): + """The emitted span should have the correct name, attributes, and status code.""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + from opentelemetry.trace import StatusCode + + from airflow.observability.traces import OverrideableRandomIdGenerator + + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider(id_generator=OverrideableRandomIdGenerator()) + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + test_tracer = provider.get_tracer("test") + + with dag_maker("test_tracing_attrs", session=session) as dag: + EmptyOperator(task_id="t1") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + ti = dr.get_task_instance("t1", session=session) + ti.state = ( + TaskInstanceState.SUCCESS if final_state == DagRunState.SUCCESS else TaskInstanceState.FAILED + ) + session.flush() + dr.dag = dag + + with mock.patch("airflow.models.dagrun.tracer", test_tracer): + dr.update_state(session=session) + + spans = in_mem_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == f"dag_run.{dr.dag_id}" + assert span.attributes["airflow.dag_id"] == dr.dag_id + assert span.attributes["airflow.dag_run.run_id"] == dr.run_id + + expected_status = StatusCode.OK if final_state == DagRunState.SUCCESS else StatusCode.ERROR + assert span.status.status_code == expected_status diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index f6b79fba29aba..aa0bd36de11ad 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1002,6 +1002,7 @@ middleware middlewares midnights milli +millis milton minikube misconfigured diff --git a/scripts/ci/docker-compose/integration-otel.yml b/scripts/ci/docker-compose/integration-otel.yml index 9d5c6c8117ded..f0d32104a14ab 100644 --- a/scripts/ci/docker-compose/integration-otel.yml +++ b/scripts/ci/docker-compose/integration-otel.yml @@ -70,7 +70,7 @@ services: - INTEGRATION_OTEL=true - OTEL_SERVICE_NAME=test - OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf - - OTEL_TRACES_EXPORTER=otlp + - OTEL_TRACES_EXPORTER=otlp_proto_http - OTEL_METRICS_EXPORTER=otlp - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://breeze-otel-collector:4318/v1/traces - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://breeze-otel-collector:4318/v1/metrics diff --git a/task-sdk/src/airflow/sdk/definitions/dag.py b/task-sdk/src/airflow/sdk/definitions/dag.py index 94e447e37a56d..c3f786584746b 100644 --- a/task-sdk/src/airflow/sdk/definitions/dag.py +++ b/task-sdk/src/airflow/sdk/definitions/dag.py @@ -1324,10 +1324,6 @@ def test( triggered_by=DagRunTriggeredByType.TEST, triggering_user_name="dag_test", ) - # Start a mock span so that one is present and not started downstream. We - # don't care about otel in dag.test and starting the span during dagrun update - # is not functioning properly in this context anyway. - dr.start_dr_spans_if_needed(tis=[]) log.debug("starting dagrun") # Instead of starting a scheduler, we run the minimal loop possible to check diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 0ef7a74a24b48..674935f5eaecb 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -19,14 +19,13 @@ from __future__ import annotations -import contextlib import contextvars import functools import os import sys import time from collections.abc import Callable, Iterable, Iterator, Mapping -from contextlib import suppress +from contextlib import ExitStack, contextmanager, suppress from datetime import datetime, timedelta, timezone from itertools import product from pathlib import Path @@ -36,6 +35,8 @@ import attrs import lazy_object_proxy import structlog +from opentelemetry import trace +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from pydantic import AwareDatetime, ConfigDict, Field, JsonValue, TypeAdapter from airflow.dag_processing.bundles.base import BaseDagBundle, BundleVersionLock @@ -133,6 +134,32 @@ from airflow.sdk.exceptions import DagRunTriggerException from airflow.sdk.types import OutletEventAccessorsProtocol +log = structlog.get_logger("task") + +tracer = trace.get_tracer(__name__) + + +@contextmanager +def _make_task_span(msg: StartupDetails): + parent_context = ( + TraceContextTextMapPropagator().extract(msg.ti.context_carrier) if msg.ti.context_carrier else None + ) + ti = msg.ti + span_name = f"task_run.{ti.task_id}" + if ti.map_index is not None and ti.map_index >= 0: + span_name += f"_{ti.map_index}" + with tracer.start_as_current_span(span_name, context=parent_context) as span: + span.set_attributes( + { + "airflow.dag_id": ti.dag_id, + "airflow.task_id": ti.task_id, + "airflow.dag_run.run_id": ti.run_id, + "airflow.task_instance.try_number": ti.try_number, + "airflow.task_instance.map_index": ti.map_index if ti.map_index is not None else -1, + } + ) + yield span + class TaskRunnerMarker: """Marker for listener hooks, to properly detect from which component they are called.""" @@ -476,8 +503,6 @@ def get_first_reschedule_date(self, context: Context) -> AwareDatetime | None: retries: int = self.task.retries or 0 first_try_number = max_tries - retries + 1 - log = structlog.get_logger(logger_name="task") - log.debug("Requesting first reschedule date from supervisor") response = SUPERVISOR_COMMS.send( @@ -494,8 +519,6 @@ def get_previous_dagrun(self, state: str | None = None) -> DagRun | None: context = self.get_template_context() dag_run = context.get("dag_run") - log = structlog.get_logger(logger_name="task") - log.debug("Getting previous Dag run", dag_run=dag_run) if dag_run is None: @@ -530,7 +553,6 @@ def get_previous_ti( context = self.get_template_context() dag_run = context.get("dag_run") - log = structlog.get_logger(logger_name="task") log.debug("Getting previous task instance", task_id=self.task_id, state=state) # Use current dag run's logical_date if not provided @@ -846,7 +868,6 @@ def _verify_bundle_access(bundle_instance: BaseDagBundle, log: Logger) -> None: def get_startup_details() -> StartupDetails: # The parent sends us a StartupDetails message un-prompted. After this, every single message is only sent # in response to us sending a request. - log = structlog.get_logger(logger_name="task") if os.environ.get("_AIRFLOW__REEXECUTED_PROCESS") == "1" and ( msgjson := os.environ.get("_AIRFLOW__STARTUP_MSG") @@ -871,7 +892,6 @@ def get_startup_details() -> StartupDetails: def startup(msg: StartupDetails) -> tuple[RuntimeTaskInstance, Context, Logger]: - log = structlog.get_logger("task") # setproctitle causes issue on Mac OS: https://github.com/benoitc/gunicorn/issues/3021 os_type = sys.platform if os_type == "darwin": @@ -1238,7 +1258,7 @@ def _on_term(signum, frame): import jinja2 # If the task failed, swallow rendering error so it doesn't mask the main error. - with contextlib.suppress(jinja2.TemplateSyntaxError, jinja2.UndefinedError): + with suppress(jinja2.TemplateSyntaxError, jinja2.UndefinedError): previous_rendered_map_index = ti.rendered_map_index ti.rendered_map_index = _render_map_index(context, ti=ti, log=log) # Send update only if value changed (e.g., user set context variables during execution) @@ -1796,6 +1816,20 @@ def finalize( log.exception("error calling listener") +@contextmanager +def flush_spans(): + try: + yield + finally: + provider = trace.get_tracer_provider() + if hasattr(provider, "force_flush"): + from airflow.sdk.configuration import conf + + timeout_millis = conf.getint("traces", "task_runner_flush_timeout_milliseconds", fallback=30000) + provider.force_flush(timeout_millis=timeout_millis) + + +@flush_spans() def main(): log = structlog.get_logger(logger_name="task") @@ -1805,38 +1839,42 @@ def main(): stats_factory = stats_utils.get_stats_factory(Stats) Stats.initialize(factory=stats_factory) - try: + stack = ExitStack() + with stack: try: - startup_details = get_startup_details() - ti, context, log = startup(msg=startup_details) - except AirflowRescheduleException as reschedule: - log.warning("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE") - SUPERVISOR_COMMS.send( - msg=RescheduleTask( - reschedule_date=reschedule.reschedule_date, - end_date=datetime.now(tz=timezone.utc), + try: + startup_details = get_startup_details() + span = _make_task_span(msg=startup_details) + stack.enter_context(span) + ti, context, log = startup(msg=startup_details) + except AirflowRescheduleException as reschedule: + log.warning("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE") + SUPERVISOR_COMMS.send( + msg=RescheduleTask( + reschedule_date=reschedule.reschedule_date, + end_date=datetime.now(tz=timezone.utc), + ) ) - ) - sys.exit(0) - with BundleVersionLock( - bundle_name=ti.bundle_instance.name, - bundle_version=ti.bundle_instance.version, - ): - state, _, error = run(ti, context, log) - context["exception"] = error - finalize(ti, state, context, log, error) - except KeyboardInterrupt: - log.exception("Ctrl-c hit") - sys.exit(2) - except Exception: - log.exception("Top level error") - sys.exit(1) - finally: - # Ensure the request socket is closed on the child side in all circumstances - # before the process fully terminates. - if SUPERVISOR_COMMS and SUPERVISOR_COMMS.socket: - with suppress(Exception): - SUPERVISOR_COMMS.socket.close() + sys.exit(0) + with BundleVersionLock( + bundle_name=ti.bundle_instance.name, + bundle_version=ti.bundle_instance.version, + ): + state, _, error = run(ti, context, log) + context["exception"] = error + finalize(ti, state, context, log, error) + except KeyboardInterrupt: + log.exception("Ctrl-c hit") + sys.exit(2) + except Exception: + log.exception("Top level error") + sys.exit(1) + finally: + # Ensure the request socket is closed on the child side in all circumstances + # before the process fully terminates. + if SUPERVISOR_COMMS and SUPERVISOR_COMMS.socket: + with suppress(Exception): + SUPERVISOR_COMMS.socket.close() def reinit_supervisor_comms() -> None: @@ -1851,7 +1889,6 @@ def reinit_supervisor_comms() -> None: if "SUPERVISOR_COMMS" not in globals(): global SUPERVISOR_COMMS - log = structlog.get_logger(logger_name="task") fd = int(os.environ.get("__AIRFLOW_SUPERVISOR_FD", "0")) diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 2a495e557f70f..05191806be8a5 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -127,6 +127,7 @@ TaskRunnerMarker, _defer_task, _execute_task, + _make_task_span, _push_xcom_if_needed, _xcom_push, finalize, @@ -367,7 +368,7 @@ def test_parse_not_found_does_not_reschedule_when_max_attempts_reached(test_dags @mock.patch("airflow.sdk.execution_time.task_runner.get_startup_details") @mock.patch("airflow.sdk.execution_time.task_runner.CommsDecoder") def test_main_sends_reschedule_task_when_startup_reschedules( - mock_comms_decoder_cls, mock_get_startup_details, mock_startup, mock_exit, time_machine + mock_comms_decoder_cls, mock_get_startup_details, mock_startup, mock_exit, time_machine, make_ti_context ): """ If startup raises AirflowRescheduleException, the task runner should report a RescheduleTask @@ -379,7 +380,23 @@ def test_main_sends_reschedule_task_when_startup_reschedules( mock_comms_instance = mock.Mock() mock_comms_instance.socket = None mock_comms_decoder_cls.__getitem__.return_value.return_value = mock_comms_instance - mock_get_startup_details.return_value = mock.Mock() + what = StartupDetails( + ti=TaskInstance( + id=uuid7(), + task_id="my_task", + dag_id="test_dag", + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + context_carrier={}, + ), + dag_rel_path="", + bundle_info=BundleInfo(name="my-bundle", version=None), + ti_context=make_ti_context(), + start_date=timezone.utcnow(), + sentry_integration="", + ) + mock_get_startup_details.return_value = what mock_startup.side_effect = AirflowRescheduleException(reschedule_date=reschedule_date) # Move time @@ -395,6 +412,102 @@ def test_main_sends_reschedule_task_when_startup_reschedules( ] +def test_task_span_is_child_of_dag_run_span(make_ti_context): + """Task span must be a child of the dag run span propagated via context_carrier.""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + + # Build a real SDK provider and exporter so we can inspect finished spans. + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + + # Create a "dag run" span whose context we will propagate into the task. + dag_run_tracer = provider.get_tracer("dag_run") + with dag_run_tracer.start_as_current_span("dag_run.test_dag") as dag_run_span: + carrier: dict[str, str] = {} + TraceContextTextMapPropagator().inject(carrier) + dag_run_span_ctx = dag_run_span.get_span_context() + + what = StartupDetails( + ti=TaskInstance( + id=uuid7(), + task_id="my_task", + dag_id="test_dag", + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + context_carrier=carrier, + ), + dag_rel_path="", + bundle_info=BundleInfo(name="my-bundle", version=None), + ti_context=make_ti_context(), + start_date=timezone.utcnow(), + sentry_integration="", + ) + + task_tracer = provider.get_tracer("airflow.sdk.execution_time.task_runner") + with mock.patch("airflow.sdk.execution_time.task_runner.tracer", task_tracer): + with _make_task_span(what) as span: + task_span_ctx = span.get_span_context() + + # The task span must share the dag run's trace ID. + assert task_span_ctx.trace_id == dag_run_span_ctx.trace_id + + # The task span's parent must be the dag run span. + finished = in_mem_exporter.get_finished_spans() + task_spans = [s for s in finished if s.name == "task_run.my_task"] + assert len(task_spans) == 1 + assert task_spans[0].parent is not None + assert task_spans[0].parent.span_id == dag_run_span_ctx.span_id + + # Span attributes are set correctly. + attrs = task_spans[0].attributes + assert attrs["airflow.dag_id"] == "test_dag" + assert attrs["airflow.task_id"] == "my_task" + assert attrs["airflow.dag_run.run_id"] == "test_run" + assert attrs["airflow.task_instance.try_number"] == 1 + + +def test_task_span_no_parent_when_no_context_carrier(make_ti_context): + """When context_carrier is absent, the task span should be a root span (no parent).""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + + what = StartupDetails( + ti=TaskInstance( + id=uuid7(), + task_id="standalone_task", + dag_id="test_dag", + run_id="test_run", + try_number=1, + dag_version_id=uuid7(), + context_carrier=None, + ), + dag_rel_path="", + bundle_info=BundleInfo(name="my-bundle", version=None), + ti_context=make_ti_context(), + start_date=timezone.utcnow(), + sentry_integration="", + ) + + task_tracer = provider.get_tracer("airflow.sdk.execution_time.task_runner") + with mock.patch("airflow.sdk.execution_time.task_runner.tracer", task_tracer): + with _make_task_span(what): + pass + + finished = in_mem_exporter.get_finished_spans() + assert len(finished) == 1 + assert finished[0].parent is None + + def test_parse_module_in_bundle_root(tmp_path: Path, make_ti_context): """Check that the bundle path is added to sys.path, so Dags can import shared modules.""" tmp_path.joinpath("util.py").write_text("NAME = 'dag_name'") From afce4ddd8d8a4b1c6b8892e0882bac10b1073ceb Mon Sep 17 00:00:00 2001 From: ANKIT KUMAR Date: Wed, 11 Mar 2026 00:52:09 +0530 Subject: [PATCH 063/595] Fix: Replace print with logger in Hive Hook kill method (#62990) Signed-off-by: Ankit Kumar --- .../apache/hive/src/airflow/providers/apache/hive/hooks/hive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py index e5ce0cc604778..e8b299d42ff64 100644 --- a/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py +++ b/providers/apache/hive/src/airflow/providers/apache/hive/hooks/hive.py @@ -527,7 +527,7 @@ def kill(self) -> None: """Kill Hive cli command.""" if hasattr(self, "sub_process"): if self.sub_process.poll() is None: - print("Killing the Hive job") + self.log.info("Killing the Hive job") self.sub_process.terminate() time.sleep(60) self.sub_process.kill() From 62c68b704d6e36bc8b786d7481c0dd5ab2608178 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:23:10 +0000 Subject: [PATCH 064/595] Refactor VaultBackend to centralize secret path resolution and fetching logic (#62643) Introduce a private helper to remove duplicated mount parsing, base path handling, and get_secret invocation across public methods. Co-authored-by: Sameer Mesiah --- .../providers/hashicorp/secrets/vault.py | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py index b60e623851025..3459314bf7030 100644 --- a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py +++ b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py @@ -173,23 +173,30 @@ def _parse_path(self, secret_path: str) -> tuple[str | None, str | None]: return split_secret_path[0], split_secret_path[1] return "", secret_path - def get_response(self, conn_id: str) -> dict | None: - """ - Get data from Vault. + def _get_secret_with_base(self, base_path: str | None, key: str) -> dict | None: + """Resolve mount and base path, then fetch the secret from Vault.""" + mount_point, key_part = self._parse_path(key) - :return: The data from the Vault path if exists - """ - mount_point, conn_key = self._parse_path(conn_id) - if self.connections_path is None or conn_key is None: + if base_path is None or key_part is None: return None - if self.connections_path == "": - secret_path = conn_key + + if base_path == "": + secret_path = key_part else: - secret_path = self.build_path(self.connections_path, conn_key) + secret_path = self.build_path(base_path, key_part) + return self.vault_client.get_secret( secret_path=(mount_point + "/" if mount_point else "") + secret_path ) + def get_response(self, conn_id: str) -> dict | None: + """ + Get data from Vault. + + :return: The data from the Vault path if exists + """ + return self._get_secret_with_base(self.connections_path, conn_id) + # Make sure connection is imported this way for type checking, otherwise when importing # the backend it will get a circular dependency and fail if TYPE_CHECKING: @@ -225,16 +232,8 @@ def get_variable(self, key: str, team_name: str | None = None) -> str | None: :param team_name: Team name associated to the task trying to access the variable (if any) :return: Variable Value retrieved from the vault """ - mount_point, variable_key = self._parse_path(key) - if self.variables_path is None or variable_key is None: - return None - if self.variables_path == "": - secret_path = variable_key - else: - secret_path = self.build_path(self.variables_path, variable_key) - response = self.vault_client.get_secret( - secret_path=(mount_point + "/" if mount_point else "") + secret_path - ) + response = self._get_secret_with_base(self.variables_path, key) + if not response: return None try: @@ -250,16 +249,7 @@ def get_config(self, key: str) -> str | None: :param key: Configuration Option Key :return: Configuration Option Value retrieved from the vault """ - mount_point, config_key = self._parse_path(key) - if self.config_path is None or config_key is None: - return None - if self.config_path == "": - secret_path = config_key - else: - secret_path = self.build_path(self.config_path, config_key) - response = self.vault_client.get_secret( - secret_path=(mount_point + "/" if mount_point else "") + secret_path - ) + response = self._get_secret_with_base(self.config_path, key) if not response: return None try: From a6eae2a0cf77875f12250e49766eb7dcc8066b32 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:24:01 +0000 Subject: [PATCH 065/595] Fix string handling for use_tls in VaultHook (#62641) Ensure use_tls extras provided as strings (e.g. "false") are correctly interpreted instead of being treated as truthy, preventing incorrect HTTPS selection and SSL errors. Existing tests were updated to cover string inputs in addition to boolean values. Co-authored-by: Sameer Mesiah --- .../src/airflow/providers/hashicorp/hooks/vault.py | 12 ++++++++---- .../tests/unit/hashicorp/hooks/test_vault.py | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py b/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py index 05d14b9b4dba8..6de3138a0c840 100644 --- a/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py +++ b/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py @@ -30,6 +30,7 @@ _VaultClient, ) from airflow.utils.helpers import merge_dicts +from airflow.utils.strings import to_boolean if TYPE_CHECKING: import hvac @@ -187,11 +188,14 @@ def __init__( else (None, None) ) - if self.connection.extra_dejson.get("use_tls") is not None: - if bool(self.connection.extra_dejson.get("use_tls")): - conn_protocol = "https" + use_tls = self.connection.extra_dejson.get("use_tls") + + if use_tls is not None: + if isinstance(use_tls, bool): + is_tls = use_tls else: - conn_protocol = "http" + is_tls = to_boolean(use_tls) + conn_protocol = "https" if is_tls else "http" else: if self.connection.conn_type == "vault": conn_protocol = "http" diff --git a/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py b/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py index 50b9f9e022c7d..141f6b838267d 100644 --- a/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py +++ b/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py @@ -199,6 +199,8 @@ def test_protocol(self, mock_hvac, mock_get_connection, protocol, expected_url): [ (True, "https://localhost:8180"), (False, "http://localhost:8180"), + ("true", "https://localhost:8180"), + ("false", "http://localhost:8180"), ], ) @mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection") From cbf4b9b8501705b7433d0a4d0c1bf859735a745c Mon Sep 17 00:00:00 2001 From: Pradeep Kalluri <128097794+kalluripradeep@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:27:58 +0000 Subject: [PATCH 066/595] fix: suppress warning for TYPE_CHECKING-only forward references in TaskFlow (#63053) Fixes #62945 --- .../tests/unit/standard/decorators/test_python.py | 12 +----------- task-sdk/src/airflow/sdk/bases/decorator.py | 10 +++------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/providers/standard/tests/unit/standard/decorators/test_python.py b/providers/standard/tests/unit/standard/decorators/test_python.py index d2aa38ec54434..95c96ce7bed89 100644 --- a/providers/standard/tests/unit/standard/decorators/test_python.py +++ b/providers/standard/tests/unit/standard/decorators/test_python.py @@ -195,17 +195,7 @@ def t3( # type: ignore[empty-body] y: int, ) -> "UnresolveableName[int, int]": ... - with pytest.warns(UserWarning, match="Cannot infer multiple_outputs.*t3") as recwarn: - line = sys._getframe().f_lineno - 5 if PY38 else sys._getframe().f_lineno - 2 - - if PY311: - # extra line explaining the error location in Py311 - line = line - 1 - - warn = recwarn[0] - assert warn.filename == __file__ - assert warn.lineno == line - + # No warning should be raised for TYPE_CHECKING-only forward references assert t3(5, 5).operator.multiple_outputs is False def test_infer_multiple_outputs_using_other_typing(self): diff --git a/task-sdk/src/airflow/sdk/bases/decorator.py b/task-sdk/src/airflow/sdk/bases/decorator.py index 53565c531a636..d76c029827814 100644 --- a/task-sdk/src/airflow/sdk/bases/decorator.py +++ b/task-sdk/src/airflow/sdk/bases/decorator.py @@ -20,7 +20,6 @@ import itertools import re import textwrap -import warnings from collections.abc import Callable, Collection, Iterator, Mapping, Sequence from contextlib import suppress from functools import cached_property, partial, update_wrapper @@ -448,12 +447,9 @@ def fake(): ... fake.__annotations__ = {"return": self.function.__annotations__["return"]} return_type = typing_extensions.get_type_hints(fake, self.function.__globals__).get("return", Any) - except NameError as e: - warnings.warn( - f"Cannot infer multiple_outputs for TaskFlow function {self.function.__name__!r} with forward" - f" type references that are not imported. (Error was {e})", - stacklevel=4, - ) + except NameError: + # Forward references using TYPE_CHECKING-only imports are valid Python patterns. + # We cannot infer multiple_outputs when the type is not available at runtime. return False except TypeError: # Can't evaluate return type. return False From 22da484082a6f68492da32556127760df166cae1 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:32:19 -0700 Subject: [PATCH 067/595] fix: use configured schema in TableauHook Server construction (#62847) Previously, TableauHook.__init__ ignored the schema field from the connection configuration, always creating HTTP connections. Now it prepends the schema (e.g., https) to the host when configured. Fixes apache/airflow#62459 Co-authored-by: Claude Sonnet 4.6 --- .../providers/tableau/hooks/tableau.py | 3 +- .../tests/unit/tableau/hooks/test_tableau.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py b/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py index 4fd083820ba8e..3e39927870027 100644 --- a/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py +++ b/providers/tableau/src/airflow/providers/tableau/hooks/tableau.py @@ -86,7 +86,8 @@ def __init__(self, site_id: str | None = None, tableau_conn_id: str = default_co self.tableau_conn_id = tableau_conn_id self.conn = self.get_connection(self.tableau_conn_id) self.site_id = site_id or self.conn.extra_dejson.get("site_id", "") - self.server = Server(self.conn.host) + server_address = f"{self.conn.schema}://{self.conn.host}" if self.conn.schema else self.conn.host + self.server = Server(server_address) verify: Any = self.conn.extra_dejson.get("verify", True) if isinstance(verify, str): verify = parse_boolean(verify) diff --git a/providers/tableau/tests/unit/tableau/hooks/test_tableau.py b/providers/tableau/tests/unit/tableau/hooks/test_tableau.py index e13642d6df963..9d5a9a18f3292 100644 --- a/providers/tableau/tests/unit/tableau/hooks/test_tableau.py +++ b/providers/tableau/tests/unit/tableau/hooks/test_tableau.py @@ -110,6 +110,17 @@ def setup_connections(self, create_connection_without_db): extra='{"auth": "jwt", "jwt_token": "fake_jwt_token", "site_id": ""}', ) ) + create_connection_without_db( + models.Connection( + conn_id="tableau_test_with_schema", + conn_type="tableau", + host="tableau", + schema="https", + login="user", + password="password", + extra='{"site_id": "my_site"}', + ) + ) @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") @patch("airflow.providers.tableau.hooks.tableau.Server") @@ -327,6 +338,26 @@ def test_get_conn_ssl_bool_param(self, mock_server, mock_tableau_auth): mock_server.return_value.auth.sign_in.assert_called_once_with(mock_tableau_auth.return_value) mock_server.return_value.auth.sign_out.assert_called_once_with() + @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") + @patch("airflow.providers.tableau.hooks.tableau.Server") + def test_get_conn_uses_schema_when_configured(self, mock_server, mock_tableau_auth): + """ + Test that Server is constructed with the schema prepended to the host when schema is configured. + """ + with TableauHook(tableau_conn_id="tableau_test_with_schema") as tableau_hook: + mock_server.assert_called_once_with(f"{tableau_hook.conn.schema}://{tableau_hook.conn.host}") + mock_server.return_value.auth.sign_out.assert_called_once_with() + + @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") + @patch("airflow.providers.tableau.hooks.tableau.Server") + def test_get_conn_uses_host_only_without_schema(self, mock_server, mock_tableau_auth): + """ + Test that Server is constructed with the host only when no schema is configured (backward compatibility). + """ + with TableauHook(tableau_conn_id="tableau_test_password") as tableau_hook: + mock_server.assert_called_once_with(tableau_hook.conn.host) + mock_server.return_value.auth.sign_out.assert_called_once_with() + @patch("airflow.providers.tableau.hooks.tableau.TableauAuth") @patch("airflow.providers.tableau.hooks.tableau.Server") @patch("airflow.providers.tableau.hooks.tableau.Pager", return_value=[1, 2, 3]) From df2e2b831b4a9f2c13c109fa609579d20fffac39 Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Wed, 11 Mar 2026 03:38:31 +0800 Subject: [PATCH 068/595] =?UTF-8?q?fix=20Inconsistent=20XCom=20Return=20Ty?= =?UTF-8?q?pe=20in=20Mapped=20Task=20Groups=20with=20Dynamic=20=E2=80=A6?= =?UTF-8?q?=20(#59104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix Inconsistent XCom Return Type in Mapped Task Groups with Dynamic Task Mapping * add unit test for xcom_pull * remove asc func --- .../src/airflow/models/taskinstance.py | 10 ++- .../tests/unit/models/test_taskinstance.py | 65 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 8112d955582fd..32daf41aec3e7 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -1683,6 +1683,7 @@ def xcom_pull( ) -> Any: """:meta private:""" # noqa: D400 # This is only kept for compatibility in tests for now while AIP-72 is in progress. + if dag_id is None: dag_id = self.dag_id if run_id is None: @@ -1714,12 +1715,15 @@ def xcom_pull( ).first() if first is None: # No matching XCom at all. return default + if map_indexes is not None or first.map_index < 0: return XComModel.deserialize_value(first) - # raise RuntimeError("Nothing should hit this anymore") - - # TODO: TaskSDK: We should remove this, but many tests still currently call `ti.run()`. See #45549 + return LazyXComSelectSequence.from_select( + query.with_only_columns(XComModel.value).order_by(None), + order_by=[XComModel.map_index.expression], + session=session, + ) # At this point either task_ids or map_indexes is explicitly multi-value. # Order return values to match task_ids and map_indexes ordering. diff --git a/airflow-core/tests/unit/models/test_taskinstance.py b/airflow-core/tests/unit/models/test_taskinstance.py index a013b09cdb0c8..82b9adc3162ae 100644 --- a/airflow-core/tests/unit/models/test_taskinstance.py +++ b/airflow-core/tests/unit/models/test_taskinstance.py @@ -2885,6 +2885,71 @@ def cmds(): out_lines = [line.strip() for line in f] assert out_lines == ["hello FOO", "goodbye FOO", "hello BAR", "goodbye BAR"] + def test_xcom_pull_unmapped_task(self, dag_maker, session): + """ + Test that xcom_pull from unmapped task returns single deserialized value. + + For unmapped tasks with map_index < 0, xcom_pull should return the single value, + not a LazyXComSelectSequence. + """ + + with dag_maker(dag_id="test_xcom_unmapped", session=session): + upstream = PythonOperator( + task_id="unmapped_task", + python_callable=lambda: {"key": "value"}, + ) + downstream = PythonOperator( + task_id="downstream", + python_callable=lambda: None, + ) + upstream >> downstream + + dag_run = dag_maker.create_dagrun(logical_date=timezone.utcnow()) + + # Run upstream task to push xcom + dag_maker.run_ti("unmapped_task", dag_run=dag_run, session=session) + + # Get downstream task instance + ti_downstream = dag_run.get_task_instance("downstream", session=session) + ti_downstream.task = dag_maker.dag.task_dict["downstream"] + + # Pull xcom - should return single dict value, not LazyXComSelectSequence + result = ti_downstream.xcom_pull(task_ids="unmapped_task", session=session) + assert isinstance(result, dict), f"Expected dict for unmapped task, got {type(result)}" + assert result == {"key": "value"} + + def test_xcom_pull_returns_lazy_sequence_for_mapped_xcom(self, dag_maker, session): + """ + Test that xcom_pull returns LazyXComSelectSequence when XComs are mapped (map_index >= 0) + and map_indexes is not specified. + """ + from airflow.models.xcom import LazyXComSelectSequence + + with dag_maker(dag_id="test_xcom_mapped_values", session=session): + + @task + def push_values(val): + return val + + upstream = push_values.expand(val=[2, 4]) + downstream = PythonOperator( + task_id="downstream", + python_callable=lambda: None, + ) + upstream >> downstream + + dag_run = dag_maker.create_dagrun(logical_date=timezone.utcnow()) + dag_maker.run_ti(upstream.operator.task_id, map_index=0, dag_run=dag_run, session=session) + dag_maker.run_ti(upstream.operator.task_id, map_index=1, dag_run=dag_run, session=session) + + ti_downstream = dag_run.get_task_instance("downstream", session=session) + ti_downstream.task = dag_maker.dag.task_dict["downstream"] + + result = ti_downstream.xcom_pull(task_ids=upstream.operator.task_id, session=session) + assert isinstance(result, LazyXComSelectSequence), ( + f"Expected LazyXComSelectSequence for mapped XComs, got {type(result)}" + ) + def _get_lazy_xcom_access_expected_sql_lines() -> list[str]: backend = os.environ.get("BACKEND") From e538b13e16afa6c19dd5c642834fa89ae44b904c Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:42:04 -0700 Subject: [PATCH 069/595] fix(providers/google): wrap sync get_job with sync_to_async in BigQueryAsyncHook (#63230) BigQueryAsyncHook._get_job() calls sync_hook.get_job() directly, blocking the event loop in triggers. This is a regression from #56363 which removed the async wrapping but left the sync call in place. Wrap with asgiref.sync_to_async to run the blocking call in a thread. Fixes apache/airflow#63182 --- .../providers/google/cloud/hooks/bigquery.py | 3 ++- .../unit/google/cloud/hooks/test_bigquery.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py b/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py index 1ee60b524ee19..c5b395b2fab34 100644 --- a/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py +++ b/providers/google/src/airflow/providers/google/cloud/hooks/bigquery.py @@ -33,6 +33,7 @@ import pendulum from aiohttp import ClientSession as ClientSession +from asgiref.sync import sync_to_async from gcloud.aio.bigquery import Job, Table as Table_async from google.cloud.bigquery import ( DEFAULT_RETRY, @@ -2089,7 +2090,7 @@ async def _get_job( ) -> BigQueryJob | UnknownJob: """Get BigQuery job by its ID, project ID and location.""" sync_hook = await self.get_sync_hook() - job = sync_hook.get_job(job_id=job_id, project_id=project_id, location=location) + job = await sync_to_async(sync_hook.get_job)(job_id=job_id, project_id=project_id, location=location) return job async def get_job_status( diff --git a/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py b/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py index bbd4f64bf4649..6a6fa8b3f3b3c 100644 --- a/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py +++ b/providers/google/tests/unit/google/cloud/hooks/test_bigquery.py @@ -1548,6 +1548,23 @@ async def test_get_job_instance(self, mock_session, mock_auth_default): result = await hook.get_job_instance(project_id=PROJECT_ID, job_id=JOB_ID, session=mock_session) assert isinstance(result, Job) + @pytest.mark.asyncio + @mock.patch("airflow.providers.google.cloud.hooks.bigquery.sync_to_async") + @mock.patch("airflow.providers.google.cloud.hooks.bigquery.BigQueryAsyncHook.get_sync_hook") + async def test_get_job_runs_via_sync_to_async(self, mock_get_sync_hook, mock_sync_to_async): + """Verify _get_job wraps the sync get_job call with sync_to_async (#63182).""" + mock_sync_hook = mock.MagicMock() + mock_get_sync_hook.return_value = mock_sync_hook + + mock_async_get_job = mock.AsyncMock(return_value=mock.MagicMock()) + mock_sync_to_async.return_value = mock_async_get_job + + hook = BigQueryAsyncHook() + await hook._get_job(job_id=JOB_ID, project_id=PROJECT_ID, location="US") + + mock_sync_to_async.assert_called_once_with(mock_sync_hook.get_job) + mock_async_get_job.assert_awaited_once_with(job_id=JOB_ID, project_id=PROJECT_ID, location="US") + @pytest.mark.parametrize( ("job_state", "error_result", "expected"), [ From 7152b2a7f2e08ba3c477d3fc3632dc82976db78c Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Wed, 11 Mar 2026 03:59:44 +0800 Subject: [PATCH 070/595] Improve the log file template for ExecuteCallback by including dag_id and run_id in the path (#62616) --- airflow-core/src/airflow/executors/workloads/callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/executors/workloads/callback.py b/airflow-core/src/airflow/executors/workloads/callback.py index 2563f9a78f553..273c55953675b 100644 --- a/airflow-core/src/airflow/executors/workloads/callback.py +++ b/airflow-core/src/airflow/executors/workloads/callback.py @@ -90,7 +90,7 @@ def make( name=dag_run.dag_model.bundle_name, version=dag_run.bundle_version, ) - fname = f"executor_callbacks/{callback.id}" # TODO: better log file template + fname = f"executor_callbacks/{dag_run.dag_id}/{dag_run.run_id}/{callback.id}" return cls( callback=CallbackDTO.model_validate(callback, from_attributes=True), From b48104e7ae680c8f7a489cea7a99ad29134d8260 Mon Sep 17 00:00:00 2001 From: Yoann <60654707+YoannAbriel@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:08:14 -0700 Subject: [PATCH 071/595] fix: warn about hardcoded 24h visibility_timeout that kills long-running Celery tasks (#62869) * fix: warn about hardcoded 24h visibility_timeout that kills long-running Celery tasks Add a warning log when the default visibility_timeout of 86400 seconds (24 hours) is applied for Redis/SQS brokers, so users with long-running tasks know to increase it. Fix misleading task_acks_late documentation that incorrectly claimed it overrides visibility_timeout (it does not for Redis/SQS brokers). Fixes apache/airflow#62218 * fix: fix test assertion for visibility_timeout type (int vs string) * fix: apply ruff format to test file * fix: update get_provider_info.py to match provider.yaml description * ci: retrigger CI (unrelated infra failures) --- providers/celery/provider.yaml | 12 +++-- .../celery/executors/default_celery.py | 7 +++ .../providers/celery/get_provider_info.py | 4 +- .../celery/executors/test_celery_executor.py | 51 +++++++++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/providers/celery/provider.yaml b/providers/celery/provider.yaml index 448d0fc369088..836a31b72a47e 100644 --- a/providers/celery/provider.yaml +++ b/providers/celery/provider.yaml @@ -310,10 +310,14 @@ config: instance then runs concurrently with the original task and the Airflow UI and logs only show an error message: 'Task Instance Not Running' FAILED: Task is in the running state' - Setting task_acks_late to True will force Celery to wait until a task is finished before a - new task instance is assigned. This effectively overrides the visibility timeout. + Setting task_acks_late to True acknowledges the task only after it completes. + Note: for Redis and SQS brokers, task_acks_late does NOT override visibility_timeout. + The broker will still redeliver tasks that exceed visibility_timeout regardless of this setting. + For long-running tasks, you must also increase [celery_broker_transport_options] visibility_timeout. + The default visibility_timeout is 86400 seconds (24 hours). See also: https://docs.celeryq.dev/en/stable/reference/celery.app.task.html#celery.app.task.Task.acks_late + https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout version_added: 3.6.0 type: boolean example: "True" @@ -368,8 +372,10 @@ config: description: | The visibility timeout defines the number of seconds to wait for the worker to acknowledge the task before the message is redelivered to another worker. + If not set, Airflow defaults to 86400 seconds (24 hours) for Redis and SQS brokers. + Tasks running longer than this value will be terminated and redelivered. Make sure to increase the visibility timeout to match the time of the longest - ETA you're planning to use. + task you're planning to run. visibility_timeout is only supported for Redis and SQS celery brokers. See: https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout diff --git a/providers/celery/src/airflow/providers/celery/executors/default_celery.py b/providers/celery/src/airflow/providers/celery/executors/default_celery.py index 50b6c7851277d..c11b7e9194899 100644 --- a/providers/celery/src/airflow/providers/celery/executors/default_celery.py +++ b/providers/celery/src/airflow/providers/celery/executors/default_celery.py @@ -58,6 +58,13 @@ def get_default_celery_config(team_conf) -> dict[str, Any]: if "visibility_timeout" not in broker_transport_options: if _broker_supports_visibility_timeout(broker_url): broker_transport_options["visibility_timeout"] = 86400 + log.warning( + "No visibility_timeout configured in [celery_broker_transport_options]. " + "Using default of 86400 seconds (24 hours). Celery tasks running longer than this " + "will be redelivered by the broker, which terminates the original task. " + "If you have long-running tasks, increase this value in your Airflow configuration: " + "[celery_broker_transport_options] visibility_timeout = " + ) if "sentinel_kwargs" in broker_transport_options: try: diff --git a/providers/celery/src/airflow/providers/celery/get_provider_info.py b/providers/celery/src/airflow/providers/celery/get_provider_info.py index 48097457f5d81..537344c7a4e66 100644 --- a/providers/celery/src/airflow/providers/celery/get_provider_info.py +++ b/providers/celery/src/airflow/providers/celery/get_provider_info.py @@ -205,7 +205,7 @@ def get_provider_info(): "default": "1.0", }, "task_acks_late": { - "description": "If an Airflow task's execution time exceeds the visibility_timeout, Celery will re-assign the\ntask to a Celery worker, even if the original task is still running successfully. The new task\ninstance then runs concurrently with the original task and the Airflow UI and logs only show an\nerror message:\n'Task Instance Not Running' FAILED: Task is in the running state'\nSetting task_acks_late to True will force Celery to wait until a task is finished before a\nnew task instance is assigned. This effectively overrides the visibility timeout.\nSee also:\nhttps://docs.celeryq.dev/en/stable/reference/celery.app.task.html#celery.app.task.Task.acks_late\n", + "description": "If an Airflow task's execution time exceeds the visibility_timeout, Celery will re-assign the\ntask to a Celery worker, even if the original task is still running successfully. The new task\ninstance then runs concurrently with the original task and the Airflow UI and logs only show an\nerror message:\n'Task Instance Not Running' FAILED: Task is in the running state'\nSetting task_acks_late to True acknowledges the task only after it completes.\nNote: for Redis and SQS brokers, task_acks_late does NOT override visibility_timeout.\nThe broker will still redeliver tasks that exceed visibility_timeout regardless of this setting.\nFor long-running tasks, you must also increase [celery_broker_transport_options] visibility_timeout.\nThe default visibility_timeout is 86400 seconds (24 hours).\nSee also:\nhttps://docs.celeryq.dev/en/stable/reference/celery.app.task.html#celery.app.task.Task.acks_late\nhttps://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout\n", "version_added": "3.6.0", "type": "boolean", "example": "True", @@ -245,7 +245,7 @@ def get_provider_info(): "description": "This section is for specifying options which can be passed to the\nunderlying celery broker transport. See:\nhttps://docs.celeryq.dev/en/latest/userguide/configuration.html#std:setting-broker_transport_options\n", "options": { "visibility_timeout": { - "description": "The visibility timeout defines the number of seconds to wait for the worker\nto acknowledge the task before the message is redelivered to another worker.\nMake sure to increase the visibility timeout to match the time of the longest\nETA you're planning to use.\nvisibility_timeout is only supported for Redis and SQS celery brokers.\nSee:\nhttps://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout\n", + "description": "The visibility timeout defines the number of seconds to wait for the worker\nto acknowledge the task before the message is redelivered to another worker.\nIf not set, Airflow defaults to 86400 seconds (24 hours) for Redis and SQS brokers.\nTasks running longer than this value will be terminated and redelivered.\nMake sure to increase the visibility timeout to match the time of the longest\ntask you're planning to run.\nvisibility_timeout is only supported for Redis and SQS celery brokers.\nSee:\nhttps://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout\n", "version_added": None, "type": "string", "example": "21600", diff --git a/providers/celery/tests/unit/celery/executors/test_celery_executor.py b/providers/celery/tests/unit/celery/executors/test_celery_executor.py index 1a7bef2523e8e..f5d34fb29162d 100644 --- a/providers/celery/tests/unit/celery/executors/test_celery_executor.py +++ b/providers/celery/tests/unit/celery/executors/test_celery_executor.py @@ -467,6 +467,57 @@ def test_celery_task_acks_late_loaded_from_string(): assert default_celery.DEFAULT_CELERY_CONFIG["task_acks_late"] is False +@conf_vars({("celery", "BROKER_URL"): "redis://localhost:6379/0"}) +def test_visibility_timeout_default_warns_when_not_configured(caplog): + """Test that a warning is logged when visibility_timeout defaults to 86400 (24h).""" + import importlib + + from airflow.providers.celery.executors.default_celery import log + + with caplog.at_level(logging.WARNING, logger=log.name): + importlib.reload(default_celery) + assert default_celery.DEFAULT_CELERY_CONFIG["broker_transport_options"]["visibility_timeout"] == 86400 + assert "No visibility_timeout configured" in caplog.text + assert "86400" in caplog.text + assert "long-running tasks" in caplog.text + + +@conf_vars( + { + ("celery", "BROKER_URL"): "redis://localhost:6379/0", + ("celery_broker_transport_options", "visibility_timeout"): "172800", + } +) +def test_visibility_timeout_no_warning_when_configured(caplog): + """Test that no warning is logged when visibility_timeout is explicitly configured.""" + import importlib + + from airflow.providers.celery.executors.default_celery import log + + with caplog.at_level(logging.WARNING, logger=log.name): + importlib.reload(default_celery) + assert ( + int(default_celery.DEFAULT_CELERY_CONFIG["broker_transport_options"]["visibility_timeout"]) + == 172800 + ) + assert "No visibility_timeout configured" not in caplog.text + + +@conf_vars({("celery", "BROKER_URL"): "amqp://guest:guest@localhost:5672//"}) +def test_visibility_timeout_not_set_for_unsupported_broker(caplog): + """Test that visibility_timeout is not set for brokers that don't support it (e.g. RabbitMQ).""" + import importlib + + from airflow.providers.celery.executors.default_celery import log + + with caplog.at_level(logging.WARNING, logger=log.name): + importlib.reload(default_celery) + assert "visibility_timeout" not in default_celery.DEFAULT_CELERY_CONFIG.get( + "broker_transport_options", {} + ) + assert "No visibility_timeout configured" not in caplog.text + + @conf_vars({("celery", "extra_celery_config"): '{"worker_max_tasks_per_child": 10}'}) def test_celery_extra_celery_config_loaded_from_string(): import importlib From 7eee2f0ae88f21342e35dcd07e3cf67f5c59f9d5 Mon Sep 17 00:00:00 2001 From: Subham Date: Wed, 11 Mar 2026 01:43:02 +0530 Subject: [PATCH 072/595] Pass timeout to defer() in MSGraphSensor (#62157) (#62241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR ensures that the timeout parameter is correctly passed to the defer() method in MSGraphSensor. Previously, timeout was not propagated, which could lead to the sensor waiting indefinitely when deferring. Related issues/PRs: • Closes #62157 • Supersedes #62241 Notes: • Improves reliability of deferrable sensors by respecting the configured timeout. --- .../microsoft/azure/sensors/msgraph.py | 4 +++- .../microsoft/azure/sensors/test_msgraph.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py index 040ccac0bb568..103fb1d1b451f 100644 --- a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py +++ b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/msgraph.py @@ -18,6 +18,7 @@ from __future__ import annotations from collections.abc import Callable, Sequence +from datetime import timedelta from typing import TYPE_CHECKING, Any from airflow.providers.common.compat.sdk import AirflowException, BaseSensorOperator @@ -27,7 +28,6 @@ from airflow.providers.microsoft.azure.triggers.msgraph import MSGraphTrigger, ResponseSerializer if TYPE_CHECKING: - from datetime import timedelta from io import BytesIO from msgraph_core import APIVersion @@ -183,9 +183,11 @@ def execute_complete( return result + # Re-defer with timeout so Airflow enforces the sensor timeout natively self.defer( trigger=TimeDeltaTrigger(self.retry_delay), method_name=self.retry_execute.__name__, + timeout=timedelta(seconds=self.timeout) if self.timeout is not None else None, ) return None diff --git a/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py b/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py index f3df0e882ae0b..66f439ba72266 100644 --- a/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py +++ b/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_msgraph.py @@ -17,8 +17,9 @@ from __future__ import annotations import json -from datetime import datetime +from datetime import datetime, timedelta from os.path import dirname +from unittest.mock import patch import pytest @@ -141,3 +142,18 @@ def test_template_fields(self): for template_field in MSGraphSensor.template_fields: getattr(sensor, template_field) + + def test_execute_complete_passes_timeout_to_defer(self): + sensor = MSGraphSensor( + task_id="check_timeout", + conn_id="powerbi", + url="myorg/admin/workspaces/scanStatus/{scanId}", + timeout=10, + ) + + with patch.object(sensor, "defer") as mock_defer: + sensor.execute_complete( + context={}, event={"status": "success", "response": json.dumps({"status": "running"})} + ) + mock_defer.assert_called_once() + assert mock_defer.call_args.kwargs["timeout"] == timedelta(seconds=10) From 4a8a803d678b1b3126b51af974c3bc8e51044ce6 Mon Sep 17 00:00:00 2001 From: SameerMesiah97 <75502260+SameerMesiah97@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:19:52 +0000 Subject: [PATCH 073/595] Refactor non-deferrable execution flow to ensure cluster state reconciliation runs after creation completes. (#61951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – Extract reconciliation logic into `_reconcile_cluster_state()` – Ensure DELETING state waits for deletion and re-creates the cluster – Ensure CREATING state is fully reconciled before returning – Handle STOPPED state via restart path – Raise explicit exception if cluster is not found after LRO completion – Return reconciled cluster to avoid stale state Update and extend unit tests to cover reconciliation scenarios in the non-deferrable path (CREATING, DELETING, STOPPED, ERROR, and timeout cases). Co-authored-by: Sameer Mesiah --- .../google/cloud/operators/dataproc.py | 134 +++++++------ .../google/cloud/operators/test_dataproc.py | 180 +++++++++++++++++- 2 files changed, 258 insertions(+), 56 deletions(-) diff --git a/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py b/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py index 7b08af16a98d5..6dc6e0ab77529 100644 --- a/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py +++ b/providers/google/src/airflow/providers/google/cloud/operators/dataproc.py @@ -811,11 +811,47 @@ def _retry_cluster_creation(self, hook: DataprocHook): self.log.info("Cluster created.") return Cluster.to_dict(cluster) + def _reconcile_cluster_state(self, hook: DataprocHook, cluster: Cluster) -> Cluster: + + if cluster.status.state == cluster.status.State.CREATING: + self.log.info("Cluster %s is in CREATING state.", self.cluster_name) + + cluster = self._wait_for_cluster_in_creating_state(hook) + self._handle_error_state(hook, cluster) + elif cluster.status.state == cluster.status.State.DELETING: + self.log.info("Cluster %s is in DELETING state.", self.cluster_name) + + self._wait_for_cluster_in_deleting_state(hook) + + self.log.info("Attempting to re-create cluster: %s", self.cluster_name) + + operation = self._create_cluster(hook) + hook.wait_for_operation( + timeout=self.timeout, + result_retry=self.retry, + operation=operation, + ) + cluster = self._get_cluster(hook) + + self._handle_error_state(hook, cluster) + elif cluster.status.state == cluster.status.State.STOPPED: + self.log.info("Cluster %s is in STOPPED state.", self.cluster_name) + + self.log.info("Attempting to re-start cluster: %s", self.cluster_name) + + # _start_cluster waits for the operation to complete. + self._start_cluster(hook) + + cluster = self._get_cluster(hook) + + return cluster + def execute(self, context: Context) -> dict: - self.log.info("Creating cluster: %s", self.cluster_name) + + self.log.info("Attempting to create cluster: %s", self.cluster_name) hook = DataprocHook(gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain) - # Save data required to display extra link no matter what the cluster status will be + # Save data required to display extra link regardless of the cluster status. project_id = self.project_id or hook.project_id if project_id: DataprocClusterLink.persist( @@ -826,37 +862,40 @@ def execute(self, context: Context) -> dict: ) try: - # First try to create a new cluster operation = self._create_cluster(hook) - if not self.deferrable and type(operation) is not str: - cluster = hook.wait_for_operation( - timeout=self.timeout, result_retry=self.retry, operation=operation + + if not self.deferrable and not isinstance(operation, str): + hook.wait_for_operation( + timeout=self.timeout, + result_retry=self.retry, + operation=operation, ) - self.log.info("Cluster created.") - return Cluster.to_dict(cluster) - cluster = hook.get_cluster( - project_id=self.project_id, region=self.region, cluster_name=self.cluster_name - ) - if cluster.status.state == cluster.status.State.RUNNING: - self.log.info("Cluster created.") - return Cluster.to_dict(cluster) - self.defer( - trigger=DataprocClusterTrigger( - cluster_name=self.cluster_name, - project_id=self.project_id, - region=self.region, - gcp_conn_id=self.gcp_conn_id, - impersonation_chain=self.impersonation_chain, - polling_interval_seconds=self.polling_interval_seconds, - delete_on_error=self.delete_on_error, - ), - method_name="execute_complete", - ) + + # Fetch current state. + cluster = self._get_cluster(hook) + + if self.deferrable: + if cluster.status.state != cluster.status.State.RUNNING: + self.defer( + trigger=DataprocClusterTrigger( + cluster_name=self.cluster_name, + project_id=self.project_id, + region=self.region, + gcp_conn_id=self.gcp_conn_id, + impersonation_chain=self.impersonation_chain, + polling_interval_seconds=self.polling_interval_seconds, + delete_on_error=self.delete_on_error, + ), + method_name="execute_complete", + ) + return + except AlreadyExists: if not self.use_if_exists: raise - self.log.info("Cluster already exists.") + self.log.info("Cluster %s already exists.", self.cluster_name) cluster = self._get_cluster(hook) + except DataprocResourceIsNotReadyError as resource_not_ready_error: if self.num_retries_if_resource_is_not_ready: attempt = self.num_retries_if_resource_is_not_ready @@ -876,39 +915,28 @@ def execute(self, context: Context) -> dict: self._delete_cluster(hook) self._wait_for_cluster_in_deleting_state(hook) raise resource_not_ready_error - except AirflowException as ae: - # There still could be a cluster created here in an ERROR state which - # should be deleted immediately rather than consuming another retry attempt - # (assuming delete_on_error is true (default)) - # This reduces overall the number of task attempts from 3 to 2 to successful cluster creation - # assuming the underlying GCE issues have resolved within that window. Users can configure - # a higher number of retry attempts in powers of two with 30s-60s wait interval + + except AirflowException as outer_airflow_exception: + # A cluster may have been created but entered ERROR state. + # If delete_on_error is enabled, delete it immediately so that + # the next retry attempt starts from a clean state. try: cluster = self._get_cluster(hook) self._handle_error_state(hook, cluster) - except AirflowException as ae_inner: - # We could get any number of failures here, including cluster not found and we - # can just ignore to ensure we surface the original cluster create failure - self.log.exception(ae_inner) + except AirflowException as inner_airflow_exception: + # Cleanup logic may raise secondary exceptions (e.g., cluster not found). + # Suppress those so that the original cluster creation failure is surfaced. + self.log.exception(inner_airflow_exception) finally: - raise ae + raise outer_airflow_exception - # Check if cluster is not in ERROR state + # Check if cluster is not in ERROR state. self._handle_error_state(hook, cluster) - if cluster.status.state == cluster.status.State.CREATING: - # Wait for cluster to be created - cluster = self._wait_for_cluster_in_creating_state(hook) - self._handle_error_state(hook, cluster) - elif cluster.status.state == cluster.status.State.DELETING: - # Wait for cluster to be deleted - self._wait_for_cluster_in_deleting_state(hook) - # Create new cluster - cluster = self._create_cluster(hook) - self._handle_error_state(hook, cluster) - elif cluster.status.state == cluster.status.State.STOPPED: - # if the cluster exists and already stopped, then start the cluster - self._start_cluster(hook) + # If cluster is not in RUNNING state, reconcile. + cluster = self._reconcile_cluster_state(hook, cluster) + + self.log.info("Cluster %s is RUNNING.", self.cluster_name) return Cluster.to_dict(cluster) def execute_complete(self, context: Context, event: dict[str, Any]) -> Any: diff --git a/providers/google/tests/unit/google/cloud/operators/test_dataproc.py b/providers/google/tests/unit/google/cloud/operators/test_dataproc.py index 3db5f497e4db5..d284c8755488a 100644 --- a/providers/google/tests/unit/google/cloud/operators/test_dataproc.py +++ b/providers/google/tests/unit/google/cloud/operators/test_dataproc.py @@ -826,7 +826,7 @@ def test_execute(self, mock_hook, to_dict_mock): # Test whether xcom push occurs before create cluster is called self.extra_links_manager_mock.assert_has_calls(expected_calls, any_order=False) - to_dict_mock.assert_called_once_with(mock_hook().wait_for_operation()) + to_dict_mock.assert_called_once_with(mock_hook.return_value.get_cluster.return_value) if AIRFLOW_V_3_0_PLUS: self.mock_ti.xcom_push.assert_called_once_with( key="dataproc_cluster", @@ -881,7 +881,7 @@ def test_execute_in_gke(self, mock_hook, to_dict_mock): # Test whether xcom push occurs before create cluster is called self.extra_links_manager_mock.assert_has_calls(expected_calls, any_order=False) - to_dict_mock.assert_called_once_with(mock_hook().wait_for_operation()) + to_dict_mock.assert_called_once_with(mock_hook.return_value.get_cluster.return_value) if AIRFLOW_V_3_0_PLUS: self.mock_ti.xcom_push.assert_called_once_with( key="dataproc_cluster", @@ -1015,7 +1015,7 @@ def test_execute_if_cluster_exists_in_deleting_state( mock_create_cluster.side_effect = [AlreadyExists("test"), cluster_running] mock_generator.return_value = [0] - mock_get_cluster.side_effect = [cluster_deleting, NotFound("test")] + mock_get_cluster.side_effect = [cluster_deleting, NotFound("test"), cluster_running] op = DataprocCreateClusterOperator( task_id=TASK_ID, @@ -1035,6 +1035,180 @@ def test_execute_if_cluster_exists_in_deleting_state( to_dict_mock.assert_called_once_with(cluster_running) + @mock.patch(DATAPROC_PATH.format("Cluster.to_dict")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._wait_for_cluster_in_deleting_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_recreates_when_deleted_during_creation( + self, + mock_hook, + mock_get_cluster, + mock_wait_for_deleting, + to_dict_mock, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + # First invocation of get_cluster should return cluster in DELETING state. + cluster_deleting = mock.MagicMock() + cluster_deleting.status.state = cluster_deleting.status.State.DELETING + + # Re-creation should return cluster in RUNNING state. + cluster_running = mock.MagicMock() + cluster_running.status.state = cluster_running.status.State.RUNNING + + mock_get_cluster.side_effect = [ + cluster_deleting, + cluster_running, + ] + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + op.execute(context=mock.MagicMock()) + + # Ensure re-creation path is traversed. + assert mock_wait_for_deleting.called + assert mock_hook.return_value.create_cluster.call_count == 2 + + to_dict_mock.assert_called_once_with(cluster_running) + + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._wait_for_cluster_in_deleting_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_deleting_timeout_raises( + self, + mock_hook, + mock_get_cluster, + mock_wait_for_deleting, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_deleting = mock.MagicMock() + cluster_deleting.status.state = cluster_deleting.status.State.DELETING + + mock_get_cluster.return_value = cluster_deleting + mock_wait_for_deleting.side_effect = AirflowException("Timeout") + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + with pytest.raises(AirflowException): + op.execute(context=mock.MagicMock()) + + # Ensure no re-creation is attempted. + assert mock_hook.return_value.create_cluster.call_count == 1 + + @mock.patch(DATAPROC_PATH.format("Cluster.to_dict")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._wait_for_cluster_in_creating_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_waits_when_still_creating( + self, + mock_hook, + mock_get_cluster, + mock_wait_for_creating, + to_dict_mock, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_creating = mock.MagicMock() + cluster_creating.status.state = cluster_creating.status.State.CREATING + + cluster_running = mock.MagicMock() + cluster_running.status.state = cluster_running.status.State.RUNNING + + mock_get_cluster.return_value = cluster_creating + mock_wait_for_creating.return_value = cluster_running + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + op.execute(context=mock.MagicMock()) + + mock_wait_for_creating.assert_called_once() + to_dict_mock.assert_called_once_with(cluster_running) + + @mock.patch(DATAPROC_PATH.format("Cluster.to_dict")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._start_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_stopped_cluster_restarts( + self, + mock_hook, + mock_get_cluster, + mock_start_cluster, + to_dict_mock, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_stopped = mock.MagicMock() + cluster_stopped.status.state = cluster_stopped.status.State.STOPPED + + mock_get_cluster.return_value = cluster_stopped + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + op.execute(context=mock.MagicMock()) + + mock_start_cluster.assert_called_once_with(mock_hook.return_value) + to_dict_mock.assert_called_once_with(cluster_stopped) + + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._handle_error_state")) + @mock.patch(DATAPROC_PATH.format("DataprocCreateClusterOperator._get_cluster")) + @mock.patch(DATAPROC_PATH.format("DataprocHook")) + def test_execute_error_state_after_wait_for_completion( + self, + mock_hook, + mock_get_cluster, + mock_handle_error, + ): + mock_hook.return_value.wait_for_operation.return_value = None + + cluster_error = mock.MagicMock() + cluster_error.status.state = cluster_error.status.State.ERROR + + mock_get_cluster.return_value = cluster_error + mock_handle_error.side_effect = AirflowException("Cluster error") + + op = DataprocCreateClusterOperator( + task_id=TASK_ID, + region=GCP_REGION, + project_id=GCP_PROJECT, + cluster_name=CLUSTER_NAME, + cluster_config=CONFIG, + deferrable=False, + ) + + with pytest.raises(AirflowException): + op.execute(context=mock.MagicMock()) + + mock_handle_error.assert_called_once() + @mock.patch(DATAPROC_PATH.format("DataprocHook")) @mock.patch(DATAPROC_TRIGGERS_PATH.format("DataprocAsyncHook")) def test_create_execute_call_defer_method(self, mock_trigger_hook, mock_hook): From a9c0bf31a8fdd081561c48e9fe28d2c79ee43edc Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Tue, 10 Mar 2026 20:38:02 +0000 Subject: [PATCH 074/595] Enable parallel backfill by eliminating shared state between providers (#63288) Add --provider and --providers-json flags to extract_parameters.py and extract_connections.py so each backfill run uses an isolated temp providers.json and only scans the target provider. In --provider mode, modules.json is not written (it would be incomplete), so concurrent runs don't clobber each other. The backfill command now creates a TemporaryDirectory with per-version providers.json files instead of patching a shared file. --- dev/breeze/doc/11_registry_tasks.rst | 11 ++ dev/breeze/doc/images/output_registry.svg | 18 ++- dev/breeze/doc/images/output_registry.txt | 2 +- .../doc/images/output_registry_backfill.svg | 38 +++-- .../doc/images/output_registry_backfill.txt | 2 +- .../commands/registry_commands.py | 136 ++++++++-------- dev/breeze/tests/test_registry_backfill.py | 145 ++++++++++-------- dev/registry/extract_connections.py | 34 +++- dev/registry/extract_parameters.py | 93 +++++++---- 9 files changed, 288 insertions(+), 191 deletions(-) diff --git a/dev/breeze/doc/11_registry_tasks.rst b/dev/breeze/doc/11_registry_tasks.rst index 6b01d9065dcd6..9be90b576b3ae 100644 --- a/dev/breeze/doc/11_registry_tasks.rst +++ b/dev/breeze/doc/11_registry_tasks.rst @@ -79,6 +79,17 @@ Example usage: # Backfill a hyphenated provider breeze registry backfill --provider microsoft-azure --version 11.0.0 +Each run uses an isolated temporary ``providers.json``, so different providers +can be backfilled in parallel from separate terminal sessions: + +.. code-block:: bash + + # Terminal 1 + breeze registry backfill --provider amazon --version 9.15.0 --version 9.14.0 + + # Terminal 2 (safe to run simultaneously) + breeze registry backfill --provider google --version 14.0.0 --version 13.0.0 + Output is written to ``registry/src/_data/versions/{provider}/{version}/``: - ``parameters.json`` — operator/sensor/hook parameters diff --git a/dev/breeze/doc/images/output_registry.svg b/dev/breeze/doc/images/output_registry.svg index e4b4f92c4f861..951851010e777 100644 --- a/dev/breeze/doc/images/output_registry.svg +++ b/dev/breeze/doc/images/output_registry.svg @@ -1,4 +1,4 @@ - + extract-data    Extract provider metadata, parameters, and connection types for the registry.                      backfill        Extract runtime parameters and connections for older provider versions. Uses 'uv run --with' to    install the specific version in a temporary environment and runs extract_parameters.py +           -extract_connections.py. No Docker needed.                                                          -publish-versionsPublish per-provider versions.json to S3 from deployed directories. Same pattern as 'breeze        -release-management publish-docs-to-s3'.                                                            -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +extract_connections.py. No Docker needed. Each version uses an isolated providers.json, so         +multiple providers can be backfilled in parallel from separate terminal sessions.                  +publish-versionsPublish per-provider versions.json to S3 from deployed directories. Same pattern as 'breeze        +release-management publish-docs-to-s3'.                                                            +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/dev/breeze/doc/images/output_registry.txt b/dev/breeze/doc/images/output_registry.txt index dae5504430be8..fadd741e8b654 100644 --- a/dev/breeze/doc/images/output_registry.txt +++ b/dev/breeze/doc/images/output_registry.txt @@ -1 +1 @@ -297843509448a55e7941eed3c0485df8 +8c9be6264d33af7facd1fbdf435697b7 diff --git a/dev/breeze/doc/images/output_registry_backfill.svg b/dev/breeze/doc/images/output_registry_backfill.svg index 12b49bb040261..4478565366e98 100644 --- a/dev/breeze/doc/images/output_registry_backfill.svg +++ b/dev/breeze/doc/images/output_registry_backfill.svg @@ -1,4 +1,4 @@ - +

{{ provider.description }}

- {% set counts = provider.module_counts or {} %} - {% set totalMods = (counts.operator or 0) + (counts.hook or 0) + (counts.sensor or 0) + (counts.trigger or 0) + (counts.transfer or 0) + (counts.notifier or 0) + (counts.secret or 0) + (counts.logging or 0) + (counts.executor or 0) + (counts.bundle or 0) + (counts.decorator or 0) %} + {% set counts = moduleCountsByProvider[provider.id] or {} %} + {% set totalMods = 0 %} + {% for t in types %}{% set totalMods = totalMods + (counts[t.id] or 0) %}{% endfor %} {{ totalMods }} module{{ "s" if totalMods != 1 }} {% if totalMods > 0 %}
- {% for mtype in ["operator", "hook", "sensor", "trigger", "transfer", "executor", "notifier", "secret", "logging", "bundle", "decorator"] %} - {% if counts[mtype] %} -
+ {% for t in types %} + {% if counts[t.id] %} +
{% endif %} {% endfor %}
diff --git a/registry/src/css/main.css b/registry/src/css/main.css index 11dfe1f06a333..e602fd7f1b0e9 100644 --- a/registry/src/css/main.css +++ b/registry/src/css/main.css @@ -2723,150 +2723,6 @@ main { } /* Stats */ -.provider-detail-page header .stats { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-3); - margin-bottom: var(--space-6); -} - -@media (min-width: 640px) { - .provider-detail-page header .stats { - grid-template-columns: repeat(3, 1fr); - } -} - -@media (min-width: 1024px) { - .provider-detail-page header .stats { - grid-template-columns: repeat(6, 1fr); - } -} - -.provider-detail-page header .stat { - background: rgb(from var(--bg-secondary) r g b / 0.5); - border-radius: var(--radius-lg); - padding: var(--space-3); -} - -.provider-detail-page header .stat-row { - display: flex; - align-items: center; - gap: var(--space-2); - margin-bottom: var(--space-1); -} - -.provider-detail-page header .stat.operator { - border: 1px solid rgb(from var(--color-operator) r g b / 0.2); -} - -.provider-detail-page header .stat.hook { - border: 1px solid rgb(from var(--color-hook) r g b / 0.2); -} - -.provider-detail-page header .stat.sensor { - border: 1px solid rgb(from var(--color-sensor) r g b / 0.2); -} - -.provider-detail-page header .stat.trigger { - border: 1px solid rgb(from var(--color-trigger) r g b / 0.2); -} - -.provider-detail-page header .stat.transfer { - border: 1px solid rgb(from var(--color-transfer) r g b / 0.2); -} - -.provider-detail-page header .stat.bundle { - border: 1px solid rgb(from var(--color-bundle) r g b / 0.2); -} - -.provider-detail-page header .stat.total { - border: 1px solid rgb(from var(--accent-primary) r g b / 0.2); -} - -.provider-detail-page header .stat .icon { - width: 1.5rem; - height: 1.5rem; - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-xs); - font-weight: var(--font-bold); -} - -.provider-detail-page header .stat.operator .icon { - background: rgb(from var(--color-operator) r g b / 0.2); - color: var(--color-operator); -} - -.provider-detail-page header .stat.hook .icon { - background: rgb(from var(--color-hook) r g b / 0.2); - color: var(--color-hook); -} - -.provider-detail-page header .stat.sensor .icon { - background: rgb(from var(--color-sensor) r g b / 0.2); - color: var(--color-sensor); -} - -.provider-detail-page header .stat.trigger .icon { - background: rgb(from var(--color-trigger) r g b / 0.2); - color: var(--color-trigger); -} - -.provider-detail-page header .stat.transfer .icon { - background: rgb(from var(--color-transfer) r g b / 0.2); - color: var(--color-transfer); -} - -.provider-detail-page header .stat.bundle .icon { - background: rgb(from var(--color-bundle) r g b / 0.2); - color: var(--color-bundle); -} - -.provider-detail-page header .stat.total .icon { - background: rgb(from var(--accent-primary) r g b / 0.2); - color: var(--accent-primary); -} - -.provider-detail-page header .stat .count { - font-size: var(--text-xl); - font-weight: var(--font-bold); -} - -.provider-detail-page header .stat.operator .count { - color: var(--color-operator); -} - -.provider-detail-page header .stat.hook .count { - color: var(--color-hook); -} - -.provider-detail-page header .stat.sensor .count { - color: var(--color-sensor); -} - -.provider-detail-page header .stat.trigger .count { - color: var(--color-trigger); -} - -.provider-detail-page header .stat.transfer .count { - color: var(--color-transfer); -} - -.provider-detail-page header .stat.bundle .count { - color: var(--color-bundle); -} - -.provider-detail-page header .stat.total .count { - color: var(--accent-primary); -} - -.provider-detail-page header .stat .label { - font-size: var(--text-xs); - color: var(--text-secondary); -} - /* Header Footer */ .provider-detail-page header .footer { display: flex; diff --git a/registry/src/js/search.js b/registry/src/js/search.js index 76416338d14cb..d7b13d523d636 100644 --- a/registry/src/js/search.js +++ b/registry/src/js/search.js @@ -24,19 +24,18 @@ let currentResults = []; let searchId = 0; - const typeLabels = { - operator: 'Operator', - hook: 'Hook', - sensor: 'Sensor', - trigger: 'Trigger', - transfer: 'Transfer', - bundle: 'Bundle', - notifier: 'Notifier', - secret: 'Secrets Backend', - logging: 'Log Handler', - executor: 'Executor', - decorator: 'Decorator', - }; + // Type labels loaded from types.json (injected via base.njk) + const typeLabels = {}; + try { + const typesEl = document.getElementById('types-data'); + if (typesEl) { + for (const t of JSON.parse(typesEl.textContent)) { + typeLabels[t.id] = t.label; + } + } + } catch (_) { + // Fallback: empty object — badges will show raw type name + } function escapeHtml(str) { const div = document.createElement('div'); diff --git a/registry/src/provider-version.njk b/registry/src/provider-version.njk index 790b19172ba5a..51179078bab49 100644 --- a/registry/src/provider-version.njk +++ b/registry/src/provider-version.njk @@ -14,7 +14,7 @@ eleventyComputed: {# Choose data source: latest uses providers.json + modules.json, older uses versionData #} {% if pv.isLatest %} - {% set moduleCounts = pv.provider.module_counts or {} %} + {% set moduleCounts = moduleCountsByProvider[pv.provider.id] or {} %} {% set deps = pv.provider.dependencies or [] %} {% set extras = pv.provider.optional_extras or {} %} {% set conns = pv.provider.connection_types or [] %} @@ -35,7 +35,8 @@ eleventyComputed: {% set sourceUrl = pv.provider.source_url %} {% endif %} -{% set totalModules = (moduleCounts.operator or 0) + (moduleCounts.hook or 0) + (moduleCounts.sensor or 0) + (moduleCounts.trigger or 0) + (moduleCounts.transfer or 0) + (moduleCounts.bundle or 0) + (moduleCounts.notifier or 0) + (moduleCounts.secret or 0) + (moduleCounts.logging or 0) + (moduleCounts.executor or 0) + (moduleCounts.decorator or 0) %} +{% set totalModules = 0 %} +{% for t in types %}{% set totalModules = totalModules + (moduleCounts[t.id] or 0) %}{% endfor %}
{# Breadcrumb #} @@ -94,77 +95,16 @@ eleventyComputed:
- {# Stats Grid #} -
- {% if moduleCounts.operator > 0 %} -
-
- O - {{ moduleCounts.operator }} -
- Operators -
- {% endif %} - {% if moduleCounts.hook > 0 %} -
-
- H - {{ moduleCounts.hook }} -
- Hooks -
- {% endif %} - {% if moduleCounts.sensor > 0 %} -
-
- S - {{ moduleCounts.sensor }} -
- Sensors -
- {% endif %} - {% if moduleCounts.trigger > 0 %} -
-
- T - {{ moduleCounts.trigger }} -
- Triggers -
- {% endif %} - {% if moduleCounts.transfer > 0 %} -
-
- X - {{ moduleCounts.transfer }} -
- Transfers -
- {% endif %} - {% if moduleCounts.bundle > 0 %} -
-
- B - {{ moduleCounts.bundle }} -
- Bundles -
- {% endif %} -
-
- Σ - {{ totalModules }} -
- Total Modules -
-
- - {# Bottom row: Downloads and actions #} + {# Bottom row: Downloads, module count, and actions #}