Copilot commented on code in PR #56324:
URL: https://github.com/apache/airflow/pull/56324#discussion_r2662090249


##########
providers/google/src/airflow/providers/google/common/hooks/base_google.py:
##########
@@ -265,17 +266,34 @@ def get_ui_field_behaviour(cls) -> dict[str, Any]:
         return {
             "hidden_fields": ["host", "schema", "login", "password", "port", 
"extra"],
             "relabeling": {},
+            "placeholders": {
+                "quota_project_id": "Project ID to use for API quota and 
billing (optional)",
+            },
+            "tooltips": {
+                "quota_project_id": "Specify a different project for 
quota/billing purposes. Useful when using shared service accounts.",
+            },
         }
 
     def __init__(
         self,
         gcp_conn_id: str = "google_cloud_default",
         impersonation_chain: str | Sequence[str] | None = None,
+        quota_project_id: str | None = None,
         **kwargs,
     ) -> None:
+        """Initialize the Google Cloud Base Hook.
+        
+        :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.
+        :param quota_project_id: Optional project ID to use for quota/billing 
purposes.
+            If None, the project ID from the GCP connection is used.

Review Comment:
   The docstring says "If None, the project ID from the GCP connection is 
used." However, this is not entirely accurate. When quota_project_id is None, 
no quota project is configured at all - it doesn't default to using the 
connection's project ID for quota purposes. The default behavior is to not set 
a quota project, which means Google Cloud will use the default project 
associated with the credentials.
   
   Consider clarifying the docstring to: "If None, no separate quota project is 
configured and the default behavior of the credentials is used."
   ```suggestion
               If None, no separate quota project is configured and the default 
behavior of the
               credentials is used.
   ```



##########
providers/google/tests/unit/google/common/hooks/test_base_google.py:
##########
@@ -541,6 +541,78 @@ def 
test_get_credentials_and_project_id_with_default_auth_and_overridden_project
         )
         assert result == ("CREDENTIALS", "SECOND_PROJECT_ID")
 
+    def test_quota_project_id_init(self):
+        """Test that quota project ID is properly initialized."""
+        hook = GoogleBaseHook(gcp_conn_id="google_cloud_default", 
quota_project_id="test-quota-project")
+        assert hook.quota_project_id == "test-quota-project"
+
+    @mock.patch("google.auth._default._load_credentials_from_file")
+    @mock.patch("os.environ", {CREDENTIALS: "/test/key-path"})
+    def test_quota_project_id_from_connection(self, mock_load_creds):
+        """Test that quota project ID from connection is applied to 
credentials."""
+        mock_creds = mock.MagicMock()
+        mock_creds.with_quota_project.return_value = mock_creds
+        mock_load_creds.return_value = (mock_creds, "test-project")
+
+        # Mock connection with quota_project_id in extras
+        uri = 
f"google-cloud-platform://?extras={json.dumps({'quota_project_id': 
'test-quota-project'})}"
+        with mock.patch.dict("os.environ", 
{"AIRFLOW_CONN_GOOGLE_CLOUD_DEFAULT": uri}):
+            hook = GoogleBaseHook(gcp_conn_id="google_cloud_default")
+            creds, _ = hook.get_credentials_and_project_id()
+            
mock_creds.with_quota_project.assert_called_once_with("test-quota-project")
+
+    @mock.patch("google.auth._default._load_credentials_from_file")
+    @mock.patch("os.environ", {CREDENTIALS: "/test/key-path"})
+    def test_quota_project_id_param_overrides_connection(self, 
mock_load_creds):
+        """Test that quota project ID from param overrides connection value."""
+        mock_creds = mock.MagicMock()
+        mock_creds.with_quota_project.return_value = mock_creds
+        mock_load_creds.return_value = (mock_creds, "test-project")
+
+        # Connection with quota_project_id in extras
+        conn_quota = "connection-quota-project"
+        uri = 
f"google-cloud-platform://?extras={json.dumps({'quota_project_id': 
conn_quota})}"
+
+        with mock.patch.dict("os.environ", 
{"AIRFLOW_CONN_GOOGLE_CLOUD_DEFAULT": uri}):
+            hook = GoogleBaseHook(gcp_conn_id="google_cloud_default", 
quota_project_id="test-quota-project")
+            creds, _ = hook.get_credentials_and_project_id()
+
+            # Should use param quota project, not connection quota project
+            
mock_creds.with_quota_project.assert_called_once_with("test-quota-project")
+
+    def test_quota_project_invalid_format(self):
+        """Test validation of quota project ID format."""
+        invalid_ids = [
+            "UPPERCASE",  # Must be lowercase
+            "special@chars",  # Invalid characters
+            "a",  # Too short

Review Comment:
   The test includes "a" (single character) as an invalid ID. While this 
correctly tests that short IDs are rejected, the regex pattern requires at 
minimum 6 characters total (1 start + 4 middle + 1 end). A single "a" would 
fail the pattern, but not because it's "too short" in the sense being tested - 
it fails because it doesn't match the structural requirements.
   
   Consider adding test cases that better represent the boundary conditions:
   - A 5-character valid structure like "ab-cd" (too short, should fail)
   - A 6-character valid structure like "ab-cde" (minimum valid, should pass - 
though this might not be a valid test case if testing invalid IDs)
   
   This would make the test more explicit about what aspects of validation are 
being tested.
   ```suggestion
               "ab-cd",  # Structurally valid but too short
   ```



##########
providers/google/src/airflow/providers/google/common/hooks/base_google.py:
##########
@@ -347,9 +377,51 @@ def get_credentials_and_project_id(self) -> 
tuple[Credentials, str | None]:
 
         return credentials, project_id
 
+    def _validate_quota_project(self, quota_project: str | None) -> None:
+        """Validate the quota project ID format and existence.

