Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion libs/core/garf_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,19 @@
# 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.
"""`garf-core` contains the base abstractions for garf framework.

__version__ = '0.1.4.post1'
These abstractions are used by an implementation for a concrete reporting API.
"""

from garf_core.base_query import BaseQuery
from garf_core.report import GarfReport
from garf_core.report_fetcher import ApiReportFetcher

__all__ = [
'BaseQuery',
'GarfReport',
'ApiReportFetcher',
]

__version__ = '0.1.5'
24 changes: 10 additions & 14 deletions libs/core/garf_core/api_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,25 @@
import abc
import contextlib
import csv
import dataclasses
import json
import os
import pathlib
from collections.abc import Sequence
from typing import Any
from typing import Any, Union

import pydantic
import requests
from typing_extensions import override
from typing_extensions import TypeAlias, override

from garf_core import exceptions
from garf_core import exceptions, query_editor

ApiRowElement: TypeAlias = Union[int, float, str, bool, list, dict, None]

@dataclasses.dataclass
class GarfApiRequest:
"""Base class for specifying request."""


@dataclasses.dataclass
class GarfApiResponse:
class GarfApiResponse(pydantic.BaseModel):
"""Base class for specifying response."""

results: list
results: list[ApiRowElement]


class GarfApiError(exceptions.GarfError):
Expand All @@ -52,7 +48,7 @@ class BaseClient(abc.ABC):

@abc.abstractmethod
def get_response(
self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
self, request: query_editor.BaseQueryElements, **kwargs: str
) -> GarfApiResponse:
"""Method for getting response."""

Expand All @@ -69,7 +65,7 @@ def __init__(self, endpoint: str, **kwargs: str) -> None:

@override
def get_response(
self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
self, request: query_editor.BaseQueryElements, **kwargs: str
) -> GarfApiResponse:
response = requests.get(f'{self.endpoint}/{request.resource_name}')
if response.status_code == self.OK:
Expand All @@ -87,7 +83,7 @@ def __init__(self, results: Sequence[dict[str, Any]], **kwargs: str) -> None:

@override
def get_response(
self, request: GarfApiRequest = GarfApiRequest(), **kwargs: str
self, request: query_editor.BaseQueryElements, **kwargs: str
) -> GarfApiResponse:
del request
return GarfApiResponse(results=self.results)
Expand Down
12 changes: 5 additions & 7 deletions libs/core/garf_core/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@
import functools
import operator
from collections.abc import Mapping, MutableSequence
from typing import Any, Union
from typing import Any

from typing_extensions import TypeAlias, override
from typing_extensions import override

from garf_core import api_clients, exceptions, query_editor

ApiRowElement: TypeAlias = Union[int, float, str, bool, list, None]


class BaseParser(abc.ABC):
"""An interface for all parsers to implement."""
Expand All @@ -41,7 +39,7 @@ def __init__(
def parse_response(
self,
response: api_clients.GarfApiResponse,
) -> list[list[ApiRowElement]]:
) -> list[list[api_clients.ApiRowElement]]:
"""Parses response."""
if not response.results:
return [[]]
Expand All @@ -62,7 +60,7 @@ class ListParser(BaseParser):
def parse_row(
self,
row: list,
) -> list[list[ApiRowElement]]:
) -> list[list[api_clients.ApiRowElement]]:
return row


Expand All @@ -73,7 +71,7 @@ class DictParser(BaseParser):
def parse_row(
self,
row: list,
) -> list[list[ApiRowElement]]:
) -> list[list[api_clients.ApiRowElement]]:
if not isinstance(row, Mapping):
raise GarfParserError
result = []
Expand Down
4 changes: 3 additions & 1 deletion libs/core/garf_core/query_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,9 @@ def remove_comments(self) -> Self:
return self

def remove_trailing_comma(self) -> Self:
self.text = re.sub(r',\s+from', ' FROM', self.query.text, re.IGNORECASE)
self.text = re.sub(
r',\s+from', ' FROM', self.query.text, count=0, flags=re.IGNORECASE
)
return self

