This is an automated email from the ASF dual-hosted git repository.

dabla pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 70a78162261 Page SqlToS3Operator like GenericTransfer (#75730)
70a78162261 is described below

commit 70a781622611f09ec34f0bbc34c4f05c59ad4c67
Author: David Blain <[email protected]>
AuthorDate: Tue Oct 27 18:45:10 2026 +0100

    Page SqlToS3Operator like GenericTransfer (#75730)
    
    * Page SqlToS3Operator like GenericTransfer
    
    A result that does not fit in memory could not be transferred with
    SqlToS3Operator: it reads the whole query into one DataFrame and
    max_rows_per_file only splits that frame afterwards. GenericTransfer
    already pages a source with page_size and paginated_sql_statement_clause;
    this gives SqlToS3Operator the same two parameters, one query per page
    through the clause and one file per page (<key>_0, <key>_1, ...), so one
    page is in memory at a time and the two operators are configured alike.
    
    The per-DataFrame write and upload and the read with its dtype fix move
    out of execute() into methods the paged and the unpaged path share.
    
    * Convert a templated page_size before paging and raise ValueError for 
argument clashes
    
    A templated page_size reaches execute() as a string, and "0" was truthy 
there:
    the operator paged with a page size of zero and never ended. page_size is 
now
    converted after rendering, in render_template_fields() as GenericTransfer 
does,
    so a rendered "0" means unpaged; a negative value is refused.
    
    The exclusion of page_size with max_rows_per_file or groupby_kwargs raises
    ValueError, as the other Amazon transfers do for an argument clash, and 
tests
    the argument with "is not None" so the constructor applies no logic to a
    template field.
    
    The paged path is a generator that yields (suffix, DataFrame) pairs like
    _partition_dataframe(), so execute() runs one loop over either source and a
    page is released before the next one is read.
    
    * Release a page before the next one is read
    
    The generator dropped its reference to a page before reading the next one,
    but the loop variable in execute() still held the previous page until the
    next pair was unpacked, which happens after the read. Two pages were alive 
at
    the peak while the docs promise one. The loop now releases the page right
    after its upload, and a test with weak references checks that no earlier
    page is alive when a page is read.
---
 providers/amazon/docs/transfer/sql_to_s3.rst       |  19 +++
 .../providers/amazon/aws/transfers/sql_to_s3.py    | 130 ++++++++++----
 .../tests/system/amazon/aws/example_sql_to_s3.py   |  13 ++
 .../unit/amazon/aws/transfers/test_sql_to_s3.py    | 188 +++++++++++++++++++++
 4 files changed, 319 insertions(+), 31 deletions(-)

diff --git a/providers/amazon/docs/transfer/sql_to_s3.rst 
b/providers/amazon/docs/transfer/sql_to_s3.rst
index 9aa9db59384..54cccb06314 100644
--- a/providers/amazon/docs/transfer/sql_to_s3.rst
+++ b/providers/amazon/docs/transfer/sql_to_s3.rst
@@ -67,6 +67,25 @@ Example usage:
     :start-after: [START howto_transfer_sql_to_s3_with_groupby_param]
     :end-before: [END howto_transfer_sql_to_s3_with_groupby_param]
 
+Paging
+------
+
+A result that does not fit in memory is read in pages with the ``page_size`` 
param, the way
+``GenericTransfer`` pages a source: one query per page through 
``paginated_sql_statement_clause``
+(``"{} LIMIT {} OFFSET {}"`` by default, formatted with the query, the page 
size and the offset), one file
+per page (``<key>_0``, ``<key>_1``, ...), and only one page in memory at a 
time. The transfer stops at the
+first page that comes back short. On most databases the query needs an ``ORDER 
BY`` for the pages to be
+stable; a hook that serves pages itself, such as one streaming a single 
result, needs none.
+``page_size`` cannot be combined with ``max_rows_per_file`` or 
``groupby_kwargs``.
+
+Example usage:
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_sql_to_s3.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_transfer_sql_to_s3_with_page_size]
+    :end-before: [END howto_transfer_sql_to_s3_with_page_size]
+
 Reference
 ---------
 
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py 
b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py
index 5313aa26c4e..90ab13c0516 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py
@@ -36,6 +36,7 @@ from airflow.providers.common.compat.sdk import (
 )
 
 if TYPE_CHECKING:
+    import jinja2
     import pandas as pd
     import polars as pl
 
@@ -96,6 +97,14 @@ class SqlToS3Operator(BaseOperator):
     :param df_kwargs: arguments to include in DataFrame ``.to_parquet()``, 
``.to_json()`` or ``.to_csv()``,
         or in ``.write_parquet()``, ``.write_json()`` or ``.write_csv()`` when 
``df_type`` is ``'polars'``.
     :param groupby_kwargs: argument to include in DataFrame ``groupby()``.
+    :param page_size: (optional) number of rows per page. The query is then 
paged the way
+        ``GenericTransfer`` pages it, one query per page through 
``paginated_sql_statement_clause``,
+        and every page is written as a file of its own (``<key>_0``, 
``<key>_1``, ...), so only one
+        page is held in memory at a time. Cannot be combined with 
``max_rows_per_file`` or
+        ``groupby_kwargs``.
+    :param paginated_sql_statement_clause: SQL statement clause used for 
pagination, formatted with the
+        query, the page size and the offset (default ``"{} LIMIT {} OFFSET 
{}"``). Only used with
+        ``page_size``.
     """
 
     template_fields: Sequence[str] = (
@@ -104,6 +113,8 @@ class SqlToS3Operator(BaseOperator):
         "query",
         "sql_conn_id",
         "aws_conn_id",
+        "page_size",
+        "paginated_sql_statement_clause",
     )
     template_ext: Sequence[str] = (".sql",)
     template_fields_renderers = {
@@ -133,6 +144,8 @@ class SqlToS3Operator(BaseOperator):
         df_kwargs: dict | None = None,
         pd_kwargs: dict | None = None,
         groupby_kwargs: dict | None = None,
+        page_size: int | None = None,
+        paginated_sql_statement_clause: str | None = None,
         **kwargs,
     ) -> None:
         super().__init__(**kwargs)
@@ -148,6 +161,8 @@ class SqlToS3Operator(BaseOperator):
         self.groupby_kwargs = groupby_kwargs or {}
         self.sql_hook_params = sql_hook_params
         self.df_type = df_type
+        self.page_size = page_size
+        self.paginated_sql_statement_clause = paginated_sql_statement_clause 
or "{} LIMIT {} OFFSET {}"
 
         if read_pd_kwargs is not None:
             warnings.warn(
@@ -175,6 +190,12 @@ class SqlToS3Operator(BaseOperator):
                 "can not be both specified. Please choose one."
             )
 
+        if self.page_size is not None and (self.max_rows_per_file or 
self.groupby_kwargs):
+            raise ValueError(
+                "SqlToS3Operator argument page_size can not be combined with "
+                "max_rows_per_file or groupby_kwargs: every page is already 
written as a file of its own."
+            )
+
         try:
             self.file_format = FILE_FORMAT[file_format.upper()]
         except KeyError:
@@ -244,49 +265,96 @@ class SqlToS3Operator(BaseOperator):
             f"{trigger} needs {' or '.join(engines)}. Install 
apache-airflow-providers-amazon[parquet]."
         )
 
+    def get_paginated_sql(self, offset: int) -> str:
+        """Format the paginated SQL statement using the current format."""
+        return self.paginated_sql_statement_clause.format(self.query, 
self.page_size, offset)
+
+    def render_template_fields(
+        self,
+        context: Context,
+        jinja_env: jinja2.Environment | None = None,
+    ) -> None:
+        super().render_template_fields(context=context, jinja_env=jinja_env)
+        # A templated page_size renders as a string; the paging decision needs 
the number.
+        if self.page_size is not None:
+            self.page_size = int(self.page_size)
+            if self.page_size < 0:
+                raise ValueError(f"SqlToS3Operator argument page_size can not 
be negative: {self.page_size}")
+
     def execute(self, context: Context) -> None:
         self._ensure_parquet_engine()
         sql_hook = self._get_hook()
         s3_conn = S3Hook(aws_conn_id=self.aws_conn_id, verify=self.verify)
-        data_df = sql_hook.get_df(
-            sql=self.query, parameters=self.parameters, df_type=self.df_type, 
**self.read_kwargs
+        pages = (
+            self._pages(sql_hook)
+            if self.page_size
+            else self._partition_dataframe(df=self._read_dataframe(sql_hook, 
self.query))
         )
+        for suffix, df in pages:
+            self._upload_dataframe(s3_conn, df, suffix)
+            del df  # release the page before the next one is read; the loop 
variable would hold it
+
+    def _pages(self, sql_hook: DbApiHook) -> Iterable[tuple[str, pd.DataFrame 
| pl.DataFrame]]:
+        """
+        Read the query one page at a time, each page keyed by its number.
+
+        The first page is always yielded, so the key exists even for an empty 
result; a later empty
+        page only tells that the previous one was the last. A page is released 
before the next is read.
+        """
+        if TYPE_CHECKING:
+            assert self.page_size is not None  # execute() pages only when 
page_size is set
+        offset = 0
+        page = 0
+        while True:
+            paginated_sql = self.get_paginated_sql(offset)
+            self.log.info("Executing: \n %s", paginated_sql)
+            df = self._read_dataframe(sql_hook, paginated_sql)
+            if page == 0 or len(df):
+                yield str(page), df
+            if len(df) < self.page_size:
+                self.log.info("Last page read; ending transfer after %d 
page(s).", page + 1)
+                return
+            del df
+            offset += self.page_size
+            page += 1
+
+    def _read_dataframe(self, sql_hook: DbApiHook, sql: str) -> pd.DataFrame | 
pl.DataFrame:
+        """Run *sql* through the hook's DataFrame API and apply the pandas 
dtype fixes."""
+        df = sql_hook.get_df(sql=sql, parameters=self.parameters, 
df_type=self.df_type, **self.read_kwargs)
         self.log.info("Data from SQL obtained")
         # Only apply dtype fixes to pandas DataFrames since Polars doesn't 
have the same NaN/None inconsistencies as panda
         if ("dtype_backend", "pyarrow") not in self.read_kwargs.items() and 
self.df_type == "pandas":
-            self._fix_dtypes(data_df, self.file_format)  # type: 
ignore[arg-type]
+            self._fix_dtypes(df, self.file_format)  # type: ignore[arg-type]
+        return df
+
+    def _upload_dataframe(self, s3_conn: S3Hook, df: pd.DataFrame | 
pl.DataFrame, suffix: str) -> None:
+        """Write *df* in the configured file format and upload it as 
``<key>_<suffix>``."""
         file_options = FILE_OPTIONS_MAP[self.file_format]
         write_function = file_options.polars_function if self.df_type == 
"polars" else file_options.function
+        buf = io.BytesIO()
+        self.log.info("Writing data to in-memory buffer")
+        clean_key = self._strip_suffixes(self.s3_key)
+        object_key = (
+            f"{clean_key}_{suffix}{file_options.suffix}" if suffix else 
f"{clean_key}{file_options.suffix}"
+        )
 
-        for group_name, df in self._partition_dataframe(df=data_df):
-            buf = io.BytesIO()
-            self.log.info("Writing data to in-memory buffer")
-            clean_key = self._strip_suffixes(self.s3_key)
-            object_key = (
-                f"{clean_key}_{group_name}{file_options.suffix}"
-                if group_name
-                else f"{clean_key}{file_options.suffix}"
-            )
-
-            if self.file_format != FILE_FORMAT.PARQUET and 
self.df_kwargs.get("compression") == "gzip":
-                object_key += ".gz"
-                df_kwargs = {k: v for k, v in self.df_kwargs.items() if k != 
"compression"}
-                with gzip.GzipFile(fileobj=buf, mode="wb", 
filename=object_key) as gz:
-                    getattr(df, write_function)(gz, **df_kwargs)
+        if self.file_format != FILE_FORMAT.PARQUET and 
self.df_kwargs.get("compression") == "gzip":
+            object_key += ".gz"
+            df_kwargs = {k: v for k, v in self.df_kwargs.items() if k != 
"compression"}
+            with gzip.GzipFile(fileobj=buf, mode="wb", filename=object_key) as 
gz:
+                getattr(df, write_function)(gz, **df_kwargs)
+        else:
+            if self.file_format == FILE_FORMAT.PARQUET:
+                getattr(df, write_function)(buf, **self.df_kwargs)
             else:
-                if self.file_format == FILE_FORMAT.PARQUET:
-                    getattr(df, write_function)(buf, **self.df_kwargs)
-                else:
-                    text_buf = io.TextIOWrapper(buf, encoding="utf-8", 
write_through=True)
-                    getattr(df, write_function)(text_buf, **self.df_kwargs)
-                    text_buf.flush()
-
-            buf.seek(0)
-
-            self.log.info("Uploading data to S3")
-            s3_conn.load_file_obj(
-                file_obj=buf, key=object_key, bucket_name=self.s3_bucket, 
replace=self.replace
-            )
+                text_buf = io.TextIOWrapper(buf, encoding="utf-8", 
write_through=True)
+                getattr(df, write_function)(text_buf, **self.df_kwargs)
+                text_buf.flush()
+
+        buf.seek(0)
+
+        self.log.info("Uploading data to S3")
+        s3_conn.load_file_obj(file_obj=buf, key=object_key, 
bucket_name=self.s3_bucket, replace=self.replace)
 
     def _partition_dataframe(
         self, df: pd.DataFrame | pl.DataFrame
diff --git a/providers/amazon/tests/system/amazon/aws/example_sql_to_s3.py 
b/providers/amazon/tests/system/amazon/aws/example_sql_to_s3.py
index 103501e14aa..3205529768c 100644
--- a/providers/amazon/tests/system/amazon/aws/example_sql_to_s3.py
+++ b/providers/amazon/tests/system/amazon/aws/example_sql_to_s3.py
@@ -186,6 +186,18 @@ with DAG(
     )
     # [END howto_transfer_sql_to_s3_with_groupby_param]
 
+    # [START howto_transfer_sql_to_s3_with_page_size]
+    sql_to_s3_task_with_page_size = SqlToS3Operator(
+        task_id="sql_to_s3_with_page_size_task",
+        sql_conn_id=conn_id_name,
+        query=SQL_QUERY,
+        s3_bucket=bucket_name,
+        s3_key=key,
+        replace=True,
+        page_size=2,
+    )
+    # [END howto_transfer_sql_to_s3_with_page_size]
+
     delete_bucket = S3DeleteBucketOperator(
         task_id="delete_bucket",
         bucket_name=bucket_name,
@@ -213,6 +225,7 @@ with DAG(
         wait_cluster_available_before_transfer,
         sql_to_s3_task,
         sql_to_s3_task_with_groupby,
+        sql_to_s3_task_with_page_size,
         # TEST TEARDOWN
         delete_bucket,
         delete_cluster,
diff --git a/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py 
b/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py
index b97ee952b42..81890fdcabc 100644
--- a/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py
+++ b/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 import gzip
 import importlib.util
 import io
+import weakref
 from unittest import mock
 
 import pandas as pd
@@ -31,6 +32,7 @@ from airflow.exceptions import 
AirflowProviderDeprecationWarning
 from airflow.models import Connection
 from airflow.providers.amazon.aws.transfers.sql_to_s3 import SqlToS3Operator
 from airflow.providers.common.compat.sdk import AirflowException, 
AirflowOptionalProviderFeatureException
+from airflow.providers.common.sql.hooks.sql import DbApiHook
 
 PANDAS_3_PLUS = Version(pd.__version__).major >= 3
 
@@ -500,6 +502,192 @@ class TestSqlToS3Operator:
             )
         )
 
+    @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
+    def test_execute_with_page_size_writes_one_file_per_page(self, 
mock_s3_hook):
+        """Paged like GenericTransfer: one query per page, one file per page, 
stop on a short page."""
+        pages = [pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [3, 4]}), 
pd.DataFrame({"a": [5]})]
+        mock_dbapi_hook = mock.Mock(return_value=mock.Mock(spec=DbApiHook))
+        get_df_mock = mock_dbapi_hook.return_value.get_df
+        get_df_mock.side_effect = pages
+        uploaded: dict[str, bytes] = {}
+        # Read the buffer at upload time: the operator's text wrapper closes 
it once the upload is done.
+        mock_s3_hook.return_value.load_file_obj.side_effect = lambda file_obj, 
key, **kwargs: uploaded.update(
+            {key: file_obj.getvalue()}
+        )
+
+        op = SqlToS3Operator(
+            query="SELECT * FROM t",
+            s3_bucket="bucket",
+            s3_key="key.csv",
+            sql_conn_id="mysql_conn_id",
+            aws_conn_id="aws_conn_id",
+            task_id="task_id",
+            replace=True,
+            df_kwargs={"index": False},
+            page_size=2,
+            dag=None,
+        )
+        op._get_hook = mock_dbapi_hook
+        op.execute(None)
+
+        assert [call.kwargs["sql"] for call in get_df_mock.call_args_list] == [
+            "SELECT * FROM t LIMIT 2 OFFSET 0",
+            "SELECT * FROM t LIMIT 2 OFFSET 2",
+            "SELECT * FROM t LIMIT 2 OFFSET 4",
+        ]
+        uploads = mock_s3_hook.return_value.load_file_obj.call_args_list
+        assert list(uploaded) == ["key_0.csv", "key_1.csv", "key_2.csv"]
+        assert all(call.kwargs["replace"] is True for call in uploads)
+        assert uploaded["key_2.csv"] == b"a\n5\n"
+
+    @pytest.mark.parametrize(
+        ("pages", "expected_keys"),
+        [
+            pytest.param(
+                [pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [3, 4]}), 
pd.DataFrame({"a": []})],
+                ["key_0.csv", "key_1.csv"],
+                id="exact-multiple-ends-with-an-empty-page",
+            ),
+            pytest.param(
+                [pd.DataFrame({"a": []})], ["key_0.csv"], 
id="empty-result-still-writes-the-first-page"
+            ),
+        ],
+    )
+    @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
+    def test_execute_with_page_size_empty_pages(self, mock_s3_hook, pages, 
expected_keys):
+        mock_dbapi_hook = mock.Mock(return_value=mock.Mock(spec=DbApiHook))
+        mock_dbapi_hook.return_value.get_df.side_effect = pages
+
+        op = SqlToS3Operator(
+            query="query",
+            s3_bucket="bucket",
+            s3_key="key",
+            sql_conn_id="mysql_conn_id",
+            aws_conn_id="aws_conn_id",
+            task_id="task_id",
+            page_size=2,
+            dag=None,
+        )
+        op._get_hook = mock_dbapi_hook
+        op.execute(None)
+
+        assert mock_dbapi_hook.return_value.get_df.call_count == len(pages)
+        uploads = mock_s3_hook.return_value.load_file_obj.call_args_list
+        assert [call.kwargs["key"] for call in uploads] == expected_keys
+
+    @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
+    def test_execute_with_page_size_uses_the_pagination_clause(self, 
mock_s3_hook):
+        """The clause is formatted with the query, the page size and the 
offset, as GenericTransfer does."""
+        mock_dbapi_hook = mock.Mock(return_value=mock.Mock(spec=DbApiHook))
+        mock_dbapi_hook.return_value.get_df.side_effect = [pd.DataFrame({"a": 
[1]})]
+
+        op = SqlToS3Operator(
+            query="SELECT * FROM t",
+            s3_bucket="bucket",
+            s3_key="key",
+            sql_conn_id="mysql_conn_id",
+            aws_conn_id="aws_conn_id",
+            task_id="task_id",
+            page_size="3",
+            paginated_sql_statement_clause="{0} OFFSET {2} ROWS FETCH NEXT {1} 
ROWS ONLY",
+            dag=None,
+        )
+        op._get_hook = mock_dbapi_hook
+        op.render_template_fields({})
+        op.execute(None)
+
+        mock_dbapi_hook.return_value.get_df.assert_called_once_with(
+            sql="SELECT * FROM t OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY", 
parameters=None, df_type="pandas"
+        )
+
+    @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
+    def 
test_execute_with_page_size_releases_a_page_before_reading_the_next(self, 
mock_s3_hook):
+        """Only one page is in memory at a time: the previous page is gone 
when the next is read."""
+        pages_alive_at_read: list[int] = []
+        page_refs: list[weakref.ref] = []
+
+        def get_df(**kwargs):
+            pages_alive_at_read.append(sum(ref() is not None for ref in 
page_refs))
+            df = pd.DataFrame({"a": [1, 2]}) if len(page_refs) < 2 else 
pd.DataFrame({"a": [1]})
+            page_refs.append(weakref.ref(df))
+            return df
+
+        mock_dbapi_hook = mock.Mock(return_value=mock.Mock(spec=DbApiHook))
+        mock_dbapi_hook.return_value.get_df.side_effect = get_df
+
+        op = SqlToS3Operator(
+            query="query",
+            s3_bucket="bucket",
+            s3_key="key",
+            sql_conn_id="mysql_conn_id",
+            aws_conn_id="aws_conn_id",
+            task_id="task_id",
+            page_size=2,
+            dag=None,
+        )
+        op._get_hook = mock_dbapi_hook
+        op.execute(None)
+
+        assert pages_alive_at_read == [0, 0, 0]
+        assert mock_s3_hook.return_value.load_file_obj.call_count == 3
+
+    @pytest.mark.parametrize(
+        "kwargs",
+        [
+            pytest.param({"max_rows_per_file": 10}, id="max_rows_per_file"),
+            pytest.param({"groupby_kwargs": {"by": "a"}}, id="groupby_kwargs"),
+        ],
+    )
+    def test_page_size_excludes_the_other_ways_to_split_the_output(self, 
kwargs):
+        with pytest.raises(ValueError, match="page_size can not be combined"):
+            SqlToS3Operator(
+                query="query",
+                s3_bucket="bucket",
+                s3_key="key",
+                sql_conn_id="mysql_conn_id",
+                task_id="task_id",
+                page_size=10,
+                **kwargs,
+            )
+
+    @pytest.mark.parametrize(
+        ("page_size", "expected"),
+        [
+            pytest.param("0", 0, id="rendered-zero-means-unpaged"),
+            pytest.param(3, 3, id="int-stays"),
+            pytest.param(None, None, id="unset-stays"),
+        ],
+    )
+    def test_render_template_fields_converts_page_size(self, page_size, 
expected):
+        """A templated page_size renders as a string; "0" must mean unpaged, 
not an endless loop."""
+        op = SqlToS3Operator(
+            query="query",
+            s3_bucket="bucket",
+            s3_key="key",
+            sql_conn_id="mysql_conn_id",
+            task_id="task_id",
+            page_size=page_size,
+            dag=None,
+        )
+
+        op.render_template_fields({})
+
+        assert op.page_size == expected
+
+    def test_render_template_fields_rejects_a_negative_page_size(self):
+        op = SqlToS3Operator(
+            query="query",
+            s3_bucket="bucket",
+            s3_key="key",
+            sql_conn_id="mysql_conn_id",
+            task_id="task_id",
+            page_size="-1",
+            dag=None,
+        )
+
+        with pytest.raises(ValueError, match="page_size can not be negative"):
+            op.render_template_fields({})
+
     
@mock.patch("airflow.providers.common.sql.operators.sql.BaseHook.get_connection")
     def test_hook_params(self, mock_get_conn):
         mock_get_conn.return_value = Connection(conn_id="postgres_test", 
conn_type="postgres")

Reply via email to