Review Comment:
   The docstring says "Validate the quota project ID format and existence" but 
the method only validates the format through a regex check. It does not 
validate the existence of the project (i.e., it doesn't check if the project 
actually exists in Google Cloud). This could be misleading for developers.
   
   Change "and existence" to just "format" in the docstring, or implement 
actual existence checking if that's the intended behavior.
   ```suggestion
           """Validate the quota project ID format.
   ```



##########
providers/google/src/airflow/providers/google/common/hooks/base_google.py:
##########
@@ -347,9 +377,51 @@ def get_credentials_and_project_id(self) -> 
tuple[Credentials, str | None]:
 
         return credentials, project_id
 
+    def _validate_quota_project(self, quota_project: str | None) -> None:
+        """Validate the quota project ID format and existence.
+
+        :param quota_project: The quota project ID to validate
+        :raises AirflowException: If the quota project ID is invalid
+        """
+        if quota_project is not None:
+            if not isinstance(quota_project, str):
+                raise AirflowException(f"quota_project_id must be a string, 
got {type(quota_project)}")
+            if not quota_project.strip():
+                raise AirflowException("quota_project_id cannot be empty")
+            # Check for valid GCP project ID format
+            if not re.match(r"^[a-z][-a-z0-9]{4,28}[a-z0-9]$", quota_project):

Review Comment:
   The regex pattern "^[a-z][-a-z0-9]{4,28}[a-z0-9]$" enforces project IDs to 
be 6-30 characters (1 starting letter + 4-28 middle characters + 1 ending 
character). However, according to Google Cloud documentation, project IDs must 
be between 6 and 30 characters, which means the minimum middle section should 
allow for 4 characters (when you have 1 starting + 4 middle + 1 ending = 6 
total). 
   
   But the pattern allows {4,28} for the middle section, which would mean:
   - Minimum: 1 + 4 + 1 = 6 characters ✓
   - Maximum: 1 + 28 + 1 = 30 characters ✓
   
   This appears correct, but consider adding a comment explaining this 
calculation to make the regex more maintainable and easier to understand for 
future developers.



##########
providers/google/tests/unit/google/common/hooks/test_base_google.py:
##########
@@ -541,6 +541,78 @@ def 
test_get_credentials_and_project_id_with_default_auth_and_overridden_project
         )
         assert result == ("CREDENTIALS", "SECOND_PROJECT_ID")
 