def extract_resource_name(self) -> Self:
Expand Down
32 changes: 16 additions & 16 deletions libs/core/garf_core/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from collections.abc import MutableSequence, Sequence
from typing import Generator, Literal, get_args

from garf_core import exceptions, parsers, query_editor
from garf_core import api_clients, exceptions, query_editor


class GarfReport:
Expand All @@ -45,9 +45,9 @@ class GarfReport:

def __init__(
self,
results: Sequence[Sequence[parsers.ApiRowElement]] | None = None,
results: Sequence[Sequence[api_clients.ApiRowElement]] | None = None,
column_names: Sequence[str] | None = None,
results_placeholder: Sequence[Sequence[parsers.ApiRowElement]]
results_placeholder: Sequence[Sequence[api_clients.ApiRowElement]]
| None = None,
query_specification: query_editor.BaseQueryElements | None = None,
auto_convert_to_scalars: bool = True,
Expand Down Expand Up @@ -92,7 +92,7 @@ def to_list(
row_type: Literal['list', 'dict', 'scalar'] = 'list',
flatten: bool = False,
distinct: bool = False,
) -> list[parsers.ApiRowElement]:
) -> list[api_clients.ApiRowElement]:
"""Converts report to a list.

Args:
Expand Down Expand Up @@ -137,7 +137,7 @@ def to_dict(
key_column: str,
value_column: str | None = None,
value_column_output: Literal['scalar', 'list'] = 'list',
) -> dict[str, parsers.ApiRowElement | list[parsers.ApiRowElement]]:
) -> dict[str, api_clients.ApiRowElement | list[api_clients.ApiRowElement]]:
"""Converts report to dictionary.

Args:
Expand Down Expand Up @@ -232,7 +232,7 @@ def to_json(self, output: Literal['json', 'jsonl'] = 'json') -> str:

def get_value(
self, column_index: int = 0, row_index: int = 0
) -> parsers.ApiRowElement:
) -> api_clients.ApiRowElement:
"""Extracts data from report as a scalar.

Raises:
Expand Down Expand Up @@ -310,7 +310,7 @@ def __getitem__(

def _get_rows_slice(
self, key: slice | int
) -> GarfReport | GarfRow | parsers.ApiRowElement:
) -> GarfReport | GarfRow | api_clients.ApiRowElement:
"""Gets one or several rows from the report.

Args:
Expand Down Expand Up @@ -476,7 +476,7 @@ def from_json(cls, json_str: str) -> GarfReport:
data = json.loads(json_str)

