garf-core 0.0.10__tar.gz → 0.0.12__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,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: garf-core
3
- Version: 0.0.10
3
+ Version: 0.0.12
4
4
  Summary: Abstracts fetching data from API based on provided SQL-like query.
5
- Author-email: "Google Inc. (gTech gPS CSE team)" <no-reply@google.com>
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
7
7
  Classifier: Programming Language :: Python :: 3 :: Only
8
8
  Classifier: Programming Language :: Python :: 3.8
@@ -22,6 +22,7 @@ Requires-Dist: jinja2
22
22
  Requires-Dist: typing-extensions
23
23
  Requires-Dist: requests
24
24
  Requires-Dist: pyyaml
25
+ Requires-Dist: pydantic
25
26
  Provides-Extra: pandas
26
27
  Requires-Dist: pandas; extra == "pandas"
27
28
  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.10'
15
+ __version__ = '0.0.12'
@@ -23,11 +23,27 @@ import re
23
23
  from typing import Generator
24
24
 
25
25
  import jinja2
26
+ import pydantic
26
27
  from dateutil import relativedelta
27
28
  from typing_extensions import Self
28
29
 
29
30
  from garf_core import exceptions
30
31
 
32
+ QueryParameters = dict[str, str | float | int]
33
+
34
+
35
+ class GarfQueryParameters(pydantic.BaseModel):
36
+ """Parameters for dynamically changing text of a query."""
37
+
38
+ macro: QueryParameters | None = None
39
+ template: QueryParameters | None = None
40
+
41
+ def model_post_init(self, __context__) -> None:
42
+ if self.macro is None:
43
+ self.macro = {}
44
+ if self.template is None:
45
+ self.template = {}
46
+
31
47
 
32
48
  class GarfQueryError(exceptions.GarfError):
33
49
  """Base exception for Garf queries."""
@@ -53,9 +69,12 @@ class GarfResourceError(GarfQueryError):
53
69
  """Specifies incorrect resource name in the query."""
54
70
 
55
71
 
56
- @dataclasses.dataclass
57
- class ProcessedField:
58
- """Helper class to store fields with its customizers.
72
+ class GarfBuiltInQueryError(GarfQueryError):
73
+ """Specifies non-existing builtin query."""
74
+
75
+
76
+ class ProcessedField(pydantic.BaseModel):
77
+ """Sore field with its customizers.
59
78
 
60
79
  Attributes:
61
80
  field: Extractable field.
@@ -136,7 +155,7 @@ class VirtualColumn:
136
155
  substitute_expression: str | None = None
137
156
 
138
157
  @classmethod
139
- def from_raw(cls, field: str, macros: dict) -> VirtualColumn:
158
+ def from_raw(cls, field: str, macros: QueryParameters) -> VirtualColumn:
140
159
  """Converts a field to virtual column."""
141
160
  if field.isdigit():
142
161
  field = int(field)
@@ -189,16 +208,16 @@ class ExtractedLineElements:
189
208
 
190
209
  @classmethod
191
210
  def from_query_line(
192
- cls, line: str, macros: dict = {}
211
+ cls,
212
+ line: str,
213
+ macros: QueryParameters | None = None,
193
214
  ) -> ExtractedLineElements:
215
+ if macros is None:
216
+ macros = {}
194
217
  field, *alias = re.split(' [Aa][Ss] ', line)
195
218
  processed_field = ProcessedField.from_raw(field)
196
219
  field = processed_field.field
197
- if field:
198
- # if field.is_valid:
199
- virtual_column = None
200
- else:
201
- virtual_column = VirtualColumn.from_raw(field, macros)
220
+ virtual_column = None if field else VirtualColumn.from_raw(field, macros)
202
221
  if alias and processed_field.customizer_type:
203
222
  customizer = {
204
223
  'type': processed_field.customizer_type,
@@ -300,11 +319,11 @@ class CommonParametersMixin:
300
319
 
301
320
  class TemplateProcessorMixin:
302
321
  def replace_params_template(
303
- self, query_text: str, params: dict | None = None
322
+ self, query_text: str, params: GarfQueryParameters | None = None
304
323
  ) -> str:
305
324
  logging.debug('Original query text:\n%s', query_text)
306
325
  if params:
307
- if templates := params.get('template'):
326
+ if templates := params.template:
308
327
  query_templates = {
309
328
  name: value for name, value in templates.items() if name in query_text
310
329
  }
@@ -315,7 +334,7 @@ class TemplateProcessorMixin:
315
334
  query_text = self.expand_jinja(query_text, {})
316
335
  else:
317
336
  query_text = self.expand_jinja(query_text, {})
318
- if macros := params.get('macro'):
337
+ if macros := params.macro:
319
338
  query_text = query_text.format(**macros)
320
339
  logging.debug('Query text after macro substitution:\n%s', query_text)
321
340
  else:
@@ -323,7 +342,7 @@ class TemplateProcessorMixin:
323
342
  return query_text
324
343
 
325
344
  def expand_jinja(
326
- self, query_text: str, template_params: dict | None = None
345
+ self, query_text: str, template_params: QueryParameters | None = None
327
346
  ) -> str:
328
347
  file_inclusions = ('% include', '% import', '% extend')
329
348
  if any(file_inclusion in query_text for file_inclusion in file_inclusions):
@@ -360,7 +379,7 @@ class QuerySpecification(CommonParametersMixin, TemplateProcessorMixin):
360
379
  self,
361
380
  text: str,
362
381
  title: str | None = None,
363
- args: dict | None = None,
382
+ args: GarfQueryParameters | None = GarfQueryParameters(),
364
383
  **kwargs: str,
365
384
  ) -> None:
366
385
  """Instantiates QuerySpecification based on text, title and optional args.
@@ -371,35 +390,40 @@ class QuerySpecification(CommonParametersMixin, TemplateProcessorMixin):
371
390
  args: Optional parameters to be dynamically injected into query text.
372
391
  api_version: Version of Google Ads API.
373
392
  """
374
- self.args = args or {}
393
+ self.args = args or GarfQueryParameters()
375
394
  self.query = BaseQueryElements(title=title, text=text)
376
395
 
377
396
  @property
378
- def macros(self) -> dict[str, str]:
397
+ def macros(self) -> QueryParameters:
379
398
  """Returns macros with injected common parameters."""
380
399
  common_params = dict(self.common_params)
381
- if macros := self.args.get('macro'):
400
+ if macros := self.args.macro:
382
401
  common_params.update(macros)
383
402
  return common_params
384
403
 
385
404
  def generate(self) -> BaseQueryElements:
386
- return (
387
- self.remove_comments()
388
- .expand()
389
- .extract_resource_name()
390
- .remove_trailing_comma()
405
+ self.remove_comments().expand().extract_resource_name()
406
+ if self.query.resource_name.startswith('builtin'):
407
+ return BaseQueryElements(
408
+ title=self.query.resource_name.replace('builtin.', ''),
409
+ text=self.query.text,
410
+ resource_name=self.query.resource_name,
411
+ is_builtin_query=True,
412
+ )
413
+ (
414
+ self.remove_trailing_comma()
391
415
  .extract_fields()
392
416
  .extract_filters()
393
417
  .extract_sorts()
394
418
  .extract_column_names()
395
419
  .extract_virtual_columns()
396
420
  .extract_customizers()
397
- .query
398
421
  )
422
+ return self.query
399
423
 
400
424
  def expand(self) -> Self:
401
425
  """Applies necessary transformations to query."""
402
- query_text = self.expand_jinja(self.query.text, self.args.get('template'))
426
+ query_text = self.expand_jinja(self.query.text, self.args.template)
403
427
  try:
404
428
  self.query.text = query_text.format(**self.macros).strip()
405
429
  except KeyError as e:
@@ -213,13 +213,22 @@ class GarfReport:
213
213
  ) from e
214
214
  return pd.DataFrame(data=self.results, columns=self.column_names)
215
215
 
216
- def to_json(self) -> str:
216
+ def to_jsonl(self) -> str:
217
+ """Converts report to JSON Lines."""
218
+ return self.to_json(output='jsonl')
219
+
220
+ def to_json(self, output: Literal['json', 'jsonl'] = 'json') -> str:
217
221
  """Converts report to JSON.
218
222
 
223
+ Args:
224
+ output: Format of json file (json or jsonl).
225
+
219
226
  Returns:
220
227
  JSON from report results and column_names.
221
228
  """
222
- return json.dumps(self.to_list(row_type='dict'))
229
+ if output == 'json':
230
+ return json.dumps(self.to_list(row_type='dict'))
231
+ return '\n'.join(json.dumps(row) for row in self.to_list(row_type='dict'))
223
232
 
