Skip to content

Commit ce5b0ec

Browse files
[executors] chore: remove BaseConfig and its child classes
1 parent 60c6d2f commit ce5b0ec

File tree

5 files changed

+22
-823
lines changed

5 files changed

+22
-823
lines changed

libs/executors/garf_executors/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
15+
# pylint: disable=C0330, g-bad-import-order, g-multiple-import
16+
17+
"""Stores mapping between API aliases and their execution context."""
18+
1419
from __future__ import annotations
1520

1621
import os

libs/executors/garf_executors/entrypoints/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ def main():
6868
if config_file := args.config:
6969
execution_config = config.Config.from_file(config_file)
7070
if not (context := execution_config.sources.get(args.source)):
71-
raise exceptions.GarfExecutorError('Missing context')
71+
raise exceptions.GarfExecutorError(
72+
f'No execution context found for source {args.source} in {config_file}'
73+
)
7274
query_executor = garf_executors.setup_executor(
7375
args.source, context.fetcher_parameters
7476
)

libs/executors/garf_executors/entrypoints/utils.py

Lines changed: 1 addition & 313 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import os
2222
import sys
2323
from collections.abc import MutableSequence, Sequence
24-
from typing import Any, TypedDict
24+
from typing import Any
2525

2626
import smart_open
2727
import yaml
@@ -30,246 +30,6 @@
3030
from rich import logging as rich_logging
3131

3232