def validate_value(value):
if not isinstance(value, get_args(parsers.ApiRowElement)):
if not isinstance(value, get_args(api_clients.ApiRowElement)):
raise TypeError(
f'Unsupported type {type(value)} for value {value}. '
'Expected types: int, float, str, bool, list, or None.'
Expand Down Expand Up @@ -520,7 +520,7 @@ class GarfRow:
"""

def __init__(
self, data: parsers.ApiRowElement, column_names: Sequence[str]
self, data: api_clients.ApiRowElement, column_names: Sequence[str]
) -> None:
"""Initializes new GarfRow.

Expand All @@ -530,11 +530,11 @@ def __init__(
super().__setattr__('data', data)
super().__setattr__('column_names', column_names)

def to_dict(self) -> dict[str, parsers.ApiRowElement]:
def to_dict(self) -> dict[str, api_clients.ApiRowElement]:
"""Maps column names to corresponding data point."""
return {x[1]: x[0] for x in zip(self.data, self.column_names)}

def get_value(self, column_index: int = 0) -> parsers.ApiRowElement:
def get_value(self, column_index: int = 0) -> api_clients.ApiRowElement:
"""Extracts data from row as a scalar.

Raises:
Expand All @@ -548,7 +548,7 @@ def get_value(self, column_index: int = 0) -> parsers.ApiRowElement:
)
return self.data[column_index]

def __getattr__(self, element: str) -> parsers.ApiRowElement:
def __getattr__(self, element: str) -> api_clients.ApiRowElement:
"""Gets element from row as an attribute.

Args:
Expand All @@ -564,7 +564,7 @@ def __getattr__(self, element: str) -> parsers.ApiRowElement:
return self.data[self.column_names.index(element)]
raise AttributeError(f'cannot find {element} element!')

def __getitem__(self, element: str | int) -> parsers.ApiRowElement:
def __getitem__(self, element: str | int) -> api_clients.ApiRowElement:
"""Gets element from row by index.

Args:
Expand All @@ -584,7 +584,7 @@ def __getitem__(self, element: str | int) -> parsers.ApiRowElement:
return self.__getattr__(element)
raise GarfReportError(f'cannot find {element} element!')

def __setattr__(self, name: str, value: parsers.ApiRowElement) -> None:
def __setattr__(self, name: str, value: api_clients.ApiRowElement) -> None:
"""Sets new value for an attribute.

Args:
Expand All @@ -609,7 +609,7 @@ def __setitem__(self, name: str, value: str | int) -> None:
self.data.append(value)
self.column_names.append(name)

def get(self, item: str) -> parsers.ApiRowElement:
def get(self, item: str) -> api_clients.ApiRowElement:
"""Extracts value as dictionary get operation.

Args:
Expand All @@ -620,7 +620,7 @@ def get(self, item: str) -> parsers.ApiRowElement:
"""
return self.__getattr__(item)

def __iter__(self) -> parsers.ApiRowElement:
def __iter__(self) -> api_clients.ApiRowElement:
"""Yields each element of a row."""
for field in self.data:
yield field
Expand Down
29 changes: 7 additions & 22 deletions libs/core/tests/unit/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,10 @@ def test_single_column_report_returns_distinct_flattened_list_legacy(
self,
single_column_report,
):
assert single_column_report.to_list(flatten=True, distinct=True) == [1, 3]
assert single_column_report.to_list(row_type='scalar', distinct=True) == [
1,
3,
]

def test_multi_column_report_converted_to_dict_list_values(
self,
Expand Down Expand Up @@ -543,26 +546,6 @@ def test_from_json_with_inconsistent_keys_raises_value_error(self):
):
report.GarfReport.from_json(json_str)

def test_from_json_with_unsupported_type_in_dict_raises_type_error(self):
json_str = '{"ad_group_id": {"nested": "value"}, "campaign_id": 1}'
with pytest.raises(
TypeError, match=r"Unsupported type <class 'dict'> for value"
):
report.GarfReport.from_json(json_str)

def test_from_json_with_unsupported_type_in_list_raises_type_error(self):
json_str = (
'[{"ad_group_id": 2, "campaign_id": {"ad_group_id": 2, '
'"campaign_id": 1}}]'
)
with pytest.raises(
TypeError,
match=r"Unsupported type <class 'dict'> for value {'ad_group_id': 2, "
r"'campaign_id': 1}. Expected types: int, float, str, bool, list, or "
r'None.',
):
report.GarfReport.from_json(json_str)

def test_from_json_with_inconsistent_column_order_raises_value_error(self):
json_str = (
'[{"ad_group_id": 2, "campaign_id": 1}, {"campaign_id": 2, '
Expand Down Expand Up @@ -610,7 +593,9 @@ def test_convert_empty_report_to_polars_returns_empty_dataframe(

def test_convert_report_to_polars(self, multi_column_report):
expected = pl.DataFrame(
data=[[1, 2], [2, 3], [3, 4]], schema=['campaign_id', 'ad_group_id']
data=[[1, 2], [2, 3], [3, 4]],
schema=['campaign_id', 'ad_group_id'],
orient='row',
)
assert multi_column_report.to_polars().equals(expected)

Expand Down
Loading