garf-core 0.0.11__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.
- {garf_core-0.0.11 → garf_core-0.0.12}/PKG-INFO +3 -2
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/__init__.py +1 -1
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/query_editor.py +49 -25
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/report_fetcher.py +27 -4
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core.egg-info/PKG-INFO +3 -2
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core.egg-info/requires.txt +1 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/pyproject.toml +2 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/README.md +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/api_clients.py +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/base_query.py +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/exceptions.py +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/parsers.py +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core/report.py +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core.egg-info/SOURCES.txt +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core.egg-info/dependency_links.txt +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core.egg-info/entry_points.txt +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/garf_core.egg-info/top_level.txt +0 -0
- {garf_core-0.0.11 → garf_core-0.0.12}/setup.cfg +0 -0
@@ -1,8 +1,8 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: garf-core
|
3
|
-
Version: 0.0.
|
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
|
@@ -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
|
-
|
57
|
-
|
58
|
-
|
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:
|
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,
|
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:
|
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.
|
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.
|
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:
|
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:
|
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) ->
|
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.
|
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
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
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.
|
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:
|
@@ -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:
|
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:
|
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.
|
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
|
@@ -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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|