garf-core 0.0.8__tar.gz → 0.0.10__tar.gz

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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: garf-core
3
- Version: 0.0.8
3
+ Version: 0.0.10
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>
6
6
  License: Apache 2.0
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.9
10
10
  Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
16
  Classifier: Operating System :: OS Independent
@@ -17,8 +18,10 @@ Classifier: License :: OSI Approved :: Apache Software License
17
18
  Requires-Python: >=3.8
18
19
  Description-Content-Type: text/markdown
19
20
  Requires-Dist: python-dateutil
20
- Requires-Dist: jinja2==3.1.4
21
+ Requires-Dist: jinja2
21
22
  Requires-Dist: typing-extensions
23
+ Requires-Dist: requests
24
+ Requires-Dist: pyyaml
22
25
  Provides-Extra: pandas
23
26
  Requires-Dist: pandas; extra == "pandas"
24
27
  Provides-Extra: polars
@@ -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.0.8'
15
+ __version__ = '0.0.10'
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -22,6 +22,8 @@ from collections.abc import Sequence
22
22
  import requests
23
23
  from typing_extensions import override
24
24
 
25
+ from garf_core import exceptions
26
+
25
27
 
26
28
  @dataclasses.dataclass
27
29
  class GarfApiRequest:
@@ -35,7 +37,7 @@ class GarfApiResponse:
35
37
  results: list
36
38
 
37
39
 
38
- class GarfApiError(Exception):
40
+ class GarfApiError(exceptions.GarfError):
39
41
  """API specific exception."""
40
42
 
41
43
 
@@ -1,4 +1,4 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -22,38 +22,10 @@
22
22
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23
23
  # See the License for the specific language governing permissions and
24
24
  # limitations under the License.
25
- """Module for defining exceptions."""
25
+ """Defines common exceptions for the garf-core library."""
26
26
 
27
27
  from __future__ import annotations
28
28
 
29
29
 
30
30
  class GarfError(Exception):
31
31
  """Base exception."""
32
-
33
-
34
- class GarfQueryException(GarfError):
35
- """Base exception for Garf queries."""
36
-
37
-
38
- class GarfParserException(GarfError):
39
- """Base exception for Garf parsers."""
40
-
41
-
42
- class GarfCustomizerException(GarfParserException):
43
- """Specifies incorrect customizer."""
44
-
45
-
46
- class GarfVirtualColumnException(GarfParserException):
47
- """Specifies incorrect virtual column type."""
48
-
49
-
50
- class GarfFieldException(GarfQueryException):
51
- """Specifies incorrect Google Ads API field."""
52
-
53
-
54
- class GarfMacroException(GarfQueryException):
55
- """Specifies incorrect macro in Garf query."""
56
-
57
-
58
- class GarfResourceException(GarfQueryException):
59
- """Specifies incorrect resource name in Google Ads API."""
@@ -1,4 +1,4 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -11,11 +11,7 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- """Module for defining various parsing strategy for GoogleAdsRow elements.
15
-
16
- GoogleAdsRowParser parses a single GoogleAdsRow and applies different parsing
17
- strategies to each element of the row.
18
- """
14
+ """Module for defining various parsing strategy for API response."""
19
15
 
20
16
  from __future__ import annotations
21
17
 
@@ -24,7 +20,7 @@ import contextlib
24
20
  import functools
25
21
  import operator
26
22
  from collections.abc import Mapping, MutableSequence
27
- from typing import Union
23
+ from typing import Any, Union
28
24
 
29
25
  from typing_extensions import TypeAlias, override
30
26
 
@@ -34,44 +30,56 @@ ApiRowElement: TypeAlias = Union[int, float, str, bool, list, None]
34
30
 
35
31
 
36
32
  class BaseParser(abc.ABC):
37
- @abc.abstractmethod
33
+ """An interface for all parsers to implement."""
34
+
38
35
  def parse_response(
39
- self, response: api_clients.GarfApiResponse
36
+ self,
37
+ response: api_clients.GarfApiResponse,
38
+ query_specification: query_editor.BaseQueryElements,
40
39
  ) -> list[list[ApiRowElement]]:
41
40
  """Parses response."""
41
+ if not response.results:
42
+ return [[]]
43
+ results = []
44
+ for result in response.results:
45
+ results.append(self.parse_row(result, query_specification))
46
+ return results
47
+
48
+ @abc.abstractmethod
49
+ def parse_row(self, row, query_specification):
50
+ """Parses single row from response."""
42
51
 
