Skip to content

Commit 791b973

Browse files
[core] feat: add support for proto message parser
1 parent ab6fdad commit 791b973

File tree

3 files changed

+111
-38
lines changed

3 files changed

+111
-38
lines changed

libs/core/garf_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626
'ApiReportFetcher',
2727
]
2828

29-
__version__ = '0.5.0'
29+
__version__ = '0.6.0'

libs/core/garf_core/parsers.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ def parse_response(
6565
span.set_attribute('num_results', len(results))
6666
return results
6767

68+
@abc.abstractmethod
69+
def get_row_element(self, row, key):
70+
"""Defines how to get a single element from a row."""
71+
6872
def _evalute_virtual_column(
6973
self,
7074
fields: list[str],
@@ -138,20 +142,31 @@ def process_customizer(
138142

139143
def _process_customizer_slice(self, row, customizer, field):
140144
slice_object = customizer.value.slice_literal
141-
return [r.get(customizer.value.value) for r in row.get(field)[slice_object]]
145+
elements = self.get_row_element(row, field)
146+
results = []
147+
for element in elements[slice_object]:
148+
results.append(self.get_row_element(element, customizer.value.value))
149+
return results
142150

143151
def _process_nested_field(self, row, customizer, field):
144-
nested_field = row.get(field)
152+
nested_field = self.get_row_element(row, field)
153+
if isinstance(nested_field, MutableSequence):
154+
return list(
155+
{
156+
self.parse_row_element(field, customizer.value)
157+
for field in nested_field
158+
}
159+
)
145160
try:
146-
return operator.attrgetter(customizer.value)(nested_field)
161+
return self.parse_row_element(field, customizer.value)
147162
except AttributeError as e:
148163
raise query_parser.GarfCustomizerError(
149164
f'nested field {customizer.value} is missing in row {row}'
150165
) from e
151166

152167
def _process_resource_index(self, row, customizer, field):
153-
resource = row.get(field, '/')
154-
_, *elements = row.get(field, '/').split('/')
168+
resource = self.get_row_element(row, field)
169+
_, *elements = resource.split('/')
155170
if not elements:
156171
raise query_parser.GarfCustomizerError(
157172
f'Not a valid resource: {resource}'
@@ -213,6 +228,10 @@ def parse_row_element(
213228
except (TypeError, KeyError):
214229
return None
215230

231+
def get_row_element(self, row, key):
232+
"""Gets element from a dict by key."""
233+
return row.get(key)
234+
216235

217236
class NumericConverterDictParser(DictParser):
218237
"""Extracts nested dict elements with numerical conversions."""
@@ -241,5 +260,24 @@ def convert_field(value):
241260
return None
242261

243262

263+
class ProtoParser(BaseParser):
264+
"""Extracts attribute from Protobuf messages."""
265+
266+
def parse_row_element(
267+
self, row: api_clients.ApiResponseRow, key: str
268+
) -> api_clients.ApiRowElement:
269+
"""Returns attributes from a Protobuf message based on a key."""
270+
try:
271+
return operator.attrgetter(key)(row)
272+
except AttributeError as e:
273+
raise query_parser.GarfFieldError(
274+
f'field {key} is missing in row {row}'
275+
) from e
276+
277+
def get_row_element(self, row, key):
278+
"""Gets nested attribute from a Protobuf message."""
279+
return operator.attrgetter(key)(row)
280+
281+
244282
class GarfParserError(exceptions.GarfError):
245283
"""Incorrect data format for parser."""

libs/core/tests/unit/test_parsers.py

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ class NestedResource(pydantic.BaseModel):
2525
nested_element: int
2626

2727

28+
class ArrayElement(pydantic.BaseModel):
29+
element: int
30+
31+
32+
class FakeProtoMessage(pydantic.BaseModel):
33+
resource: str
34+
resource_id: int
35+
resource_name: str
36+
resource_data: NestedResource
37+
array_data: list[ArrayElement]
38+
39+
2840
class TestDictParser:
2941
@pytest.fixture
3042
def test_parser(self):
@@ -174,38 +186,6 @@ def test_parse_response_raises_customizer_error_on_invalid_resource(self):
174186
):
175187
test_parser.parse_response(test_response)
176188

177-
def test_parse_response_returns_correct_nested_attribute(self):
178-
spec = query_editor.QuerySpecification(
179-
text='SELECT resource:nested_element AS column FROM test'
180-
).generate()
181-
test_parser = parsers.DictParser(spec)
182-
test_response = api_clients.GarfApiResponse(
183-
results=[
184-
{'resource': NestedResource(nested_element=1)},
185-
{'resource': NestedResource(nested_element=2)},
186-
]
187-
)
188-
parsed_row = test_parser.parse_response(test_response)
189-
assert parsed_row == [[1], [2]]
190-
191-
def test_parse_response_raises_customizer_error_on_missing_nested_attribute(
192-
self,
193-
):
194-
spec = query_editor.QuerySpecification(
195-
text='SELECT resource:missing_element AS column FROM test'
196-
).generate()
197-
test_parser = parsers.DictParser(spec)
198-
test_response = api_clients.GarfApiResponse(
199-
results=[
200-
{'resource': NestedResource(nested_element=1)},
201-
]
202-
)
203-
with pytest.raises(
204-
query_parser.GarfCustomizerError,
205-
match='nested field missing_element is missing in row',
206-
):
207-
test_parser.parse_response(test_response)
208-
209189
def test_parse_response_skips_omitted_columns(self):
210190
test_specification = query_editor.QuerySpecification(
211191
'SELECT test_column_1 AS _, test_column_2 FROM test'
@@ -237,3 +217,58 @@ def test_parse_row_returns_converted_numeric_values(self, test_parser):
237217
expected_row = [1]
238218

239219
assert parsed_row == expected_row
220+
221+
222+
class TestProtoParser:
223+
@pytest.fixture
224+
def test_parser(self):
225+
spec = query_editor.QuerySpecification(
226+
"""
227+
SELECT
228+
resource_id,
229+
resource_id + 1 AS next_resource_id,
230+
resource_name,
231+
resource_data.nested_element,
232+
array_data[0].element AS slice_element
233+
FROM test
234+
"""
235+
).generate()
236+
return parsers.ProtoParser(spec)
237+
238+
def test_parse_row_returns_converted_numeric_values(self, test_parser):
239+
test_row = FakeProtoMessage(
240+
resource='resources/1/resource/99',
241+
resource_id=1,
242+
resource_name='test',
243+
resource_data=NestedResource(nested_element=10),
244+
array_data=[ArrayElement(element=100)],
245+
)
246+
247+
parsed_row = test_parser.parse_row(test_row)
248+
expected_row = [1, 2, 'test', 10, [100]]
249+
250+
assert parsed_row == expected_row
251+
252+
def test_parse_response_raises_customizer_error_on_missing_nested_attribute(
253+
self,
254+
):
255+
spec = query_editor.QuerySpecification(
256+
text='SELECT resource_data:missing_element AS column FROM test'
257+
).generate()
258+
test_parser = parsers.ProtoParser(spec)
259+
test_response = api_clients.GarfApiResponse(
260+
results=[
261+
FakeProtoMessage(
262+
resource='resources/1/resource/99',
263+
resource_id=1,
264+
resource_name='test',
265+
resource_data=NestedResource(nested_element=10),
266+
array_data=[ArrayElement(element=100)],
267+
)
268+
]
269+
)
270+
with pytest.raises(
271+
query_parser.GarfFieldError,
272+
match='field missing_element is missing in row resource_data',
273+
):
274+
test_parser.parse_response(test_response)

0 commit comments

Comments
 (0)