garf-core 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
garf_core/__init__.py CHANGED
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = '0.1.0'
15
+ __version__ = '0.1.2'
@@ -20,6 +20,8 @@ from __future__ import annotations
20
20
 
21
21
  import logging
22
22
  import os
23
+ from collections.abc import Sequence
24
+ from typing import Any
23
25
 
24
26
  from garf_core import (
25
27
  api_clients,
@@ -36,7 +38,7 @@ class FakeApiReportFetcher(report_fetcher.ApiReportFetcher):
36
38
 
37
39
  def __init__(
38
40
  self,
39
- data: list[dict[str, parsers.ApiRowElement]] | None = None,
41
+ api_client: api_clients.FakeApiClient | None = None,
40
42
  parser: parsers.BaseParser = parsers.DictParser,
41
43
  query_specification_builder: query_editor.QuerySpecification = (
42
44
  query_editor.QuerySpecification
@@ -46,29 +48,33 @@ class FakeApiReportFetcher(report_fetcher.ApiReportFetcher):
46
48
  json_location: str | os.PathLike[str] | None = None,
47
49
  **kwargs: str,
48
50
  ) -> None:
49
- if not data and not (
51
+ if not api_client and not (
50
52
  data_location := json_location or csv_location or data_location
51
53
  ):
52
54
  raise report_fetcher.ApiReportFetcherError(
53
55
  'Missing fake data for the fetcher.'
54
56
  )
55
- api_client = (
56
- api_clients.FakeApiClient(data)
57
- if data
58
- else api_clients.FakeApiClient.from_file(data_location)
59
- )
57
+ if not api_client:
58
+ api_client = api_clients.FakeApiClient.from_file(data_location)
60
59
  super().__init__(api_client, parser, query_specification_builder, **kwargs)
61
60
 
61
+ @classmethod
62
+ def from_data(cls, data: Sequence[dict[str, Any]]) -> FakeApiReportFetcher:
63
+ """Initializes FakeApiReportFetcher from a sequence of data."""
64
+ return FakeApiReportFetcher(
65
+ api_client=api_clients.FakeApiClient(results=data)
66
+ )
67
+
62
68
  @classmethod
63
69
  def from_csv(
64
70
  cls, file_location: str | os.PathLike[str]
65
71
  ) -> FakeApiReportFetcher:
66
- """Initialized FakeApiReportFetcher from a csv file."""
72
+ """Initializes FakeApiReportFetcher from a csv file."""
67
73
  return FakeApiReportFetcher(csv_location=file_location)
68
74
 
69
75
  @classmethod
70
76
  def from_json(
71
77
  cls, file_location: str | os.PathLike[str]
72
78
  ) -> FakeApiReportFetcher:
73
- """Initialized FakeApiReportFetcher from a json file."""
79
+ """Initializes FakeApiReportFetcher from a json file."""
74
80
  return FakeApiReportFetcher(json_location=file_location)
@@ -40,11 +40,12 @@ class RestApiReportFetcher(report_fetcher.ApiReportFetcher):
40
40
 
41
41
  def __init__(
42
42
  self,
43
- endpoint: str,
43
+ api_client: api_clients.RestApiClient | None = None,
44
44
  parser: parsers.BaseParser = parsers.DictParser,
45
45
  query_specification_builder: query_editor.QuerySpecification = (
46
46
  query_editor.QuerySpecification
47
47
  ),
48
+ endpoint: str | None = None,
48
49
  **kwargs: str,
49
50
  ) -> None:
50
51
  """Instantiates RestApiReportFetcher.
@@ -54,5 +55,17 @@ class RestApiReportFetcher(report_fetcher.ApiReportFetcher):
54
55
  parser: Type of parser to convert API response.
55
56
  query_specification_builder: Class to perform query parsing.
56
57
  """
57
- api_client = api_clients.RestApiClient(endpoint)
58
+ if not api_client and not endpoint:
59
+ raise report_fetcher.ApiReportFetcherError(
60
+ 'Missing api_client or endpoint for the fetcher.'
61
+ )
62
+ if not api_client:
63
+ api_client = api_clients.RestApiClient(endpoint)
58
64
  super().__init__(api_client, parser, query_specification_builder, **kwargs)
65
+
66
+ @classmethod
67
+ def from_endpoint(cls, endpoint: str) -> RestApiReportFetcher:
68
+ """Initializes RestApiReportFetcher: from an API endpoint."""
69
+ return RestApiReportFetcher(
70
+ api_client=api_clients.RestApiClient(endpoint=endpoint)
71
+ )
garf_core/parsers.py CHANGED
@@ -32,21 +32,26 @@ ApiRowElement: TypeAlias = Union[int, float, str, bool, list, None]
32
32
  class BaseParser(abc.ABC):
33
33
  """An interface for all parsers to implement."""
34
34
 
35
+ def __init__(
36
+ self, query_specification: query_editor.BaseQueryElements
37
+ ) -> None:
38
+ """Initializes BaseParser."""
39
+ self.query_spec = query_specification
40
+
35
41
  def parse_response(
36
42
  self,
37
43
  response: api_clients.GarfApiResponse,
38
- query_specification: query_editor.BaseQueryElements,
39
44
  ) -> list[list[ApiRowElement]]:
40
45
  """Parses response."""
41
46
  if not response.results:
42
47
  return [[]]
43
48
  results = []
44
49
  for result in response.results:
45
- results.append(self.parse_row(result, query_specification))
50
+ results.append(self.parse_row(result))
46
51
  return results
47
52
 
48
53
  @abc.abstractmethod
49
- def parse_row(self, row, query_specification):
54
+ def parse_row(self, row):
50
55
  """Parses single row from response."""
51
56
 
52
57
 
@@ -57,7 +62,6 @@ class ListParser(BaseParser):
57
62
  def parse_row(
58
63
  self,
59
64
  row: list,
60
- query_specification: query_editor.BaseQueryElements,
61
65
  ) -> list[list[ApiRowElement]]:
62
66
  return row
63
67
 
@@ -69,12 +73,11 @@ class DictParser(BaseParser):
69
73
  def parse_row(
70
74
  self,
71
75
  row: list,
72
- query_specification: query_editor.BaseQueryElements,
73
76
  ) -> list[list[ApiRowElement]]:
74
77
  if not isinstance(row, Mapping):
75
78
  raise GarfParserError
76
79
  result = []
77
- for field in query_specification.fields:
80
+ for field in self.query_spec.fields:
78
81
  result.append(self.get_nested_field(row, field))
79
82
  return result
80
83
 
garf_core/report.py CHANGED
@@ -45,25 +45,25 @@ class GarfReport:
45
45
 
46
46
  def __init__(
47
47
  self,
48
- results: Sequence[Sequence[parsers.ApiRowElement]],
49
- column_names: Sequence[str],
48
+ results: Sequence[Sequence[parsers.ApiRowElement]] | None = None,
49
+ column_names: Sequence[str] | None = None,
50
50
  results_placeholder: Sequence[Sequence[parsers.ApiRowElement]]
51
51
  | None = None,
52
- query_specification: query_editor.BaseQuerySpecification | None = None,
52
+ query_specification: query_editor.BaseQueryElements | None = None,
53
53
  auto_convert_to_scalars: bool = True,
54
54
  ) -> None:
55
55
  """Initializes GarfReport from API response.
56
56
 
57
57
  Args:
58
- results: Contains data from Ads API in a form of nested list
59
- column_names: Maps in each element in sublist of results to name.
60
- results_placeholder: Optional placeholder values for missing results.
61
- query_specification: Specification used to get data from Ads API.
62
- auto_convert_to_scalars: Whether to simplify slicing operations.
58
+ results: Contains data from Ads API in a form of nested list
59
+ column_names: Maps in each element in sublist of results to name.
60
+ results_placeholder: Optional placeholder values for missing results.
61
+ query_specification: Specification used to get data from Ads API.
62
+ auto_convert_to_scalars: Whether to simplify slicing operations.
63
63
  """
64
- self.results = results
65
- self.column_names = column_names
66
- self._multi_column_report = len(column_names) > 1
64
+ self.results = results or []
65
+ self.column_names = column_names or []
66
+ self._multi_column_report = len(column_names) > 1 if column_names else False
67
67
  if results_placeholder:
68
68
  self.results_placeholder = list(results_placeholder)
69
69
  else:
@@ -238,6 +238,8 @@ class GarfReport:
238
238
  Raises:
239
239
  GarfReportError: If row or column index are out of bounds.
240
240
  """
241
+ if not self:
242
+ raise GarfReportError('Cannot get value from an empty report')
241
243
  if column_index >= len(self.column_names):
242
244
  raise GarfReportError(
243
245
  'Column %d of report is not found; report contains only %d columns.',
@@ -298,6 +300,10 @@ class GarfReport:
298
300
  Raises:
299
301
  GarfReportError: When incorrect column_name specified.
300
302
  """
303
+ if not self:
304
+ if isinstance(key, (MutableSequence, str)):
305
+ raise GarfReportError(f"Cannot get '{key}' from an empty report")
306
+ raise GarfReportError('Cannot get element from an empty report')
301
307
  if isinstance(key, (MutableSequence, str)):
302
308
  return self._get_columns_slice(key)
303
309
  return self._get_rows_slice(key)
@@ -342,6 +348,8 @@ class GarfReport:
342
348
  Raises:
343
349
  GarfReportError: When incorrect column_name specified.
344
350
  """
351
+ if not self:
352
+ return self
345
353
  if isinstance(key, str):
346
354
  key = [key]
347
355
  if set(key).issubset(set(self.column_names)):
@@ -356,15 +364,13 @@ class GarfReport:
356
364
  results.append(rows)
357
365
  # TODO: propagate placeholders and query specification to new report
358
366
  return GarfReport(results, key)
359
- non_existing_keys = set(key).intersection(set(self.column_names))
367
+ non_existing_keys = set(key).difference(set(self.column_names))
360
368
  if len(non_existing_keys) > 1:
361
- message = (
362
- f"Columns '{', '.join(list(non_existing_keys))}' "
363
- 'cannot be found in the report'
364
- )
365
- message = (
366
- f"Column '{non_existing_keys.pop()}' " 'cannot be found in the report'
367
- )
369
+ missing_columns = ', '.join(list(non_existing_keys))
370
+ else:
371
+ missing_columns = non_existing_keys.pop()
372
+
373
+ message = f"Columns '{missing_columns}' cannot be found in the report"
368
374
  raise GarfReportError(message)
369
375
 
370
376
  def __eq__(self, other) -> bool:
@@ -39,6 +39,17 @@ class ApiReportFetcherError(exceptions.GarfError):
39
39
  """Base exception for all ApiReportFetchers."""
40
40
 
41
41
 
42
+ class MissingApiReportFetcherError(ApiReportFetcherError):
43
+ """Exception for not found report fetcher."""
44
+
45
+ def __init__(self, source: str) -> None:
46
+ """Initializes MissingApiReportFetcherError."""
47
+ self.source = source
48
+
49
+ def __str__(self) -> str:
50
+ return f'No fetcher available for the source "{self.source}"'
51
+
52
+
42
53
  class ApiReportFetcher:
43
54
  """Class responsible for getting data from report API.
44
55
 
@@ -69,7 +80,7 @@ class ApiReportFetcher:
69
80
  Mapping between query name and function for generating GarfReport.
70
81
  """
71
82
  self.api_client = api_client
72
- self.parser = parser()
83
+ self.parser = parser
73
84
  self.query_specification_builder = query_specification_builder
74
85
  self.query_args = kwargs
75
86
  self.builtin_queries = builtin_queries or {}
@@ -135,7 +146,7 @@ class ApiReportFetcher:
135
146
  return builtin_report(self, **kwargs)
136
147
 
137
148
  response = self.api_client.get_response(query, **kwargs)
138
- parsed_response = self.parser.parse_response(response, query)
149
+ parsed_response = self.parser(query).parse_response(response)
139
150
  return report.GarfReport(
140
151
  results=parsed_response, column_names=query.column_names
141
152
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: garf-core
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Abstracts fetching data from API based on provided SQL-like query.
5
5
  Author-email: "Google Inc. (gTech gPS CSE team)" <no-reply@google.com>, Andrei Markin <andrey.markin.ppc@gmail.com>
6
6
  License: Apache 2.0
@@ -0,0 +1,16 @@
1
+ garf_core/__init__.py,sha256=GIvTQD8k9INUEIQO5HBfLShtzbins5jjqQfi0z2J9h0,598
2
+ garf_core/api_clients.py,sha256=k4vjFsA3JOx7Hp_pXIQNwzMXqq4u4dDcz-ddkJC0JhI,4651
3
+ garf_core/base_query.py,sha256=ZDAw2ojmismXRO0HXEvKDukpS7OAc7390LnM8kvCSCY,1201
4
+ garf_core/exceptions.py,sha256=Gzvkl2M-rA_XQRAMd3CC62KHeFQE_b6uby0fD0pouw4,1269
5
+ garf_core/parsers.py,sha256=-178UL2hbrC83GkMqAPgrmpGpJitR7T9xT3OgJ8w16Y,3189
6
+ garf_core/query_editor.py,sha256=lA1aMFc_1wo6e2CyVISarnZxtH2jnKkuZfJecQqslDg,17191
7
+ garf_core/report.py,sha256=O0SPxgVQckxi_QuD3vy1h_1mi5aMjQNw0LYP-jgN2Do,20262
8
+ garf_core/report_fetcher.py,sha256=dgErrkRv1bdPN0npX3atZbJrOpzy9OmoPC38e2FpBSQ,4883
9
+ garf_core/fetchers/__init__.py,sha256=_cSjg1D5RhUKxaVeVbaDdb8AAoI9glKJXgN5H4qXFkw,783
10
+ garf_core/fetchers/fake.py,sha256=fgJjxuHyd6EIUflUtj8r_HfaMS1YTDrOqDlaj6Kvbjs,2584
11
+ garf_core/fetchers/rest.py,sha256=-5B2-Ck_t3hG99ym59AKwlzctiDxRFI2Nnc8STBxRDo,2201
12
+ garf_core-0.1.2.dist-info/METADATA,sha256=dGX17SeAetNBUlagvqWDGHX4yzIura36Bl1CP0Bd0OU,2443
13
+ garf_core-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ garf_core-0.1.2.dist-info/entry_points.txt,sha256=u4h-ujHO1hbxVXRQzwcC4ftju9_KBYtq5mCLKEBHMj0,69
15
+ garf_core-0.1.2.dist-info/top_level.txt,sha256=Gj-Zp7fM2turGut5vTJuo5xEcKfow7cTLX3y3WFfNgA,10
16
+ garf_core-0.1.2.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- garf_core/__init__.py,sha256=TVigAs35ZN82B6dDphIRXsuJqB2gTIaFv23NVzjAtT8,598
2
- garf_core/api_clients.py,sha256=k4vjFsA3JOx7Hp_pXIQNwzMXqq4u4dDcz-ddkJC0JhI,4651
3
- garf_core/base_query.py,sha256=ZDAw2ojmismXRO0HXEvKDukpS7OAc7390LnM8kvCSCY,1201
4
- garf_core/exceptions.py,sha256=Gzvkl2M-rA_XQRAMd3CC62KHeFQE_b6uby0fD0pouw4,1269
5
- garf_core/parsers.py,sha256=Uj7aT7roYUofivrBKVKWfcSeG37oYGGLIJ-op2C3hZc,3238
6
- garf_core/query_editor.py,sha256=lA1aMFc_1wo6e2CyVISarnZxtH2jnKkuZfJecQqslDg,17191
7
- garf_core/report.py,sha256=2z4tUR5mg29CkwodGaFIXs8Vpo2DCyyzwJWXhBrT0R4,19910
8
- garf_core/report_fetcher.py,sha256=o6YT_o0t9EPutiJk9R637lQabkHSaOrzoF2ciOmpQPA,4560
9
- garf_core/fetchers/__init__.py,sha256=_cSjg1D5RhUKxaVeVbaDdb8AAoI9glKJXgN5H4qXFkw,783
10
- garf_core/fetchers/fake.py,sha256=Ba8EnSIZyNkTza3kfo2OlWWQSKcOawp1aIoyoX-Uw74,2313
11
- garf_core/fetchers/rest.py,sha256=0d6sRVcxBSwWpMv5TH2L9lGGxad5vbK24q9-Ql8DcCc,1701
12
- garf_core-0.1.0.dist-info/METADATA,sha256=V9eJEM7smpVTRquelpfDiWhqQUxIKGQSocoHpcVXmrQ,2443
13
- garf_core-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- garf_core-0.1.0.dist-info/entry_points.txt,sha256=u4h-ujHO1hbxVXRQzwcC4ftju9_KBYtq5mCLKEBHMj0,69
15
- garf_core-0.1.0.dist-info/top_level.txt,sha256=Gj-Zp7fM2turGut5vTJuo5xEcKfow7cTLX3y3WFfNgA,10
16
- garf_core-0.1.0.dist-info/RECORD,,