33-
class GarfQueryParameters(TypedDict):
34-
"""Annotation for dictionary of query specific parameters passed via CLI.
35-
36-
Attributes:
37-
macros: Mapping for elements that will be replaced in the queries.
38-
template: Mapping for elements that will rendered via Jinja templates.
39-
"""
40-
41-
macros: dict[str, str]
42-
template: dict[str, str]
43-
44-
45-
@dataclasses.dataclass
46-
class BaseConfig:
47-
"""Base config to inherit other configs from."""
48-
49-
def __add__(self, other: BaseConfig) -> BaseConfig:
50-
"""Creates new config of the same type from two configs.
51-
52-
Parameters from added config overwrite already present parameters.
53-
54-
Args:
55-
other: Config that could be merged with the original one.
56-
57-
Returns:
58-
New config with values from both configs.
59-
"""
60-
right_dict = _remove_empty_values(self.__dict__)
61-
left_dict = _remove_empty_values(other.__dict__)
62-
new_dict = {**right_dict, **left_dict}
63-
return self.__class__(**new_dict)
64-
65-
@classmethod
66-
def from_dict(
67-
cls, config_parameters: dict[str, str | GarfQueryParameters]
68-
) -> BaseConfig:
69-
"""Builds config from provided parameters ignoring empty ones."""
70-
return cls(**_remove_empty_values(config_parameters))
71-
72-
73-
@dataclasses.dataclass
74-
class GarfConfig(BaseConfig):
75-
"""Stores values to run garf from command line.
76-
77-
Attributes:
78-
account:
79-
Account(s) to get data from.
80-
output:
81-
Specifies where to store fetched data (console, csv, BQ.)
82-
api_version:
83-
Google Ads API version.
84-
params:
85-
Any parameters passed to Garf query for substitution.
86-
writer_params:
87-
Any parameters that can be passed to writer for data saving.
88-
customer_ids_query:
89-
Query text to limit accounts fetched from Ads API.
90-
customer_ids_query_file:
91-
Path to query to limit accounts fetched from Ads API.
92-
"""
93-
94-
account: str | list[str] | None = None
95-
output: str = 'console'
96-
params: GarfQueryParameters = dataclasses.field(default_factory=dict)
97-
writer_params: dict[str, str | int] = dataclasses.field(default_factory=dict)
98-
customer_ids_query: str | None = None
99-
customer_ids_query_file: str | None = None
100-
101-
def __post_init__(self) -> None:
102-
"""Ensures that values passed during __init__ correctly formatted."""
103-
if isinstance(self.account, MutableSequence):
104-
self.account = [
105-
str(account).replace('-', '').strip() for account in self.account
106-
]
107-
else:
108-
self.account = (
109-
str(self.account).replace('-', '').strip() if self.account else None
110-
)
111-
self.writer_params = {
112-
key.replace('-', '_'): value for key, value in self.writer_params.items()
113-
}
114-
115-
116-
class GarfConfigException(Exception):
117-
"""Exception for invalid GarfConfig."""
118-
119-
120-
@dataclasses.dataclass
121-
class GarfBqConfig(BaseConfig):
122-
"""Stores values to run garf-bq from command line.
123-
124-
Attributes:
125-
project:
126-
Google Cloud project name.
127-
dataset_location:
128-
Location of BigQuery dataset.
129-
params:
130-
Any parameters passed to BigQuery query for substitution.
131-
"""
132-
133-
project: str | None = None
134-
dataset_location: str | None = None
135-
params: GarfQueryParameters = dataclasses.field(default_factory=dict)
136-
137-
138-
@dataclasses.dataclass
139-
class GarfSqlConfig(BaseConfig):
140-
"""Stores values to run garf-sql from command line.
141-
142-
Attributes:
143-
connection_string:
144-
Connection string to SqlAlchemy database engine.
145-
params:
146-
Any parameters passed to SQL query for substitution.
147-
"""
148-
149-
connection_string: str | None = None
150-
params: GarfQueryParameters = dataclasses.field(default_factory=dict)
151-
152-
153-
class ConfigBuilder:
154-
"""Builds config of provided type.
155-
156-
Config can be created from file, build from arguments or both.
157-
158-
Attributes:
159-
config: Concrete config class that needs to be built.
160-
"""
161-
162-
_config_mapping: dict[str, BaseConfig] = {
163-
'garf': GarfConfig,
164-
'garf-bq': GarfBqConfig,
165-
'garf-sql': GarfSqlConfig,
166-
}
167-
168-
def __init__(self, config_type: str) -> None:
169-
"""Sets concrete config type.
170-
171-
Args:
172-
config_type: Type of config that should be built.
173-
174-
Raises:
175-
GarfConfigException: When incorrect config_type is supplied.
176-
"""
177-
if config_type not in self._config_mapping:
178-
raise GarfConfigException(f'Invalid config_type: {config_type}')
179-
self._config_type = config_type
180-
self.config = self._config_mapping.get(config_type)
181-
182-
def build(
183-
self, parameters: dict[str, str], cli_named_args: Sequence[str]
184-
) -> BaseConfig | None:
185-
"""Builds config from file, build from arguments or both ways.
186-
187-
When there are both config_file and CLI arguments the latter have more
188-
priority.
189-
190-
Args:
191-
parameters: Parsed CLI arguments.
192-
cli_named_args: Unparsed CLI args in a form `--key.subkey=value`.
193-
194-
Returns:
195-
Concrete config with injected values.
196-
"""
197-
if not (garf_config_path := parameters.get('garf_config')):
198-
return self._build_config(parameters, cli_named_args)
199-
config_file = self._load_config(garf_config_path)
200-
config_cli = self._build_config(
201-
parameters, cli_named_args, init_defaults=False
202-
)
203-
if config_file and config_cli:
204-
config_file = config_file + config_cli
205-
return config_file
206-
207-
def _build_config(
208-
self,
209-
parameters: dict[str, str],
210-
cli_named_args: Sequence[str],
211-
init_defaults: bool = True,
212-
) -> BaseConfig | None:
213-
"""Builds config from named and unnamed CLI parameters.
214-
215-
Args:
216-
parameters: Parsed CLI arguments.
217-
cli_named_args: Unparsed CLI args in a form `--key.subkey=value`.
218-
init_defaults: Whether to provided default config values if
219-
expected parameter is missing
220-
221-
Returns:
222-
Concrete config with injected values.
223-
"""
224-
output = parameters.get('output')
225-
config_parameters = {
226-
k: v for k, v in parameters.items() if k in self.config.__annotations__
227-
}
228-
cli_params = ParamsParser(['macro', 'template', output]).parse(
229-
cli_named_args
230-
)
231-
cli_params = _remove_empty_values(cli_params)
232-
if output and (writer_params := cli_params.get(output)):
233-
_ = cli_params.pop(output)
234-
config_parameters.update({'writer_params': writer_params})
235-
if cli_params:
236-
config_parameters.update({'params': cli_params})
237-
if not config_parameters:
238-
return None
239-
if init_defaults:
240-
return self.config.from_dict(config_parameters)
241-
return self.config(**config_parameters)
242-
243-
def _load_config(self, garf_config_path: str) -> BaseConfig:
244-
"""Loads config from provided path.
245-
246-
Args:
247-
garf_config_path: Path to local or remote storage.
248-
249-
Returns:
250-
Concreate config with values taken from config file.
251-
252-
Raises:
253-
GarfConfigException:
254-
If config file missing `garf` section.
255-
"""
256-
with smart_open.open(garf_config_path, encoding='utf-8') as f:
257-
config = yaml.safe_load(f)
258-
garf_section = config.get(self._config_type)
259-
if not garf_section:
260-
raise GarfConfigException(
261-
f'Invalid config, must have `{self._config_type}` section!'
262-
)
263-
config_parameters = {
264-
k: v for k, v in garf_section.items() if k in self.config.__annotations__
265-
}
266-
if params := garf_section.get('params', {}):
267-
config_parameters.update({'params': params})
268-
if writer_params := garf_section.get(garf_section.get('output', '')):
269-
config_parameters.update({'writer_params': writer_params})
270-
return self.config(**config_parameters)
271-
272-
27333
class ParamsParser:
27434
def __init__(self, identifiers: Sequence[str]) -> None:
27535
self.identifiers = identifiers
@@ -373,78 +133,6 @@ def convert_date(date_string: str) -> str:
373133
return (new_date - delta).strftime('%Y-%m-%d')
374134