+    def test_quota_project_id_init(self):
+        """Test that quota project ID is properly initialized."""
+        hook = GoogleBaseHook(gcp_conn_id="google_cloud_default", 
quota_project_id="test-quota-project")
+        assert hook.quota_project_id == "test-quota-project"
+
+    @mock.patch("google.auth._default._load_credentials_from_file")
+    @mock.patch("os.environ", {CREDENTIALS: "/test/key-path"})
+    def test_quota_project_id_from_connection(self, mock_load_creds):
+        """Test that quota project ID from connection is applied to 
credentials."""
+        mock_creds = mock.MagicMock()
+        mock_creds.with_quota_project.return_value = mock_creds
+        mock_load_creds.return_value = (mock_creds, "test-project")
+
+        # Mock connection with quota_project_id in extras
+        uri = 
f"google-cloud-platform://?extras={json.dumps({'quota_project_id': 
'test-quota-project'})}"
+        with mock.patch.dict("os.environ", 
{"AIRFLOW_CONN_GOOGLE_CLOUD_DEFAULT": uri}):
+            hook = GoogleBaseHook(gcp_conn_id="google_cloud_default")
+            creds, _ = hook.get_credentials_and_project_id()
+            
mock_creds.with_quota_project.assert_called_once_with("test-quota-project")
+
+    @mock.patch("google.auth._default._load_credentials_from_file")
+    @mock.patch("os.environ", {CREDENTIALS: "/test/key-path"})
+    def test_quota_project_id_param_overrides_connection(self, 
mock_load_creds):
+        """Test that quota project ID from param overrides connection value."""
+        mock_creds = mock.MagicMock()
+        mock_creds.with_quota_project.return_value = mock_creds
+        mock_load_creds.return_value = (mock_creds, "test-project")
+
+        # Connection with quota_project_id in extras
+        conn_quota = "connection-quota-project"
+        uri = 
f"google-cloud-platform://?extras={json.dumps({'quota_project_id': 
conn_quota})}"
+
+        with mock.patch.dict("os.environ", 
{"AIRFLOW_CONN_GOOGLE_CLOUD_DEFAULT": uri}):
+            hook = GoogleBaseHook(gcp_conn_id="google_cloud_default", 
quota_project_id="test-quota-project")
+            creds, _ = hook.get_credentials_and_project_id()
+
+            # Should use param quota project, not connection quota project
+            
mock_creds.with_quota_project.assert_called_once_with("test-quota-project")
+
+    def test_quota_project_invalid_format(self):
+        """Test validation of quota project ID format."""
+        invalid_ids = [
+            "UPPERCASE",  # Must be lowercase
+            "special@chars",  # Invalid characters
+            "a",  # Too short
+            "a" * 31,  # Too long

Review Comment:
   The test includes "a" * 31 (31 characters) as an invalid ID in the test 
list. However, this value would not match the regex pattern 
"^[a-z][-a-z0-9]{4,28}[a-z0-9]$" because it's a string of all 'a' characters 
with no hyphens or digits, meaning it doesn't satisfy the pattern structure 
which requires specific character types in different positions.
   
   The test should use a more realistic invalid ID that would be 31 characters 
but still matches the pattern structure, such as "a" + "-" * 28 + "a" (which 
would be 30 characters with valid structure) or better yet "a" + "b" * 29 (31 
lowercase letters). This would properly test the length validation aspect of 
the regex.
   ```suggestion
               "a" + "b" * 30,  # Too long (31 characters, structurally valid 
but exceeds max length)
   ```



##########
providers/google/docs/connections/gcp.rst:
##########
@@ -309,3 +328,95 @@ Note that as domain-wide delegation is currently supported 
by most of the Google
 * All of Google Cloud operators and hooks.
 * Firebase hooks.
 * All transfer operators that involve Google cloud in different providers, for 
example: 
:class:`airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator`.
+
+
+Quota Project Support
+---------------------
+
+Airflow's Google Cloud providers support specifying a "quota project" (a 
billing project) for
+API calls. That lets API usage be billed to a different Google Cloud project 
than the one that
+owns the service account. This is useful for organizations that share service 
accounts but
+centralize billing in specific projects.
+
+Usage
+~~~~~
+
+There are two ways to set a quota project in Airflow:
+
+- Via connection extras (recommended for environment-wide defaults).
+- Directly on operators or hooks (recommended when a single task must bill to 
a different project).
+
+Connection extras
+^^^^^^^^^^^^^^^^^
+
+Add the quota project ID to the Google Cloud connection extras. For example:
+
+.. code-block:: json
+
+  {
+    "quota_project_id": "your-billing-project-id"
+  }
+
+You can set this via the Airflow UI, the Connections REST API, or an 
environment variable, for
+example:
+
+.. code-block:: bash
+
+  export AIRFLOW_CONN_GOOGLE_CLOUD_DEFAULT='{
+    "conn_type": "google-cloud-platform",
+    "extra": {
+      "quota_project_id": "your-billing-project-id"
+    }
+  }'
+
+Operator / Hook parameter
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You can also pass the quota project directly when creating an operator or 
hook. This takes
+precedence over the connection extras:
+
+.. code-block:: python
+
+  from airflow.providers.google.cloud.operators.bigquery import 
BigQueryExecuteQueryOperator
+
+  task = BigQueryExecuteQueryOperator(
+    task_id='execute_query',
+    sql='SELECT * FROM `my_project.dataset.table`',
+    quota_project_id='your-billing-project-id'
+  )
+
+  # Or when creating a hook
+  from airflow.providers.google.cloud.hooks.bigquery import BigQueryHook
+
+  hook = BigQueryHook(quota_project_id='your-billing-project-id')
+
+Priority
+~~~~~~~
+
+If a quota project is provided both in the connection extras and as an 
operator/hook
+parameter, the operator/hook parameter wins.
+
+Compatibility
+~~~~~~~~~~~~~
+
+This setting works with Google Cloud services that support the quota project 
mechanism (the
+`x-goog-user-project` header), for example:
+
+- BigQuery
+- Cloud Storage
+- Dataflow
+- Other Google Cloud APIs that accept quota project headers
+
+Impact
+~~~~~~
+
+Using a quota project affects where API usage is billed, which quotas are 
applied, and how
+usage is reported for monitoring and auditing.
+
+Examples
+~~~~~~~~
+
+**Examples**
+
+See the example DAG: 
``airflow/providers/google/cloud/example_dags/example_quota_project.py``