224
233
  def get_value(
225
234
  self, column_index: int = 0, row_index: int = 0
@@ -11,17 +11,19 @@
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
+
15
+ # pylint: disable=C0330, g-bad-import-order, g-multiple-import
16
+
14
17
  """Module for getting data from API based on a query.
15
18
 
16
19
  ApiReportFetcher performs fetching data from API, parsing it
17
20
  and returning GarfReport.
18
21
  """
19
- # pylint: disable=C0330, g-bad-import-order, g-multiple-import
20
22
 
21
23
  from __future__ import annotations
22
24
 
23
25
  import logging
24
- from typing import Any
26
+ from typing import Any, Callable
25
27
 
26
28
  from garf_core import (
27
29
  api_clients,
@@ -49,6 +51,8 @@ class ApiReportFetcher:
49
51
  query_specification_builder: query_editor.QuerySpecification = (
50
52
  query_editor.QuerySpecification
51
53
  ),
54
+ builtin_queries: dict[str, Callable[[ApiReportFetcher], report.GarfReport]]
55
+ | None = None,
52
56
  **kwargs: str,
53
57
  ) -> None:
54
58
  """Instantiates ApiReportFetcher based on provided api client.
@@ -57,16 +61,26 @@ class ApiReportFetcher:
57
61
  api_client: Instantiated api client.
58
62
  parser: Type of parser to convert API response.
59
63
  query_specification_builder: Class to perform query parsing.
64
+ builtin_queries:
65
+ Mapping between query name and function for generating GarfReport.
60
66
  """
61
67
  self.api_client = api_client
62
68
  self.parser = parser()
63
69
  self.query_specification_builder = query_specification_builder
64
70
  self.query_args = kwargs
71
+ self.builtin_queries = builtin_queries or {}
72
+
73
+ def add_builtin_queries(
74
+ self,
75
+ builtin_queries: dict[str, Callable[[ApiReportFetcher], report.GarfReport]],
76
+ ) -> None:
77
+ """Adds new built-in queries to the fetcher."""
78
+ self.builtin_queries.update(builtin_queries)
65
79
 
66
80
  async def afetch(
67
81
  self,
68
82
  query_specification: str | query_editor.QueryElements,
69
- args: dict[str, Any] | None = None,
83
+ args: query_editor.GarfQueryParameters | None = None,
70
84
  **kwargs: str,
71
85
  ) -> report.GarfReport:
72
86
  """Asynchronously fetches data from API based on query_specification.
@@ -84,7 +98,7 @@ class ApiReportFetcher:
84
98
  def fetch(
85
99
  self,
86
100
  query_specification: str | query_editor.QuerySpecification,
87
- args: dict[str, Any] | None = None,
101
+ args: query_editor.GarfQueryParameters | None = None,
88
102
  **kwargs: str,
89
103
  ) -> report.GarfReport:
90
104
  """Fetches data from API based on query_specification.
@@ -101,12 +115,21 @@ class ApiReportFetcher:
101
115
  GarfExecutorException:
102
116
  When customer_ids are not provided or API returned error.
103
117
  """
118
+ if args is None:
119
+ args = query_editor.GarfQueryParameters()
104
120
  if not isinstance(query_specification, query_editor.QuerySpecification):
105
121
  query_specification = self.query_specification_builder(
106
122
  text=str(query_specification),
107
123
  args=args,
108
124
  )
109
125
  query = query_specification.generate()
126
+ if query.is_builtin_query:
127
+ if not (builtin_report := self.builtin_queries.get(query.title)):
128
+ raise query_editor.GarfBuiltInQueryError(
129
+ f'Cannot find the built-in query "{query.title}"'
130
+ )
131
+ return builtin_report(self, **kwargs)
132
+
110
133
  response = self.api_client.get_response(query, **kwargs)
111
134
  parsed_response = self.parser.parse_response(response, query)
112
135
  return report.GarfReport(
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: garf-core
3
- Version: 0.0.10
3
+ Version: 0.0.12
4
4
  Summary: Abstracts fetching data from API based on provided SQL-like query.
5
- Author-email: "Google Inc. (gTech gPS CSE team)" <no-reply@google.com>
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
7
7
  Classifier: Programming Language :: Python :: 3 :: Only
8
8
  Classifier: Programming Language :: Python :: 3.8
@@ -22,6 +22,7 @@ Requires-Dist: jinja2
22
22
  Requires-Dist: typing-extensions
23
23
  Requires-Dist: requests
24
24
  Requires-Dist: pyyaml
25
+ Requires-Dist: pydantic
25
26
  Provides-Extra: pandas
26
27
  Requires-Dist: pandas; extra == "pandas"
27
28
  Provides-Extra: polars
@@ -3,6 +3,7 @@ jinja2
3
3
  typing-extensions
4
4
  requests
5
5
  pyyaml
6
+ pydantic
6
7
 
7
8
  [all]
8
9
  garf-core[pandas,polars]
@@ -10,9 +10,11 @@ dependencies = [
10
10
  "typing-extensions",
11
11
  "requests",
12
12
  "pyyaml",
13
+ "pydantic",
13
14
  ]
14
15
  authors = [
15
16
  {name = "Google Inc. (gTech gPS CSE team)", email = "no-reply@google.com"},
17
+ {name = "Andrei Markin", email = "andrey.markin.ppc@gmail.com"},
16
18
  ]
17
19
  requires-python = ">=3.8"
18
20
  description = "Abstracts fetching data from API based on provided SQL-like query."
File without changes
File without changes