375135

376-
class ConfigSaver:
377-
def __init__(self, path: str) -> None:
378-
self.path = path
379-
380-
def save(self, garf_config: BaseConfig):
381-
if os.path.exists(self.path):
382-
with smart_open.open(self.path, 'r', encoding='utf-8') as f:
383-
config = yaml.safe_load(f)
384-
else:
385-
config = {}
386-
config = self.prepare_config(config, garf_config)
387-
with smart_open.open(self.path, 'w', encoding='utf-8') as f:
388-
yaml.dump(
389-
config, f, default_flow_style=False, sort_keys=False, encoding='utf-8'
390-
)
391-
392-
def prepare_config(self, config: dict, garf_config: BaseConfig) -> dict:
393-
garf = dataclasses.asdict(garf_config)
394-
if isinstance(garf_config, GarfConfig):
395-
garf[garf_config.output] = garf_config.writer_params
396-
if not isinstance(garf_config.account, MutableSequence):
397-
garf['account'] = garf_config.account.split(',')
398-
del garf['writer_params']
399-
garf = _remove_empty_values(garf)
400-
config.update({'garf': garf})
401-
if isinstance(garf_config, GarfBqConfig):
402-
garf = _remove_empty_values(garf)
403-
config.update({'garf-bq': garf})
404-
if isinstance(garf_config, GarfSqlConfig):
405-
garf = _remove_empty_values(garf)
406-
config.update({'garf-sql': garf})
407-
return config
408-
409-
410-
def initialize_runtime_parameters(config: BaseConfig) -> BaseConfig:
411-
"""Formats parameters and add common parameter in config.
412-
413-
Initialization identifies whether there are `date` parameters and performs
414-
necessary date conversions.
415-
Set of parameters that need to be generally available are injected into
416-
every parameter section of the config.
417-
418-
Args:
419-
config: Instantiated config.
420-
421-
Returns:
422-
Config with formatted parameters.
423-
"""
424-
common_params = query_editor.CommonParametersMixin().common_params
425-
for key, param in config.params.items():
426-
for key_param, value_param in param.items():
427-
config.params[key][key_param] = convert_date(value_param)
428-
for common_param_key, common_param_value in common_params.items():
429-
if common_param_key not in config.params[key]:
430-
config.params[key][common_param_key] = common_param_value
431-
return config
432-
433-
434-
def _remove_empty_values(dict_object: dict[str, Any]) -> dict[str, Any]:
435-
"""Remove all empty elements: strings, dictionaries from a dictionary."""
436-
if isinstance(dict_object, dict):
437-
return {
438-
key: value
439-
for key, value in (
440-
(key, _remove_empty_values(value)) for key, value in dict_object.items()
441-
)
442-
if value
443-
}
444-
if isinstance(dict_object, (int, str, MutableSequence)):
445-
return dict_object
446-
447-
448136
def init_logging(
449137
loglevel: str = 'INFO', logger_type: str = 'local', name: str = __name__
450138
) -> logging.Logger:

libs/executors/garf_executors/execution_context.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
# pylint: disable=C0330, g-bad-import-order, g-multiple-import
1616

17+
"""Captures parameters for fetching data from APIs."""
18+
1719
from __future__ import annotations
1820

1921
import os
@@ -37,14 +39,22 @@ class ExecutionContext(pydantic.BaseModel):
3739
writer_parameters: Optional parameters to setup writer.
3840
"""
3941

40-
query_parameters: query_editor.GarfQueryParameters = pydantic.Field(
42+
query_parameters: query_editor.GarfQueryParameters | None = pydantic.Field(
4143
default_factory=dict
4244
)
43-
fetcher_parameters: dict[str, str | list[str | int]] = pydantic.Field(
45+
fetcher_parameters: dict[str, str | list[str | int]] | None = pydantic.Field(
4446
default_factory=dict
4547
)
4648
writer: str | None = None
47-
writer_parameters: dict[str, str] = pydantic.Field(default_factory=dict)
49+
writer_parameters: dict[str, str] | None = pydantic.Field(
50+
default_factory=dict
51+
)
52+
53+
def model_post_init(self, __context__) -> None:
54+
if self.fetcher_parameters is None:
55+
self.fetcher_parameters = {}
56+
if self.writer_parameters is None:
57+
self.writer_parameters = {}
4858

4959
@classmethod
5060
def from_file(

0 commit comments

Comments
 (0)