Skip to content

Commit 3de2aee

Browse files
[core] feat: add support for generating results_placeholder
1 parent f63a469 commit 3de2aee

File tree

5 files changed

+129
-20
lines changed

5 files changed

+129
-20
lines changed

libs/community/google/ads/garf_google_ads/api_clients.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ def __init__(
9191
self.ads_service = self.client.get_service('GoogleAdsService')
9292
self.kwargs = kwargs
9393

94+
def _get_google_ads_row(self) -> google_ads_service.GoogleAdsRow:
95+
"""Gets GoogleAdsRow proto message for a given API version."""
96+
google_ads_service = importlib.import_module(
97+
f'google.ads.googleads.{self.api_version}.'
98+
f'services.types.google_ads_service'
99+
)
100+
return google_ads_service.GoogleAdsRow()
101+
94102
@override
95103
@tenacity.retry(
96104
stop=tenacity.stop_after_attempt(3),
@@ -103,24 +111,14 @@ def __init__(
103111
def get_response(
104112
self, request: query_editor.GoogleAdsApiQuery, account: int, **kwargs: str
105113
) -> api_clients.GarfApiResponse:
106-
"""Executes query for a given entity_id (customer_id).
107-
108-
Args:
109-
account: Google Ads customer_id.
110-
query_text: GAQL query text.
111-
112-
Returns:
113-
SearchGoogleAdsStreamResponse for a given API version.
114-
115-
Raises:
116-
google_exceptions.InternalServerError:
117-
When data cannot be fetched from Ads API.
118-
"""
114+
"""Executes a single API request for a given customer_id and GAQL query."""
119115
response = self.ads_service.search_stream(
120116
customer_id=account, query=request.text
121117
)
122118
results = [result for batch in response for result in batch.results]
123-
return api_clients.GarfApiResponse(results=results)
119+
return api_clients.GarfApiResponse(
120+
results=results, results_placeholder=[self._get_google_ads_row()]
121+
)
124122

125123
def _init_client(
126124
self,

libs/community/google/youtube/youtube-data-api/garf_youtube_data_api/api_clients.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,47 @@ def service(self):
7272
return self._service
7373
return build('youtube', self.api_version, developerKey=self.api_key)
7474

75+
def infer_types(self, service, name, fields):
76+
results = {}
77+
ress = service._schema.schemas.get(name)
78+
props = ress.get('properties')
79+
for field in fields:
80+
if prop := props.get(field):
81+
if ref := prop.get('$ref'):
82+
results[field] = self.infer_types(service, ref, [field])
83+
else:
84+
results[field] = prop.get('type') or prop.get('type')
85+
else:
86+
results.update(
87+
{k: v.get('format') or v.get('type') for k, v in props.items()}
88+
)
89+
return results
90+
91+
def generate_sample_values(self, types):
92+
results = {}
93+
type_mapping = {
94+
'string': '',
95+
'int64': 1,
96+
'int32': 1,
97+
'uint64': 1,
98+
'uint32': 1,
99+
'double': 1.0,
100+
'boolean': True,
101+
'date-time': '2024-01-01',
102+
'google-datetime': '2024-01-01T',
103+
'google-duration': '2H',
104+
}
105+
for key, value in types.items():
106+
if isinstance(value, dict):
107+
results[key] = self.generate_sample_values(value)
108+
else:
109+
results[key] = type_mapping.get(value)
110+
return results
111+
112+
def simulate(self, service, name, fields):
113+
types = self.infer_types(service, name, fields)
114+
return self.generate_sample_values(types)
115+
75116
@override
76117
@telemetry.tracer.start_as_current_span('youtube_data_api.get_response')
77118
def get_response(
@@ -80,9 +121,12 @@ def get_response(
80121
span = trace.get_current_span()
81122
for k, v in kwargs.items():
82123
span.set_attribute(f'youtube_data_api.kwargs.{k}', v)
83-
fields = [field.split('.')[0] for field in request.fields]
124+
fields = {field.split('.')[0] for field in request.fields}
84125
sub_service = getattr(self.service, request.resource_name)()
85126
part_str = ','.join(fields)
127+
128+
sim_res = self.simulate(sub_service, 'Video', fields)
129+
86130
result = self._list(sub_service, part=part_str, **kwargs)
87131
results = []
88132
if data := result.get('items'):
@@ -129,8 +173,12 @@ def get_response(
129173
break
130174
if include_row:
131175
filtered_results.append(row)
132-
return api_clients.GarfApiResponse(results=filtered_results)
133-
return api_clients.GarfApiResponse(results=results)
176+
return api_clients.GarfApiResponse(
177+
results=filtered_results, results_placeholder=[sim_res]
178+
)
179+
return api_clients.GarfApiResponse(
180+
results=results, results_placeholder=[sim_res]
181+
)
134182

135183
def _list(
136184
self, service, part: str, next_page_token: str | None = None, **kwargs

libs/core/garf_core/api_clients.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class GarfApiResponse(pydantic.BaseModel):
4040
"""Base class for specifying response."""
4141

4242
results: list[ApiResponseRow | Any]
43+
results_placeholder: list[ApiResponseRow | Any] = pydantic.Field(
44+
default_factory=list
45+
)
4346

4447
def __bool__(self) -> bool:
4548
return bool(self.results)
@@ -100,17 +103,27 @@ def get_response(
100103
class FakeApiClient(BaseClient):
101104
"""Fake class for specifying API client."""
102105

103-
def __init__(self, results: Sequence[dict[str, Any]], **kwargs: str) -> None:
106+
def __init__(
107+
self,
108+
results: Sequence[dict[str, Any]],
109+
results_placeholder: Sequence[dict[str, Any]] | None = None,
110+
**kwargs: str,
111+
) -> None:
104112
"""Initializes FakeApiClient."""
105113
self.results = list(results)
114+
self.results_placeholder = (
115+
[] if not results_placeholder else list(results_placeholder)
116+
)
106117
self.kwargs = kwargs
107118

108119
@override
109120
def get_response(
110121
self, request: query_editor.BaseQueryElements, **kwargs: str
111122
) -> GarfApiResponse:
112123
del request
113-
return GarfApiResponse(results=self.results)
124+
return GarfApiResponse(
125+
results=self.results, results_placeholder=self.results_placeholder
126+
)
114127

115128
@classmethod
116129
def from_file(cls, file_location: str | os.PathLike[str]) -> FakeApiClient:

libs/core/garf_core/report_fetcher.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,14 @@ def fetch(
181181
logger.info('Cached version not found, generating')
182182
response = self.api_client.call_api(query, **kwargs)
183183
if not response:
184-
return report.GarfReport(query_specification=query)
184+
placeholder_parsed_response = self.parser(query).parse_response(
185+
api_clients.GarfApiResponse(results=response.results_placeholder)
186+
)
187+
return report.GarfReport(
188+
query_specification=query,
189+
results_placeholder=placeholder_parsed_response,
190+
column_names=[c for c in query.column_names if c != '_'],
191+
)
185192

186193
parsed_response = self.parser(query).parse_response(response)
187194
fetched_report = report.GarfReport(

libs/core/tests/unit/test_report_fetcher.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,46 @@ def test_fetch_parses_virtual_columns(self, test_dict_report_fetcher):
159159
)
160160

161161
assert test_report == expected_report
162+
163+
def test_fetch_returns_results_placeholder_when_missing_results(self):
164+
test_api_client = api_clients.FakeApiClient(
165+
results=[],
166+
results_placeholder=[
167+
{'column': {'name': 1}, 'other_column': 2},
168+
],
169+
)
170+
test_fetcher = report_fetcher.ApiReportFetcher(
171+
api_client=test_api_client, parser=parsers.DictParser
172+
)
173+
174+
query = """
175+
SELECT
176+
column.name,
177+
other_column,
178+
0 AS constant_column,
179+
column.name + other_column AS calculated_column,
180+
'http://example.com/' + column.name AS concat_column,
181+
'{current_date}' AS magic_column
182+
FROM test
183+
"""
184+
test_report = test_fetcher.fetch(query)
185+
186+
current_date = datetime.date.today().strftime('%Y-%m-%d')
187+
expected_report = report.GarfReport(
188+
results_placeholder=[
189+
[1, 2, 0, 1 + 2, 'http://example.com/1', current_date],
190+
],
191+
column_names=[
192+
'column_name',
193+
'other_column',
194+
'constant_column',
195+
'calculated_column',
196+
'concat_column',
197+
'magic_column',
198+
],
199+
)
200+
201+
assert (test_report.results_placeholder, test_report.column_names) == (
202+
expected_report.results_placeholder,
203+
expected_report.column_names,
204+
)

0 commit comments

Comments
 (0)