Review Comment:
   The file path reference 
"airflow/providers/google/cloud/example_dags/example_quota_project.py" is 
incorrect. Based on the actual file structure, the example is located at 
"tests/system/providers/google/common/example_quota_project_system.py". This 
will cause confusion for users trying to find the example.
   
   Update the path to the correct location where the example file exists.
   ```suggestion
   See the example DAG: 
``tests/system/providers/google/common/example_quota_project_system.py``
   ```



##########
providers/google/src/airflow/providers/google/common/hooks/base_google.py:
##########
@@ -338,6 +356,18 @@ def get_credentials_and_project_id(self) -> 
tuple[Credentials, str | None]:
             idp_extra_params_dict=idp_extra_params_dict,
         )
 
+        # Support for quota project ID
+        quota_project = self.quota_project_id or 
self._get_field("quota_project_id")
+        if quota_project:
+            self._validate_quota_project(quota_project)
+            try:
+                credentials = credentials.with_quota_project(quota_project)
+            except Exception as e:
+                raise AirflowException(
+                    f"Failed to configure quota project '{quota_project}': 
{str(e)}. "
+                    "Ensure the service account has permission to use the 
quota project."
+                ) from e

Review Comment:
   The get_credentials_and_project_id() method does not check for anonymous 
credentials before applying the quota project (lines 359-369), while 
get_credentials() does have this check (line 408). This inconsistency means 
that if anonymous credentials are used, get_credentials_and_project_id() might 
attempt to call with_quota_project() on anonymous credentials, which could fail 
or behave unexpectedly.
   
   For consistency and correctness, add a check for is_anonymous before 
applying the quota project in get_credentials_and_project_id(), similar to the 
check in get_credentials(). Alternatively, if the duplicate logic in 
get_credentials() is removed as suggested in another comment, ensure the check 
for anonymous credentials is retained in get_credentials_and_project_id().



##########
providers/google/src/airflow/providers/google/common/hooks/base_google.py:
##########
@@ -347,9 +377,51 @@ def get_credentials_and_project_id(self) -> 
tuple[Credentials, str | None]:
 
         return credentials, project_id
 