43
52
 
44
53
  class ListParser(BaseParser):
54
+ """Returns API results as is."""
55
+
45
56
  @override
46
- def parse_response(
57
+ def parse_row(
47
58
  self,
48
- response: api_clients.GarfApiResponse,
59
+ row: list,
49
60
  query_specification: query_editor.BaseQueryElements,
50
61
  ) -> list[list[ApiRowElement]]:
51
- del query_specification
52
- return response.results
62
+ return row
53
63
 
54
64
 
55
65
  class DictParser(BaseParser):
66
+ """Extracts nested dict elements."""
67
+
56
68
  @override
57
- def parse_response(
69
+ def parse_row(
58
70
  self,
59
- response: api_clients.GarfApiResponse,
71
+ row: list,
60
72
  query_specification: query_editor.BaseQueryElements,
61
73
  ) -> list[list[ApiRowElement]]:
62
- if not response.results:
63
- return [[]]
64
- if not isinstance(response.results[0], Mapping):
65
- return GarfParserError
66
- results = []
67
- for result in response.results:
68
- row = []
69
- for field in query_specification.fields:
70
- row.append(self.get_nested_field(result, field))
71
- results.append(row)
72
- return results
73
-
74
- def get_nested_field(self, dictionary, key):
74
+ if not isinstance(row, Mapping):
75
+ raise GarfParserError
76
+ result = []
77
+ for field in query_specification.fields:
78
+ result.append(self.get_nested_field(row, field))
79
+ return result
80
+
81
+ def get_nested_field(self, dictionary: dict[str, Any], key: str):
82
+ """Returns nested fields from a dictionary."""
75
83
  key = key.split('.')
76
84
  try:
77
85
  return functools.reduce(operator.getitem, key, dictionary)
@@ -80,7 +88,11 @@ class DictParser(BaseParser):
80
88
 
81
89
 
82
90
  class NumericConverterDictParser(DictParser):
83
- def get_nested_field(self, dictionary, key):
91
+ """Extracts nested dict elements with numerical conversions."""
92
+
93
+ def get_nested_field(self, dictionary: dict[str, Any], key: str):
94
+ """Extract nested field with int/float conversion."""
95
+
84
96
  def convert_field(value):
85
97
  for type_ in (int, float):
86
98
  with contextlib.suppress(ValueError):
@@ -29,6 +29,30 @@ from typing_extensions import Self
29
29
  from garf_core import exceptions
30
30
 
31
31
 
32
+ class GarfQueryError(exceptions.GarfError):
33
+ """Base exception for Garf queries."""
34
+
35
+
36
+ class GarfCustomizerError(GarfQueryError):
37
+ """Specifies incorrect customizer."""
38
+
39
+
40
+ class GarfVirtualColumnError(GarfQueryError):
41
+ """Specifies incorrect virtual column type."""
42
+
43
+
44
+ class GarfFieldError(GarfQueryError):
45
+ """Specifies incorrect fields from API."""
46
+
47
+
48
+ class GarfMacroError(GarfQueryError):
49
+ """Specifies incorrect macro in Garf query."""
50
+
51
+
52
+ class GarfResourceError(GarfQueryError):
53
+ """Specifies incorrect resource name in the query."""
54
+
55
+
32
56
  @dataclasses.dataclass
33
57
  class ProcessedField:
34
58
  """Helper class to store fields with its customizers.
@@ -141,7 +165,7 @@ class VirtualColumn:
141
165
  substitute_expression=substitute_expression.replace('.', '_'),
142
166
  )
143
167
  if not _is_quoted_string(field):
144
- raise exceptions.GarfFieldException(f"Incorrect field '{field}'.")
168
+ raise GarfFieldError(f"Incorrect field '{field}'.")
145
169
  field = field.replace("'", '').replace('"', '')
146
170
  field = field.format(**macros) if macros else field
147
171
  return VirtualColumn(type='built-in', value=field)
@@ -183,9 +207,7 @@ class ExtractedLineElements:
183
207
  else:
184
208
  customizer = {}
185
209
  if virtual_column and not alias:
186
- raise exceptions.GarfVirtualColumnException(
187
- 'Virtual attributes should be aliased'
188
- )
210
+ raise GarfVirtualColumnError('Virtual attributes should be aliased')
189
211
  return ExtractedLineElements(
190
212
  field=_format_type_field_name(field)
191
213
  if not virtual_column and field
@@ -209,15 +231,14 @@ class BaseQueryElements:
209
231
  """Contains raw query and parsed elements.
210
232
 
211
233
  Attributes:
212
- query_title: Title of the query that needs to be parsed.
213
- query_text: Text of the query that needs to be parsed.
234
+ title: Title of the query that needs to be parsed.
235
+ text: Text of the query that needs to be parsed.
214
236
  resource_name: Name of Google Ads API reporting resource.
215
237
  fields: Ads API fields that need to be fetched.
216
238
  column_names: Friendly names for fields which are used when saving data
217
239
  column_names: Friendly names for fields which are used when saving data
218
240
  customizers: Attributes of fields that need to be be extracted.
219
241
  virtual_columns: Attributes of fields that need to be be calculated.
220
- is_constant_resource: Whether resource considered a constant one.
221
242
  is_builtin_query: Whether query is built-in.
222
243
  """
223
244
 
@@ -234,6 +255,7 @@ class BaseQueryElements:
234
255
  virtual_columns: dict[str, VirtualColumn] = dataclasses.field(
235
256
  default_factory=dict
236
257
  )
258
+ is_builtin_query: bool = False
237
259
 
238
260
  def __eq__(self, other: BaseQueryElements) -> bool: # noqa: D105
239
261
  return (
@@ -381,9 +403,7 @@ class QuerySpecification(CommonParametersMixin, TemplateProcessorMixin):
381
403
  try:
382
404
  self.query.text = query_text.format(**self.macros).strip()
383
405
  except KeyError as e:
384
- raise exceptions.GarfMacroException(
385
- f'No value provided for macro {str(e)}.'
386
- ) from e
406
+ raise GarfMacroError(f'No value provided for macro {str(e)}.') from e
387
407
  return self
388
408
 
389
409
  def remove_comments(self) -> Self:
@@ -424,9 +444,7 @@ class QuerySpecification(CommonParametersMixin, TemplateProcessorMixin):
424
444
  ):
425
445
  self.query.resource_name = str(resource_name[0]).strip()
426
446
  return self
427
- raise exceptions.GarfResourceException(
428
- f'No resource found in query: {self.query.text}'
429
- )
447
+ raise GarfResourceError(f'No resource found in query: {self.query.text}')
430
448
 
431
449
  def extract_fields(self) -> Self:
432
450
  for line in self._extract_query_lines():
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ import json
27
27
  import warnings
28
28
  from collections import defaultdict
29
29
  from collections.abc import MutableSequence, Sequence
30
- from typing import Generator, Literal
30
+ from typing import Generator, Literal, get_args
31
31
 
32
32
  from garf_core import exceptions, parsers, query_editor
33
33
 
@@ -414,8 +414,8 @@ class GarfReport:
414
414
  import polars as pl
415
415
  except ImportError as e:
416
416
  raise ImportError(
417
- 'Please install garf-io with Polars support '
418
- '- `pip install garf-io[polars]`'
417
+ 'Please install garf-core with Polars support '
418
+ '- `pip install garf-core[polars]`'
419
419
  ) from e
420
420
  return cls(
421
421
  results=df.to_numpy().tolist(), column_names=list(df.schema.keys())
@@ -438,11 +438,63 @@ class GarfReport:
438
438
  import pandas as pd
439
439
  except ImportError as e:
440
440
  raise ImportError(
441
- 'Please install garf-io with Pandas support '
442
- '- `pip install garf-io[pandas]`'
441
+ 'Please install garf-core with Pandas support '
442
+ '- `pip install garf-core[pandas]`'
443
443
  ) from e
444
444
  return cls(results=df.values.tolist(), column_names=list(df.columns.values))
445
445
 
446
+ @classmethod
447
+ def from_json(cls, json_str: str) -> GarfReport:
448
+ """Creates a GarfReport object from a JSON string.
449
+
450
+ Args:
451
+ json_str: JSON string representation of the data.
452
+
453
+ Returns:
454
+ Report build from a json string.
455
+
456
+ Raises:
457
+ TypeError: If any value in the JSON data is not a supported type.
458
+ ValueError: If `data` is a list but not all dictionaries
459
+ have the same keys.
460
+ """
461
+ data = json.loads(json_str)
462
+
463
+ def validate_value(value):
464
+ if not isinstance(value, get_args(parsers.ApiRowElement)):
465
+ raise TypeError(
466
+ f'Unsupported type {type(value)} for value {value}. '
467
+ 'Expected types: int, float, str, bool, list, or None.'
468
+ )
469
+ return value
470
+
471
+ # Case 1: `data` is a dictionary
472
+ if isinstance(data, dict):
473
+ column_names = list(data.keys())
474
+ if not data.values():
475
+ results = []
476
+ else:
477
+ results = [[validate_value(value) for value in data.values()]]
478
+
479
+ # Case 2: `data` is a list of dictionaries, each representing a row
480
+ elif isinstance(data, list):
481
+ column_names = list(data[0].keys()) if data else []
482
+ for row in data:
483
+ if not isinstance(row, dict):
484
+ raise TypeError('All elements in the list must be dictionaries.')
485
+ if list(row.keys()) != column_names:
486
+ raise ValueError(
487
+ 'All dictionaries must have consistent keys in the same order.'
488
+ )
489
+ results = [
490
+ [validate_value(value) for value in row.values()] for row in data
491
+ ]
492
+ else:
493
+ raise TypeError(
494
+ 'Input JSON must be a dictionary or a list of dictionaries.'
495
+ )
496
+ return cls(results=results, column_names=column_names)
497
+
446
498
 
447
499
  class GarfRow:
448
500
  """Helper class to simplify iteration of GarfReport.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLf
1
+ # Copyright 2025 Google LLf
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -126,12 +126,17 @@ class RestApiReportFetcher(ApiReportFetcher):
126
126
  self,
127
127
  endpoint: str,
128
128
  parser: parsers.BaseParser = parsers.DictParser,
129
+ query_specification_builder: query_editor.QuerySpecification = (
130
+ query_editor.QuerySpecification
131
+ ),
132
+ **kwargs: str,
129
133
  ) -> None:
130
134
  """Instantiates RestApiReportFetcher.
131
135
 
132
136
  Args:
133
137
  endpoint: URL of API endpoint.
134
138
  parser: Type of parser to convert API response.
139
+ query_specification_builder: Class to perform query parsing.
135
140
  """
136
- self.api_client = api_clients.RestApiClient(endpoint)
137
- self.parser = parser()
141
+ api_client = api_clients.RestApiClient(endpoint)
142
+ super().__init__(api_client, parser, query_specification_builder, **kwargs)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: garf-core
3
- Version: 0.0.8
3
+ Version: 0.0.10
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>
6
6
  License: Apache 2.0
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.9
10
10
  Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
16
  Classifier: Operating System :: OS Independent
@@ -17,8 +18,10 @@ Classifier: License :: OSI Approved :: Apache Software License
17
18
  Requires-Python: >=3.8
18
19
  Description-Content-Type: text/markdown
19
20
  Requires-Dist: python-dateutil
20
- Requires-Dist: jinja2==3.1.4
21
+ Requires-Dist: jinja2
21
22
  Requires-Dist: typing-extensions
23
+ Requires-Dist: requests
24
+ Requires-Dist: pyyaml
22
25
  Provides-Extra: pandas
23
26
  Requires-Dist: pandas; extra == "pandas"
24
27
  Provides-Extra: polars
@@ -1,6 +1,8 @@
1
1
  python-dateutil
2
- jinja2==3.1.4
2
+ jinja2
3
3
  typing-extensions
4
+ requests
5
+ pyyaml
4
6
 
5
7
  [all]
6
8
  garf-core[pandas,polars]
@@ -6,8 +6,10 @@ build-backend = "setuptools.build_meta"
6
6
  name = "garf-core"
7
7
  dependencies = [
8
8
  "python-dateutil",
9
- "jinja2==3.1.4",
9
+ "jinja2",
10
10
  "typing-extensions",
11
+ "requests",
12
+ "pyyaml",
11
13
  ]
12
14
  authors = [
13
15
  {name = "Google Inc. (gTech gPS CSE team)", email = "no-reply@google.com"},
@@ -23,6 +25,7 @@ classifiers = [
23
25
  "Programming Language :: Python :: 3.10",
24
26
  "Programming Language :: Python :: 3.11",
25
27
  "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
26
29
  "Intended Audience :: Developers",
27
30
  "Topic :: Software Development :: Libraries :: Python Modules",
28
31
  "Operating System :: OS Independent",
File without changes
File without changes