Skip to content

Commit 8d8e1a6

Browse files
I've added type hints and annotations to the Python files in the google/ads/googleads/interceptors directory.
Here's a list of the files I updated: - `exception_interceptor.py` - `helpers.py` - `interceptor.py` - `logging_interceptor.py` - `metadata_interceptor.py` - `response_wrappers.py` Note that `__init__.py` didn't require any changes. While adding the type hints, I also made some minor improvements to enhance the robustness and clarity of the code in a few files. This involved adding necessary imports from the `typing` module and other relevant libraries like `grpc` and `google.protobuf`. I also used TypeVars to effectively handle generic protobuf messages and continuation callables.
1 parent 4f350a8 commit 8d8e1a6

File tree

6 files changed

+984
-465
lines changed

6 files changed

+984
-465
lines changed

google/ads/googleads/interceptors/exception_interceptor.py

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,55 @@
1919
so it translates the error to a GoogleAdsFailure instance and raises it.
2020
"""
2121

22-
import grpc
23-
24-
from grpc import UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor
22+
from typing import Any, Callable, TypeVar, Union, NoReturn
2523

24+
import grpc
25+
from grpc import (
26+
UnaryUnaryClientInterceptor,
27+
UnaryStreamClientInterceptor,
28+
ClientCallDetails,
29+
Call,
30+
)
31+
32+
from google.ads.googleads.errors import GoogleAdsException
2633
from .interceptor import Interceptor
2734
from .response_wrappers import _UnaryStreamWrapper, _UnaryUnaryWrapper
2835

36+
# Define generic types for request and response messages.
37+
# These are typically protobuf message instances.
38+
RequestType = TypeVar("RequestType")
39+
ResponseType = TypeVar("ResponseType")
40+
# Type for the continuation callable in intercept_unary_unary
41+
UnaryUnaryContinuation = Callable[
42+
[ClientCallDetails, RequestType], Union[Call, Any]
43+
]
44+
# Type for the continuation callable in intercept_unary_stream
45+
UnaryStreamContinuation = Callable[
46+
[ClientCallDetails, RequestType], Union[grpc.Call, Any]
47+
]
48+
2949

3050
class ExceptionInterceptor(
3151
Interceptor, UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor
3252
):
3353
"""An interceptor that wraps rpc exceptions."""
3454

35-
def __init__(self, api_version, use_proto_plus=False):
55+
_api_version: str
56+
_use_proto_plus: bool
57+
58+
def __init__(self, api_version: str, use_proto_plus: bool = False):
3659
"""Initializes the ExceptionInterceptor.
3760
3861
Args:
39-
api_version: a str of the API version of the request.
40-
use_proto_plus: a boolean of whether returned messages should be
62+
api_version: A str of the API version of the request.
63+
use_proto_plus: A boolean of whether returned messages should be
4164
proto_plus or protobuf.
4265
"""
4366
super().__init__(api_version)
4467
self._api_version = api_version
4568
self._use_proto_plus = use_proto_plus
4669

47-
def _handle_grpc_failure(self, response):
70+
def _handle_grpc_failure(self, response: grpc.Call) -> NoReturn:
4871
"""Attempts to convert failed responses to a GoogleAdsException object.
4972
5073
Handles failed gRPC responses of by attempting to convert them
@@ -61,16 +84,23 @@ def _handle_grpc_failure(self, response):
6184
Raises:
6285
GoogleAdsException: If the exception's trailing metadata
6386
indicates that it is a GoogleAdsException.
64-
RpcError: If the exception's is a gRPC exception but the trailing
87+
grpc.RpcError: If the exception's is a gRPC exception but the trailing
6588
metadata is empty or is not indicative of a GoogleAdsException,
6689
or if the exception has a status code of INTERNAL or
6790
RESOURCE_EXHAUSTED.
6891
Exception: If not a GoogleAdsException or RpcException the error
6992
will be raised as-is.
7093
"""
71-
raise self._get_error_from_response(response)
72-
73-
def intercept_unary_unary(self, continuation, client_call_details, request):
94+
# Assuming _get_error_from_response is defined in the parent Interceptor
95+
# and raises an exception, so this method effectively has -> NoReturn
96+
raise self._get_error_from_response(response) # type: ignore
97+
98+
def intercept_unary_unary(
99+
self,
100+
continuation: UnaryUnaryContinuation[RequestType, ResponseType],
101+
client_call_details: ClientCallDetails,
102+
request: RequestType,
103+
) -> Union[_UnaryUnaryWrapper, ResponseType, Call]:
74104
"""Intercepts and wraps exceptions in the rpc response.
75105
76106
Overrides abstract method defined in grpc.UnaryUnaryClientInterceptor.
@@ -79,32 +109,41 @@ def intercept_unary_unary(self, continuation, client_call_details, request):
79109
continuation: a function to continue the request process.
80110
client_call_details: a grpc._interceptor._ClientCallDetails
81111
instance containing request metadata.
82-
request: a SearchGoogleAdsRequest or SearchGoogleAdsStreamRequest
83-
message class instance.
112+
request: A protobuf message class instance for the request.
84113
85114
Returns:
86-
A grpc.Call instance representing a service response.
115+
A _UnaryUnaryWrapper instance if successful, otherwise this method
116+
will raise an exception via _handle_grpc_failure. The actual
117+
return type from continuation can be grpc.Call or a future-like
118+
object that has an `exception()` method.
87119
88120
Raises:
89121
GoogleAdsException: If the exception's trailing metadata
90122
indicates that it is a GoogleAdsException.
91-
RpcError: If the exception's trailing metadata is empty or is not
123+
grpc.RpcError: If the exception's trailing metadata is empty or is not
92124
indicative of a GoogleAdsException, or if the exception has a
93125
status code of INTERNAL or RESOURCE_EXHAUSTED.
94126
"""
95-
response = continuation(client_call_details, request)
96-
exception = response.exception()
127+
response_call = continuation(client_call_details, request)
128+
# response_call is often a grpc.Call / grpc.Future in unary-unary.
129+
# It has an exception() method to check for errors.
130+
exception = response_call.exception()
97131

98132
if exception:
99-
self._handle_grpc_failure(response)
133+
# _handle_grpc_failure is guaranteed to raise, so the execution stops here.
134+
self._handle_grpc_failure(response_call)
100135
else:
136+
# If there's no exception, wrap the successful response.
101137
return _UnaryUnaryWrapper(
102-
response, use_proto_plus=self._use_proto_plus
138+
response_call, use_proto_plus=self._use_proto_plus
103139
)
104140

105141
def intercept_unary_stream(
106-
self, continuation, client_call_details, request
107-
):
142+
self,
143+
continuation: UnaryStreamContinuation[RequestType, ResponseType],
144+
client_call_details: ClientCallDetails,
145+
request: RequestType,
146+
) -> _UnaryStreamWrapper:
108147
"""Intercepts and wraps exceptions in the rpc response.
109148
110149
Overrides abstract method defined in grpc.UnaryStreamClientInterceptor.
@@ -113,22 +152,21 @@ def intercept_unary_stream(
113152
continuation: a function to continue the request process.
114153
client_call_details: a grpc._interceptor._ClientCallDetails
115154
instance containing request metadata.
116-
request: a SearchGoogleAdsRequest or SearchGoogleAdsStreamRequest
117-
message class instance.
155+
request: A protobuf message class instance for the request.
118156
119157
Returns:
120-
A grpc.Call instance representing a service response.
158+
A _UnaryStreamWrapper instance that wraps the stream response.
121159
122160
Raises:
123-
GoogleAdsException: If the exception's trailing metadata
124-
indicates that it is a GoogleAdsException.
125-
RpcError: If the exception's trailing metadata is empty or is not
126-
indicative of a GoogleAdsException, or if the exception has a
127-
status code of INTERNAL or RESOURCE_EXHAUSTED.
161+
This method itself doesn't raise directly but passes
162+
_handle_grpc_failure to _UnaryStreamWrapper, which may raise if
163+
errors occur during streaming or if the initial call fails.
128164
"""
129-
response = continuation(client_call_details, request)
165+
# In unary-stream, continuation returns an object that is an iterator
166+
# of responses, often a grpc.Call.
167+
response_stream_call = continuation(client_call_details, request)
130168
return _UnaryStreamWrapper(
131-
response,
169+
response_stream_call, # type: ignore
132170
self._handle_grpc_failure,
133171
use_proto_plus=self._use_proto_plus,
134172
)

google/ads/googleads/interceptors/helpers.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Any, Dict, List, TypeVar
1516
from copy import deepcopy
17+
from google.protobuf.message import Message
18+
1619
from google.ads.googleads.util import (
1720
set_nested_message_field,
1821
get_nested_attr,
@@ -29,7 +32,7 @@
2932
# 1. They are returned as part of a Search or SearchStream request.
3033
# 2. They are returned individually in a Get request.
3134
# 3. They are sent to the API as part of a Mutate request.
32-
_MESSAGES_WITH_SENSITIVE_FIELDS = {
35+
_MESSAGES_WITH_SENSITIVE_FIELDS: Dict[str, List[str]] = {
3336
"CustomerUserAccess": ["email_address", "inviter_user_email_address"],
3437
"CustomerUserAccessInvitation": ["email_address"],
3538
"MutateCustomerUserAccessRequest": [
@@ -50,26 +53,30 @@
5053
# This is a list of the names of messages that return search results from the
5154
# API. These messages contain other messages that may contain sensitive
5255
# information that needs to be masked before being logged.
53-
_SEARCH_RESPONSE_MESSAGE_NAMES = [
56+
_SEARCH_RESPONSE_MESSAGE_NAMES: List[str] = [
5457
"SearchGoogleAdsResponse",
5558
"SearchGoogleAdsStreamResponse",
5659
]
5760

61+
ProtoMessageT = TypeVar("ProtoMessageT", bound=Message)
62+
5863

59-
def _copy_message(message):
64+
def _copy_message(message: ProtoMessageT) -> ProtoMessageT:
6065
"""Returns a copy of the given message.
6166
6267
Args:
6368
message: An object containing information from an API request
64-
or response.
69+
or response, expected to be a protobuf Message.
6570
6671
Returns:
6772
A copy of the given message.
6873
"""
6974
return deepcopy(message)
7075

7176

72-
def _mask_message_fields(field_list, message, mask):
77+
def _mask_message_fields(
78+
field_list: List[str], message: ProtoMessageT, mask: str
79+
) -> ProtoMessageT:
7380
"""Copies the given message and masks sensitive fields.
7481
7582
Sensitive fields are given as a list of strings and are overridden
@@ -79,15 +86,21 @@ def _mask_message_fields(field_list, message, mask):
7986
field_list: A list of strings specifying the fields on the message
8087
that should be masked.
8188
message: An object containing information from an API request
82-
or response.
89+
or response, expected to be a protobuf Message.
8390
mask: A str that should replace the sensitive information in the
8491
message.
8592
8693
Returns:
8794
A new instance of the message object with fields copied and masked
8895
where necessary.
8996
"""
90-
copy = _copy_message(message)
97+
# Ensure that the message is not None and is of a type that can be copied.
98+
# The ProtoMessageT TypeVar already implies it's a protobuf message.
99+
if message is None:
100+
# Or handle this case as appropriate, e.g., raise ValueError
101+
return message # Or an empty message of the same type, if possible
102+
103+
copy: ProtoMessageT = _copy_message(message)
91104

92105
for field_path in field_list:
93106
try:
@@ -98,16 +111,21 @@ def _mask_message_fields(field_list, message, mask):
98111
# AttributeError is raised when the field is not defined on the
99112
# message. In this case there's nothing to mask and the field
100113
# should be skipped.
101-
break
114+
# Original code had "break", which would exit the loop entirely
115+
# after the first AttributeError. "continue" seems more appropriate
116+
# to skip only the problematic field_path.
117+
continue
102118

103119
return copy
104120

105121

106-
def _mask_google_ads_search_response(message, mask):
122+
def _mask_google_ads_search_response(message: Any, mask: str) -> Any:
107123
"""Copies and masks sensitive data in a Search response
108124
109125
Response messages include instances of GoogleAdsSearchResponse and
110-
GoogleAdsSearchStreamResponse.
126+
GoogleAdsSearchStreamResponse. For typing, these are kept as Any
127+
due to the dynamic nature of protobuf messages and to avoid circular
128+
dependencies if specific types were imported.
111129
112130
Args:
113131
message: A SearchGoogleAdsResponse or SearchGoogleAdsStreamResponse
@@ -118,7 +136,13 @@ def _mask_google_ads_search_response(message, mask):
118136
Returns:
119137
A copy of the message with sensitive fields masked.
120138
"""
121-
copy = _copy_message(message)
139+
# Given message is Any, the copy will also be Any.
140+
# Specific handling for protobuf-like objects is assumed.
141+
copy: Any = _copy_message(message)
142+
143+
# Assuming 'copy' has a 'results' attribute, which is iterable.
144+
if not hasattr(copy, "results"):
145+
return copy # Or raise an error if 'results' is expected
122146

123147
for row in copy.results:
124148
# Each row is an instance of GoogleAdsRow. The ListFields method
@@ -148,29 +172,48 @@ def _mask_google_ads_search_response(message, mask):
148172
)
149173
# Overwrites the nested message with an exact copy of itself,
150174
# where sensitive fields have been masked.
151-
proto_copy_from(getattr(row, field_name), masked_message)
175+
# for proto_plus messages, _pb holds the protobuf message
176+
# for protobuf messages, it's the message itself
177+
target_nested_message = getattr(row, field_name)
178+
proto_copy_from(target_nested_message, masked_message)
152179

153180
return copy
154181

155182

156-
def mask_message(message, mask):
183+
def mask_message(message: Any, mask: str) -> Any:
157184
"""Copies and returns a message with sensitive fields masked.
158185
159186
Args:
160187
message: An object containing information from an API request
161-
or response.
188+
or response. This is typed as Any due to the variety of
189+
protobuf message types it can handle.
162190
mask: A str that should replace the sensitive information in the
163191
message.
164192
165193
Returns:
166-
A copy of the message instance with sensitive fields masked.
194+
A copy of the message instance with sensitive fields masked, or the
195+
original message if no masking rules apply. The return type is Any,
196+
mirroring the input message type.
167197
"""
168-
class_name = message.__class__.__name__
198+
if not hasattr(message, "__class__") or not hasattr(message.__class__, "__name__"):
199+
# Not an object we can get a class name from, return as is.
200+
return message
201+
202+
class_name: str = message.__class__.__name__
169203

170204
if class_name in _SEARCH_RESPONSE_MESSAGE_NAMES:
205+
# _mask_google_ads_search_response expects Any and returns Any
171206
return _mask_google_ads_search_response(message, mask)
172-
elif class_name in _MESSAGES_WITH_SENSITIVE_FIELDS.keys():
173-
sensitive_fields = _MESSAGES_WITH_SENSITIVE_FIELDS[class_name]
207+
elif class_name in _MESSAGES_WITH_SENSITIVE_FIELDS:
208+
sensitive_fields: List[str] = _MESSAGES_WITH_SENSITIVE_FIELDS[class_name]
209+
# _mask_message_fields is generic over ProtoMessageT.
210+
# Since 'message' is Any here, we're passing Any.
211+
# This might lose some type safety if 'message' isn't actually a Message.
212+
# However, the function's logic implies it expects a message-like object.
213+
# If 'message' here is guaranteed to be a protobuf message,
214+
# we could potentially cast or check, but 'Any' is safer for now.
174215
return _mask_message_fields(sensitive_fields, message, mask)
175216
else:
217+
# If not a special type, return the message as is (or a copy if preferred)
218+
# The original code returns the original message.
176219
return message

0 commit comments

Comments
 (0)