+    def _validate_quota_project(self, quota_project: str | None) -> None:
+        """Validate the quota project ID format and existence.
+
+        :param quota_project: The quota project ID to validate
+        :raises AirflowException: If the quota project ID is invalid
+        """
+        if quota_project is not None:
+            if not isinstance(quota_project, str):
+                raise AirflowException(f"quota_project_id must be a string, 
got {type(quota_project)}")
+            if not quota_project.strip():
+                raise AirflowException("quota_project_id cannot be empty")
+            # Check for valid GCP project ID format
+            if not re.match(r"^[a-z][-a-z0-9]{4,28}[a-z0-9]$", quota_project):
+                raise AirflowException(
+                    f"Invalid quota_project_id '{quota_project}'. "
+                    "Project IDs must start with a lowercase letter and can 
contain "
+                    "only lowercase letters, digits, and hyphens."

Review Comment:
   The validation error message states "Project IDs must start with a lowercase 
letter and can contain only lowercase letters, digits, and hyphens." However, 
this doesn't mention the length requirements (6-30 characters) which are also 
enforced by the regex. This could leave users confused about why their project 
ID is rejected if it has valid characters but incorrect length.
   
   Update the error message to include the length requirement: "Project IDs 
must be 6-30 characters long, start with a lowercase letter, and can contain 
only lowercase letters, digits, and hyphens."
   ```suggestion
                       "Project IDs must be 6-30 characters long, start with a 
lowercase letter, and can "
                       "contain only lowercase letters, digits, and hyphens."
   ```



##########
providers/google/docs/connections/gcp.rst:
##########
@@ -121,6 +121,25 @@ Scopes (comma separated)
     <https://developers.google.com/identity/protocols/googlescopes>`_ to
     authenticate with.
 
+
+Quota Project ID (optional)
+---------------------------
+
+The Google Cloud project ID to use for API quota and billing purposes. This is 
useful
+when using a shared service account but want to attribute quota/billing to a 
different
+project. If not specified, the default project from the connection is used. 
Must be a
+valid GCP project ID (lowercase letters, digits, hyphens, 6–30 characters, 
starting
+with a letter).

Review Comment:
   The documentation states "If not specified, the default project from the 
connection is used." This is misleading. When quota_project_id is not 
specified, no quota project is set on the credentials, which means Google Cloud 
uses the default behavior of the credentials (typically the project that owns 
the service account). It does not automatically use the "project" field from 
the connection configuration as the quota project.
   
   Clarify the documentation to explain that when not specified, no separate 
quota project is configured and Google Cloud's default behavior applies.
   ```suggestion
   project. If not specified, no separate quota project is configured on the 
credentials and
   Google Cloud's default behavior applies (typically the project that owns the 
service
   account/credentials); it is not automatically derived from the connection's 
``Project Id`` field.
   ```



##########
providers/google/src/airflow/providers/google/common/hooks/base_google.py:
##########
@@ -347,9 +377,51 @@ def get_credentials_and_project_id(self) -> 
tuple[Credentials, str | None]:
 
         return credentials, project_id
 
+    def _validate_quota_project(self, quota_project: str | None) -> None:
+        """Validate the quota project ID format and existence.
+
+        :param quota_project: The quota project ID to validate
+        :raises AirflowException: If the quota project ID is invalid
+        """
+        if quota_project is not None:
+            if not isinstance(quota_project, str):
+                raise AirflowException(f"quota_project_id must be a string, 
got {type(quota_project)}")
+            if not quota_project.strip():
+                raise AirflowException("quota_project_id cannot be empty")
+            # Check for valid GCP project ID format
+            if not re.match(r"^[a-z][-a-z0-9]{4,28}[a-z0-9]$", quota_project):
+                raise AirflowException(
+                    f"Invalid quota_project_id '{quota_project}'. "
+                    "Project IDs must start with a lowercase letter and can 
contain "
+                    "only lowercase letters, digits, and hyphens."
+                )
+
     def get_credentials(self) -> Credentials:
-        """Return the Credentials object for Google API."""
+        """Return the Credentials object for Google API.
+        
+        :return: Google Cloud credentials object
+        :raises AirflowException: If credentials cannot be configured with 
quota project
+        """
         credentials, _ = self.get_credentials_and_project_id()
+        
+        # If credentials are anonymous, skip quota project logic
+        is_anonymous = self._get_field("is_anonymous")
+        quota_project = self.quota_project_id or 
self._get_field("quota_project_id")
+        if quota_project and not is_anonymous:
+            self._validate_quota_project(quota_project)
+            if not hasattr(credentials, "with_quota_project"):
+                raise AirflowException(
+                    f"Credentials of type {type(credentials).__name__} do not 
support "
+                    "quota project configuration. Please use a different 
authentication method "
+                    "or remove the quota_project_id setting."
+                )
+            try:
+                credentials = credentials.with_quota_project(quota_project)
+            except Exception as e:
+                raise AirflowException(
+                    f"Failed to configure quota project '{quota_project}': 
{str(e)}"
+                ) from e
+        

Review Comment:
   This method duplicates the quota project logic that already exists in 
get_credentials_and_project_id() (lines 359-369). The get_credentials() method 
calls get_credentials_and_project_id() which already applies the quota project 
to credentials. This means the quota project configuration is attempted twice, 
which is redundant and could lead to confusion during debugging.
   
   Consider removing the duplicate quota project logic from get_credentials() 
since get_credentials_and_project_id() already handles this configuration. The 
get_credentials() method can simply return the credentials obtained from 
get_credentials_and_project_id().
   ```suggestion
   
           :return: Google Cloud credentials object
           """
           credentials, _ = self.get_credentials_and_project_id()
   ```



##########
tests/system/providers/google/common/example_quota_project_system.py:
##########
@@ -0,0 +1,112 @@
+# 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 demonstrates Google Cloud quota project functionality.
+This DAG shows how to use quota project settings with Google Cloud connections
+and operators.
+"""
+from __future__ import annotations
+
+import os
+from datetime import datetime
+
+from airflow import DAG
+from airflow.operators.python import PythonOperator
+from airflow.providers.google.cloud.hooks.bigquery import BigQueryHook
+from airflow.utils.email import send_email
+
+ENV_ID = os.environ.get("SYSTEM_TESTS_ENV_ID", "default")
+DAG_ID = "example_quota_project"
+
+def test_quota_project(gcp_conn_id: str = "google_cloud_default", 
quota_project_id: str | None = None, **context):
+    """Test quota project configuration with Google Cloud credentials."""
+    hook = BigQueryHook(gcp_conn_id=gcp_conn_id, 
quota_project_id=quota_project_id)
+    credentials, project_id = hook.get_credentials_and_project_id()
+    quota_project = getattr(credentials, "_quota_project_id", None)
+    
+    hook.log.info("Testing Google Cloud quota project configuration:")
+    hook.log.info("Credentials project ID: %s", project_id)
+    hook.log.info("Quota project ID: %s", quota_project)
+    
+    return {
+        "project_id": project_id,
+        "quota_project_id": quota_project,
+    }
+
+with DAG(
+    DAG_ID,
+    schedule="@once",
+    start_date=datetime(2021, 1, 1),
+    catchup=False,
+    tags=["example", "quota-project"],
+) as dag:
+    # [START howto_quota_project_from_connection]
+    # This task uses quota project from connection if configured
+    test_conn_quota = PythonOperator(
+        task_id="test_connection_quota",
+        python_callable=test_quota_project,
+        op_kwargs={"gcp_conn_id": "google_cloud_default"},  # Assumes 
quota_project_id in connection extras
+    )
+    # [END howto_quota_project_from_connection]
+
+    # [START howto_quota_project_from_param]
+    # This task explicitly sets quota project
+    test_param_quota = PythonOperator(
+        task_id="test_parameter_quota",
+        python_callable=test_quota_project,
+        op_kwargs={
+            "gcp_conn_id": "google_cloud_default",
+            "quota_project_id": "explicit-quota-project",
+        },
+    )
+    # [END howto_quota_project_from_param]
+
+    # Task dependencies
+    test_conn_quota >> test_param_quota
+
+# [START howto_quota_project_email_on_failure]
+def _handle_quota_failure(context):
+    """Send an email if quota project test fails."""
+    dag_id = context["task_instance"].dag_id
+    task_id = context["task_instance"].task_id
+    logs_url = context["task_instance"].log_url
+    
+    send_email(
+        to="[email protected]",
+        subject=f"[Airflow] Quota Project Test Failed - {dag_id}.{task_id}",
+        html_content=f"""
+        Quota project test failed.<br/>
+        DAG: {dag_id}<br/>
+        Task: {task_id}<br/>
+        Logs: <a href="{logs_url}">Link</a>
+        """,
+    )
+# [END howto_quota_project_email_on_failure]
+
+for task in dag.tasks:
+    task.on_failure_callback = _handle_quota_failure

Review Comment:
   The send_email import and the _handle_quota_failure function with email 
functionality are included in this system test example. However, this 
introduces unnecessary complexity and dependencies (email configuration) for 
what should be a simple demonstration of quota project functionality. System 
test examples should focus on demonstrating the feature being tested, not 
generic failure handling patterns.
   
   Consider removing the send_email import and the email failure callback 
section (lines 81-101) to keep the example focused on the quota project feature 
itself. If failure handling examples are needed, they should be in separate, 
dedicated example files.



##########
providers/google/tests/unit/google/common/hooks/test_base_google.py:
##########
@@ -541,6 +541,78 @@ def 
test_get_credentials_and_project_id_with_default_auth_and_overridden_project
         )
         assert result == ("CREDENTIALS", "SECOND_PROJECT_ID")
 
+    def test_quota_project_id_init(self):
+        """Test that quota project ID is properly initialized."""
+        hook = GoogleBaseHook(gcp_conn_id="google_cloud_default", 
quota_project_id="test-quota-project")
+        assert hook.quota_project_id == "test-quota-project"
+
+    @mock.patch("google.auth._default._load_credentials_from_file")
+    @mock.patch("os.environ", {CREDENTIALS: "/test/key-path"})
+    def test_quota_project_id_from_connection(self, mock_load_creds):
+        """Test that quota project ID from connection is applied to 
credentials."""
+        mock_creds = mock.MagicMock()
+        mock_creds.with_quota_project.return_value = mock_creds
+        mock_load_creds.return_value = (mock_creds, "test-project")
+
+        # Mock connection with quota_project_id in extras
+        uri = 
f"google-cloud-platform://?extras={json.dumps({'quota_project_id': 
'test-quota-project'})}"
+        with mock.patch.dict("os.environ", 
{"AIRFLOW_CONN_GOOGLE_CLOUD_DEFAULT": uri}):
+            hook = GoogleBaseHook(gcp_conn_id="google_cloud_default")
+            creds, _ = hook.get_credentials_and_project_id()
+            
mock_creds.with_quota_project.assert_called_once_with("test-quota-project")
+
+    @mock.patch("google.auth._default._load_credentials_from_file")
+    @mock.patch("os.environ", {CREDENTIALS: "/test/key-path"})
+    def test_quota_project_id_param_overrides_connection(self, 
mock_load_creds):
+        """Test that quota project ID from param overrides connection value."""
+        mock_creds = mock.MagicMock()
+        mock_creds.with_quota_project.return_value = mock_creds
+        mock_load_creds.return_value = (mock_creds, "test-project")
+
+        # Connection with quota_project_id in extras
+        conn_quota = "connection-quota-project"
+        uri = 
f"google-cloud-platform://?extras={json.dumps({'quota_project_id': 
conn_quota})}"
+
+        with mock.patch.dict("os.environ", 
{"AIRFLOW_CONN_GOOGLE_CLOUD_DEFAULT": uri}):
+            hook = GoogleBaseHook(gcp_conn_id="google_cloud_default", 
quota_project_id="test-quota-project")
+            creds, _ = hook.get_credentials_and_project_id()
+
+            # Should use param quota project, not connection quota project
+            
mock_creds.with_quota_project.assert_called_once_with("test-quota-project")
+
+    def test_quota_project_invalid_format(self):
+        """Test validation of quota project ID format."""
+        invalid_ids = [
+            "UPPERCASE",  # Must be lowercase
+            "special@chars",  # Invalid characters
+            "a",  # Too short
+            "a" * 31,  # Too long
+            "1starts-with-number",  # Must start with letter
+            "",  # Empty string
+            None,  # None value

Review Comment:
   The validation logic checks if quota_project is not None at line 386, but 
then the test list includes None as an invalid value (line 592). This creates 
an inconsistency: the validation method will not raise an exception for None 
(it returns early), but the test expects an exception. 
   
   If None should be treated as invalid, the validation method should raise an 
exception for None values. If None is acceptable (which makes more sense since 
it means "no quota project"), then None should be removed from the invalid_ids 
test list.
   ```suggestion
   
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to