garf-executors 0.0.1__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.
Potentially problematic release.
This version of garf-executors might be problematic. Click here for more details.
- garf_executors/__init__.py +26 -0
- garf_executors/api_executor.py +98 -0
- garf_executors/bq_executor.py +123 -0
- garf_executors/entrypoints/__init__.py +0 -0
- garf_executors/entrypoints/cli/__init__.py +0 -0
- garf_executors/entrypoints/cli/api.py +213 -0
- garf_executors/entrypoints/cli/bq.py +112 -0
- garf_executors/entrypoints/cli/gaarf.py +213 -0
- garf_executors/entrypoints/cli/sql.py +109 -0
- garf_executors/entrypoints/utils.py +470 -0
- garf_executors/sql_executor.py +79 -0
- garf_executors-0.0.1.dist-info/METADATA +30 -0
- garf_executors-0.0.1.dist-info/RECORD +16 -0
- garf_executors-0.0.1.dist-info/WHEEL +5 -0
- garf_executors-0.0.1.dist-info/entry_points.txt +3 -0
- garf_executors-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Copyright 2022 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Module for various helpers for executing Garf as CLI tool."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import datetime
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import traceback
|
|
24
|
+
from collections.abc import MutableSequence, Sequence
|
|
25
|
+
from typing import Any, Callable, TypedDict
|
|
26
|
+
|
|
27
|
+
import smart_open
|
|
28
|
+
import yaml
|
|
29
|
+
from dateutil import relativedelta
|
|
30
|
+
from rich import logging as rich_logging
|
|
31
|
+
|
|
32
|
+
from garf_core import query_editor
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GarfQueryParameters(TypedDict):
|
|
36
|
+
"""Annotation for dictionary of query specific parameters passed via CLI.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
macros: Mapping for elements that will be replaced in the queries.
|
|
40
|
+
template: Mapping for elements that will rendered via Jinja templates.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
macros: dict[str, str]
|
|
44
|
+
template: dict[str, str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclasses.dataclass
|
|
48
|
+
class BaseConfig:
|
|
49
|
+
"""Base config to inherit other configs from."""
|
|
50
|
+
|
|
51
|
+
def __add__(self, other: BaseConfig) -> BaseConfig:
|
|
52
|
+
"""Creates new config of the same type from two configs.
|
|
53
|
+
|
|
54
|
+
Parameters from added config overwrite already present parameters.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
other: Config that could be merged with the original one.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
New config with values from both configs.
|
|
61
|
+
"""
|
|
62
|
+
right_dict = _remove_empty_values(self.__dict__)
|
|
63
|
+
left_dict = _remove_empty_values(other.__dict__)
|
|
64
|
+
new_dict = {**right_dict, **left_dict}
|
|
65
|
+
return self.__class__(**new_dict)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(
|
|
69
|
+
cls, config_parameters: dict[str, str | GarfQueryParameters]
|
|
70
|
+
) -> BaseConfig:
|
|
71
|
+
"""Builds config from provided parameters ignoring empty ones."""
|
|
72
|
+
return cls(**_remove_empty_values(config_parameters))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclasses.dataclass
|
|
76
|
+
class GarfConfig(BaseConfig):
|
|
77
|
+
"""Stores values to run garf from command line.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
account:
|
|
81
|
+
Account(s) to get data from.
|
|
82
|
+
output:
|
|
83
|
+
Specifies where to store fetched data (console, csv, BQ.)
|
|
84
|
+
api_version:
|
|
85
|
+
Google Ads API version.
|
|
86
|
+
params:
|
|
87
|
+
Any parameters passed to Garf query for substitution.
|
|
88
|
+
writer_params:
|
|
89
|
+
Any parameters that can be passed to writer for data saving.
|
|
90
|
+
customer_ids_query:
|
|
91
|
+
Query text to limit accounts fetched from Ads API.
|
|
92
|
+
customer_ids_query_file:
|
|
93
|
+
Path to query to limit accounts fetched from Ads API.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
account: str | list[str] | None = None
|
|
97
|
+
output: str = 'console'
|
|
98
|
+
params: GarfQueryParameters = dataclasses.field(default_factory=dict)
|
|
99
|
+
writer_params: dict[str, str | int] = dataclasses.field(default_factory=dict)
|
|
100
|
+
customer_ids_query: str | None = None
|
|
101
|
+
customer_ids_query_file: str | None = None
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
"""Ensures that values passed during __init__ correctly formatted."""
|
|
105
|
+
if isinstance(self.account, MutableSequence):
|
|
106
|
+
self.account = [
|
|
107
|
+
str(account).replace('-', '').strip() for account in self.account
|
|
108
|
+
]
|
|
109
|
+
else:
|
|
110
|
+
self.account = (
|
|
111
|
+
str(self.account).replace('-', '').strip() if self.account else None
|
|
112
|
+
)
|
|
113
|
+
self.writer_params = {
|
|
114
|
+
key.replace('-', '_'): value for key, value in self.writer_params.items()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GarfConfigException(Exception):
|
|
119
|
+
"""Exception for invalid GarfConfig."""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclasses.dataclass
|
|
123
|
+
class GarfBqConfig(BaseConfig):
|
|
124
|
+
"""Stores values to run garf-bq from command line.
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
project:
|
|
128
|
+
Google Cloud project name.
|
|
129
|
+
dataset_location:
|
|
130
|
+
Location of BigQuery dataset.
|
|
131
|
+
params:
|
|
132
|
+
Any parameters passed to BigQuery query for substitution.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
project: str | None = None
|
|
136
|
+
dataset_location: str | None = None
|
|
137
|
+
params: GarfQueryParameters = dataclasses.field(default_factory=dict)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclasses.dataclass
|
|
141
|
+
class GarfSqlConfig(BaseConfig):
|
|
142
|
+
"""Stores values to run garf-sql from command line.
|
|
143
|
+
|
|
144
|
+
Attributes:
|
|
145
|
+
connection_string:
|
|
146
|
+
Connection string to SqlAlchemy database engine.
|
|
147
|
+
params:
|
|
148
|
+
Any parameters passed to SQL query for substitution.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
connection_string: str | None = None
|
|
152
|
+
params: GarfQueryParameters = dataclasses.field(default_factory=dict)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ConfigBuilder:
|
|
156
|
+
"""Builds config of provided type.
|
|
157
|
+
|
|
158
|
+
Config can be created from file, build from arguments or both.
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
config: Concrete config class that needs to be built.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
_config_mapping: dict[str, BaseConfig] = {
|
|
165
|
+
'garf': GarfConfig,
|
|
166
|
+
'garf-bq': GarfBqConfig,
|
|
167
|
+
'garf-sql': GarfSqlConfig,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def __init__(self, config_type: str) -> None:
|
|
171
|
+
"""Sets concrete config type.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
config_type: Type of config that should be built.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
GarfConfigException: When incorrect config_type is supplied.
|
|
178
|
+
"""
|
|
179
|
+
if config_type not in self._config_mapping:
|
|
180
|
+
raise GarfConfigException(f'Invalid config_type: {config_type}')
|
|
181
|
+
self._config_type = config_type
|
|
182
|
+
self.config = self._config_mapping.get(config_type)
|
|
183
|
+
|
|
184
|
+
def build(
|
|
185
|
+
self, parameters: dict[str, str], cli_named_args: Sequence[str]
|
|
186
|
+
) -> BaseConfig | None:
|
|
187
|
+
"""Builds config from file, build from arguments or both ways.
|
|
188
|
+
|
|
189
|
+
When there are both config_file and CLI arguments the latter have more
|
|
190
|
+
priority.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
parameters: Parsed CLI arguments.
|
|
194
|
+
cli_named_args: Unparsed CLI args in a form `--key.subkey=value`.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Concrete config with injected values.
|
|
198
|
+
"""
|
|
199
|
+
if not (garf_config_path := parameters.get('garf_config')):
|
|
200
|
+
return self._build_config(parameters, cli_named_args)
|
|
201
|
+
config_file = self._load_config(garf_config_path)
|
|
202
|
+
config_cli = self._build_config(
|
|
203
|
+
parameters, cli_named_args, init_defaults=False
|
|
204
|
+
)
|
|
205
|
+
if config_file and config_cli:
|
|
206
|
+
config_file = config_file + config_cli
|
|
207
|
+
return config_file
|
|
208
|
+
|
|
209
|
+
def _build_config(
|
|
210
|
+
self,
|
|
211
|
+
parameters: dict[str, str],
|
|
212
|
+
cli_named_args: Sequence[str],
|
|
213
|
+
init_defaults: bool = True,
|
|
214
|
+
) -> BaseConfig | None:
|
|
215
|
+
"""Builds config from named and unnamed CLI parameters.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
parameters: Parsed CLI arguments.
|
|
219
|
+
cli_named_args: Unparsed CLI args in a form `--key.subkey=value`.
|
|
220
|
+
init_defaults: Whether to provided default config values if
|
|
221
|
+
expected parameter is missing
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Concrete config with injected values.
|
|
225
|
+
"""
|
|
226
|
+
output = parameters.get('output')
|
|
227
|
+
config_parameters = {
|
|
228
|
+
k: v for k, v in parameters.items() if k in self.config.__annotations__
|
|
229
|
+
}
|
|
230
|
+
cli_params = ParamsParser(['macro', 'template', output]).parse(
|
|
231
|
+
cli_named_args
|
|
232
|
+
)
|
|
233
|
+
cli_params = _remove_empty_values(cli_params)
|
|
234
|
+
if output and (writer_params := cli_params.get(output)):
|
|
235
|
+
_ = cli_params.pop(output)
|
|
236
|
+
config_parameters.update({'writer_params': writer_params})
|
|
237
|
+
if cli_params:
|
|
238
|
+
config_parameters.update({'params': cli_params})
|
|
239
|
+
if not config_parameters:
|
|
240
|
+
return None
|
|
241
|
+
if init_defaults:
|
|
242
|
+
return self.config.from_dict(config_parameters)
|
|
243
|
+
return self.config(**config_parameters)
|
|
244
|
+
|
|
245
|
+
def _load_config(self, garf_config_path: str) -> BaseConfig:
|
|
246
|
+
"""Loads config from provided path.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
garf_config_path: Path to local or remote storage.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Concreate config with values taken from config file.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
GarfConfigException:
|
|
256
|
+
If config file missing `garf` section.
|
|
257
|
+
"""
|
|
258
|
+
with smart_open.open(garf_config_path, encoding='utf-8') as f:
|
|
259
|
+
config = yaml.safe_load(f)
|
|
260
|
+
garf_section = config.get(self._config_type)
|
|
261
|
+
if not garf_section:
|
|
262
|
+
raise GarfConfigException(
|
|
263
|
+
f'Invalid config, must have `{self._config_type}` section!'
|
|
264
|
+
)
|
|
265
|
+
config_parameters = {
|
|
266
|
+
k: v for k, v in garf_section.items() if k in self.config.__annotations__
|
|
267
|
+
}
|
|
268
|
+
if params := garf_section.get('params', {}):
|
|
269
|
+
config_parameters.update({'params': params})
|
|
270
|
+
if writer_params := garf_section.get(garf_section.get('output', '')):
|
|
271
|
+
config_parameters.update({'writer_params': writer_params})
|
|
272
|
+
return self.config(**config_parameters)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class ParamsParser:
|
|
276
|
+
def __init__(self, identifiers: Sequence[str]) -> None:
|
|
277
|
+
self.identifiers = identifiers
|
|
278
|
+
|
|
279
|
+
def parse(self, params: Sequence) -> dict[str, dict | None]:
|
|
280
|
+
return {
|
|
281
|
+
identifier: self._parse_params(identifier, params)
|
|
282
|
+
for identifier in self.identifiers
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
def _parse_params(self, identifier: str, params: Sequence[Any]) -> dict:
|
|
286
|
+
parsed_params = {}
|
|
287
|
+
if params:
|
|
288
|
+
raw_params = [param.split('=', maxsplit=1) for param in params]
|
|
289
|
+
for param in raw_params:
|
|
290
|
+
param_pair = self._identify_param_pair(identifier, param)
|
|
291
|
+
if param_pair:
|
|
292
|
+
parsed_params.update(param_pair)
|
|
293
|
+
return parsed_params
|
|
294
|
+
|
|
295
|
+
def _identify_param_pair(
|
|
296
|
+
self, identifier: str, param: Sequence[str]
|
|
297
|
+
) -> dict[str, Any] | None:
|
|
298
|
+
key = param[0]
|
|
299
|
+
if not identifier or identifier not in key:
|
|
300
|
+
return None
|
|
301
|
+
provided_identifier, key = key.split('.')
|
|
302
|
+
if provided_identifier.replace('--', '') not in self.identifiers:
|
|
303
|
+
raise GarfParamsException(
|
|
304
|
+
f'CLI argument {provided_identifier} is not supported'
|
|
305
|
+
f", supported arguments {', '.join(self.identifiers)}"
|
|
306
|
+
)
|
|
307
|
+
key = key.replace('-', '_')
|
|
308
|
+
if len(param) == 2:
|
|
309
|
+
# TODO: b/337860595 - Ensure that writer params are converted to int
|
|
310
|
+
return {key: param[1]}
|
|
311
|
+
raise GarfParamsException(
|
|
312
|
+
f'{identifier} {key} is invalid,'
|
|
313
|
+
f'--{identifier}.key=value is the correct format'
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class GarfParamsException(Exception):
|
|
318
|
+
"""Defines exception for incorrect parameters."""
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def convert_date(date_string: str) -> str:
|
|
322
|
+
"""Converts specific dates parameters to actual dates.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Date string in YYYY-MM-DD format.
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
ValueError:
|
|
329
|
+
If dynamic lookback value (:YYYYMMDD-N) is incorrect.
|
|
330
|
+
"""
|
|
331
|
+
if isinstance(date_string, list) or date_string.find(':YYYY') == -1:
|
|
332
|
+
return date_string
|
|
333
|
+
current_date = datetime.date.today()
|
|
334
|
+
date_object = date_string.split('-')
|
|
335
|
+
base_date = date_object[0]
|
|
336
|
+
if len(date_object) == 2:
|
|
337
|
+
try:
|
|
338
|
+
days_ago = int(date_object[1])
|
|
339
|
+
except ValueError as e:
|
|
340
|
+
raise ValueError(
|
|
341
|
+
'Must provide numeric value for a number lookback period, '
|
|
342
|
+
'i.e. :YYYYMMDD-1'
|
|
343
|
+
) from e
|
|
344
|
+
else:
|
|
345
|
+
days_ago = 0
|
|
346
|
+
if base_date == ':YYYY':
|
|
347
|
+
new_date = datetime.datetime(current_date.year, 1, 1)
|
|
348
|
+
delta = relativedelta.relativedelta(years=days_ago)
|
|
349
|
+
elif base_date == ':YYYYMM':
|
|
350
|
+
new_date = datetime.datetime(current_date.year, current_date.month, 1)
|
|
351
|
+
delta = relativedelta.relativedelta(months=days_ago)
|
|
352
|
+
elif base_date == ':YYYYMMDD':
|
|
353
|
+
new_date = current_date
|
|
354
|
+
delta = relativedelta.relativedelta(days=days_ago)
|
|
355
|
+
return (new_date - delta).strftime('%Y-%m-%d')
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class ConfigSaver:
|
|
359
|
+
def __init__(self, path: str) -> None:
|
|
360
|
+
self.path = path
|
|
361
|
+
|
|
362
|
+
def save(self, garf_config: BaseConfig):
|
|
363
|
+
if os.path.exists(self.path):
|
|
364
|
+
with smart_open.open(self.path, 'r', encoding='utf-8') as f:
|
|
365
|
+
config = yaml.safe_load(f)
|
|
366
|
+
else:
|
|
367
|
+
config = {}
|
|
368
|
+
config = self.prepare_config(config, garf_config)
|
|
369
|
+
with smart_open.open(self.path, 'w', encoding='utf-8') as f:
|
|
370
|
+
yaml.dump(
|
|
371
|
+
config, f, default_flow_style=False, sort_keys=False, encoding='utf-8'
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def prepare_config(self, config: dict, garf_config: BaseConfig) -> dict:
|
|
375
|
+
garf = dataclasses.asdict(garf_config)
|
|
376
|
+
if isinstance(garf_config, GarfConfig):
|
|
377
|
+
garf[garf_config.output] = garf_config.writer_params
|
|
378
|
+
if not isinstance(garf_config.account, MutableSequence):
|
|
379
|
+
garf['account'] = garf_config.account.split(',')
|
|
380
|
+
del garf['writer_params']
|
|
381
|
+
garf = _remove_empty_values(garf)
|
|
382
|
+
config.update({'garf': garf})
|
|
383
|
+
if isinstance(garf_config, GarfBqConfig):
|
|
384
|
+
garf = _remove_empty_values(garf)
|
|
385
|
+
config.update({'garf-bq': garf})
|
|
386
|
+
if isinstance(garf_config, GarfSqlConfig):
|
|
387
|
+
garf = _remove_empty_values(garf)
|
|
388
|
+
config.update({'garf-sql': garf})
|
|
389
|
+
return config
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def initialize_runtime_parameters(config: BaseConfig) -> BaseConfig:
|
|
393
|
+
"""Formats parameters and add common parameter in config.
|
|
394
|
+
|
|
395
|
+
Initialization identifies whether there are `date` parameters and performs
|
|
396
|
+
necessary date conversions.
|
|
397
|
+
Set of parameters that need to be generally available are injected into
|
|
398
|
+
every parameter section of the config.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
config: Instantiated config.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Config with formatted parameters.
|
|
405
|
+
"""
|
|
406
|
+
common_params = query_editor.CommonParametersMixin().common_params
|
|
407
|
+
for key, param in config.params.items():
|
|
408
|
+
for key_param, value_param in param.items():
|
|
409
|
+
config.params[key][key_param] = convert_date(value_param)
|
|
410
|
+
for common_param_key, common_param_value in common_params.items():
|
|
411
|
+
if common_param_key not in config.params[key]:
|
|
412
|
+
config.params[key][common_param_key] = common_param_value
|
|
413
|
+
return config
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _remove_empty_values(dict_object: dict[str, Any]) -> dict[str, Any]:
|
|
417
|
+
"""Remove all empty elements: strings, dictionaries from a dictionary."""
|
|
418
|
+
if isinstance(dict_object, dict):
|
|
419
|
+
return {
|
|
420
|
+
key: value
|
|
421
|
+
for key, value in (
|
|
422
|
+
(key, _remove_empty_values(value)) for key, value in dict_object.items()
|
|
423
|
+
)
|
|
424
|
+
if value
|
|
425
|
+
}
|
|
426
|
+
if isinstance(dict_object, (int, str, MutableSequence)):
|
|
427
|
+
return dict_object
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def garf_runner(query: str, callback: Callable, logger) -> None:
|
|
431
|
+
try:
|
|
432
|
+
logger.debug('starting query %s', query)
|
|
433
|
+
callback()
|
|
434
|
+
logger.info('%s executed successfully', query)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
traceback.print_tb(e.__traceback__)
|
|
437
|
+
logger.error('%s generated an exception: %s', query, str(e))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def postprocessor_runner(query: str, callback: Callable, logger) -> None:
|
|
441
|
+
try:
|
|
442
|
+
logger.debug('starting query %s', query)
|
|
443
|
+
callback()
|
|
444
|
+
logger.info('%s executed successfully', query)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
logger.error('%s generated an exception: %s', query, str(e))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def init_logging(
|
|
450
|
+
loglevel: str = 'INFO', logger_type: str = 'local', name: str = __name__
|
|
451
|
+
) -> logging.Logger:
|
|
452
|
+
if logger_type == 'rich':
|
|
453
|
+
logging.basicConfig(
|
|
454
|
+
format='%(message)s',
|
|
455
|
+
level=loglevel,
|
|
456
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
457
|
+
handlers=[
|
|
458
|
+
rich_logging.RichHandler(rich_tracebacks=True),
|
|
459
|
+
],
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
logging.basicConfig(
|
|
463
|
+
format='[%(asctime)s][%(name)s][%(levelname)s] %(message)s',
|
|
464
|
+
stream=sys.stdout,
|
|
465
|
+
level=loglevel,
|
|
466
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
467
|
+
)
|
|
468
|
+
logging.getLogger('smart_open.smart_open_lib').setLevel(logging.WARNING)
|
|
469
|
+
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
|
470
|
+
return logging.getLogger(name)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright 2024 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Defines mechanism for executing queries via SqlAlchemy."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import sqlalchemy
|
|
20
|
+
except ImportError as e:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
'Please install garf-executors with sqlalchemy support '
|
|
23
|
+
'- `pip install garf-executors[sqlalchemy]`'
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import re
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import pandas as pd
|
|
31
|
+
|
|
32
|
+
from garf_core import query_editor
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SqlAlchemyQueryExecutor(query_editor.TemplateProcessorMixin):
|
|
36
|
+
"""Handles query execution via SqlAlchemy.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
engine: Initialized Engine object to operated on a given database.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, engine: sqlalchemy.engine.base.Engine) -> None:
|
|
43
|
+
"""Initializes executor with a given engine.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
engine: Initialized Engine object to operated on a given database.
|
|
47
|
+
"""
|
|
48
|
+
self.engine = engine
|
|
49
|
+
|
|
50
|
+
def execute(
|
|
51
|
+
self,
|
|
52
|
+
script_name: str | None,
|
|
53
|
+
query_text: str,
|
|
54
|
+
params: dict[str, Any] | None = None,
|
|
55
|
+
) -> pd.DataFrame:
|
|
56
|
+
"""Executes query in a given database via SqlAlchemy.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
script_name: Script identifier.
|
|
60
|
+
query_text: Query to be executed.
|
|
61
|
+
params: Optional parameters to be replaced in query text.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
DataFrame if query returns some data otherwise empty DataFrame.
|
|
65
|
+
"""
|
|
66
|
+
logging.info('Executing script: %s', script_name)
|
|
67
|
+
query_text = self.replace_params_template(query_text, params)
|
|
68
|
+
with self.engine.begin() as conn:
|
|
69
|
+
if re.findall(r'(create|update) ', query_text.lower()):
|
|
70
|
+
conn.connection.executescript(query_text)
|
|
71
|
+
return pd.DataFrame()
|
|
72
|
+
temp_table_name = f'temp_{script_name}'.replace('.', '_')
|
|
73
|
+
query_text = f'CREATE TABLE {temp_table_name} AS {query_text}'
|
|
74
|
+
conn.connection.executescript(query_text)
|
|
75
|
+
try:
|
|
76
|
+
result = pd.read_sql(f'SELECT * FROM {temp_table_name}', conn)
|
|
77
|
+
finally:
|
|
78
|
+
conn.connection.execute(f'DROP TABLE {temp_table_name}')
|
|
79
|
+
return result
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: garf-executors
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Executes queries against API and writes data to local/remote storage.
|
|
5
|
+
Author-email: "Google Inc. (gTech gPS CSE team)" <no-reply@google.com>
|
|
6
|
+
License: Apache 2.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: garf-core
|
|
20
|
+
Requires-Dist: garf-io
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: garf-executors[bq,sql] ; extra == 'all'
|
|
23
|
+
Provides-Extra: bq
|
|
24
|
+
Requires-Dist: garf-io[bq] ; extra == 'bq'
|
|
25
|
+
Requires-Dist: pandas ; extra == 'bq'
|
|
26
|
+
Provides-Extra: sql
|
|
27
|
+
Requires-Dist: garf-io[sqlalchemy] ; extra == 'sql'
|
|
28
|
+
Requires-Dist: pandas ; extra == 'sql'
|
|
29
|
+
|
|
30
|
+
# Gaarf Executors
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
garf_executors/__init__.py,sha256=EWaHzmbB0XnTnYoR0bNsknBBbCet7tpv7gUwahtBIC0,873
|
|
2
|
+
garf_executors/api_executor.py,sha256=axB7msuZ7LXFk_f3MjIyGM2AhDILyu7g70zqNW0hG6Q,3149
|
|
3
|
+
garf_executors/bq_executor.py,sha256=JBPxbDRYgUgpJv6SqYiFPypTFjZGIZ-SOOb6dS2sZQY,3822
|
|
4
|
+
garf_executors/sql_executor.py,sha256=vBNQ4HZZYxP_EYAh8Z4BerzLESfsNpXdhENzXIw-OIo,2532
|
|
5
|
+
garf_executors/entrypoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
garf_executors/entrypoints/utils.py,sha256=07SUDnnjznO8o3r2qHsVFAz8TDbvv6T8MhtLdWxv8rc,15085
|
|
7
|
+
garf_executors/entrypoints/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
garf_executors/entrypoints/cli/api.py,sha256=0QzB7KdXyZnHhaMEGsFhj2B8hyNxV-F5JW6zC90rTcQ,7118
|
|
9
|
+
garf_executors/entrypoints/cli/bq.py,sha256=Rk3nTkcGhyp1hnSSFsLFIFPXo33l_B_O1MiCK2fdZM8,3824
|
|
10
|
+
garf_executors/entrypoints/cli/gaarf.py,sha256=0QzB7KdXyZnHhaMEGsFhj2B8hyNxV-F5JW6zC90rTcQ,7118
|
|
11
|
+
garf_executors/entrypoints/cli/sql.py,sha256=tXQwhrLNUvfORxVdaZHnIawAR06oWZGro2vcreJ22Kc,3753
|
|
12
|
+
garf_executors-0.0.1.dist-info/METADATA,sha256=ksb0r59ozF1UFEOfQ5zS_-oh3Wus8ByjpVxrN8KmV8M,1180
|
|
13
|
+
garf_executors-0.0.1.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
|
|
14
|
+
garf_executors-0.0.1.dist-info/entry_points.txt,sha256=ksbFBDblKlOYqNyYoL3uaZVYWEYM_KWb0sWrvUamhd4,136
|
|
15
|
+
garf_executors-0.0.1.dist-info/top_level.txt,sha256=sP4dCXOENPn1hDFAunjMV8Js4NND_KGeO_gQWuaT0EY,15
|
|
16
|
+
garf_executors-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
garf_executors
|