garf-executors 0.2.3__py3-none-any.whl → 1.0.2__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.
Files changed (44) hide show
  1. garf/executors/__init__.py +60 -0
  2. garf/executors/api_executor.py +143 -0
  3. garf/executors/bq_executor.py +177 -0
  4. garf/executors/config.py +52 -0
  5. garf/executors/entrypoints/__init__.py +0 -0
  6. garf/executors/entrypoints/cli.py +177 -0
  7. {garf_executors → garf/executors}/entrypoints/grpc_server.py +5 -6
  8. garf/executors/entrypoints/server.py +117 -0
  9. garf/executors/entrypoints/tracer.py +57 -0
  10. garf/executors/entrypoints/utils.py +140 -0
  11. garf/executors/exceptions.py +17 -0
  12. garf/executors/execution_context.py +117 -0
  13. garf/executors/executor.py +124 -0
  14. garf/executors/fetchers.py +78 -0
  15. garf/executors/query_processor.py +61 -0
  16. garf/executors/sql_executor.py +142 -0
  17. garf/executors/telemetry.py +20 -0
  18. garf/executors/workflow.py +109 -0
  19. garf_executors/__init__.py +9 -44
  20. garf_executors/api_executor.py +9 -121
  21. garf_executors/bq_executor.py +9 -161
  22. garf_executors/config.py +9 -37
  23. garf_executors/entrypoints/__init__.py +25 -0
  24. garf_executors/entrypoints/cli.py +9 -148
  25. garf_executors/entrypoints/grcp_server.py +25 -0
  26. garf_executors/entrypoints/server.py +9 -102
  27. garf_executors/entrypoints/tracer.py +8 -40
  28. garf_executors/entrypoints/utils.py +9 -124
  29. garf_executors/exceptions.py +11 -3
  30. garf_executors/execution_context.py +9 -100
  31. garf_executors/executor.py +9 -108
  32. garf_executors/fetchers.py +9 -63
  33. garf_executors/sql_executor.py +9 -125
  34. garf_executors/telemetry.py +10 -5
  35. garf_executors/workflow.py +8 -79
  36. {garf_executors-0.2.3.dist-info → garf_executors-1.0.2.dist-info}/METADATA +11 -5
  37. garf_executors-1.0.2.dist-info/RECORD +42 -0
  38. garf_executors-1.0.2.dist-info/entry_points.txt +2 -0
  39. {garf_executors-0.2.3.dist-info → garf_executors-1.0.2.dist-info}/top_level.txt +1 -0
  40. garf_executors-0.2.3.dist-info/RECORD +0 -24
  41. garf_executors-0.2.3.dist-info/entry_points.txt +0 -2
  42. {garf_executors → garf/executors}/garf_pb2.py +0 -0
  43. {garf_executors → garf/executors}/garf_pb2_grpc.py +0 -0
  44. {garf_executors-0.2.3.dist-info → garf_executors-1.0.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,61 @@
1
+ # Copyright 2026 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
+
15
+ import contextlib
16
+
17
+ from garf.core import query_editor
18
+ from garf.executors import exceptions, execution_context
19
+
20
+
21
+ def process_gquery(
22
+ context: execution_context.ExecutionContext,
23
+ ) -> execution_context.ExecutionContext:
24
+ for k, v in context.fetcher_parameters.items():
25
+ if isinstance(v, str) and v.startswith('gquery'):
26
+ no_writer_context = context.model_copy(update={'writer': None})
27
+ try:
28
+ _, alias, query = v.split(':', maxsplit=3)
29
+ except ValueError:
30
+ raise exceptions.GarfExecutorError(
31
+ f'Incorrect gquery format, should be gquery:alias:query, got {v}'
32
+ )
33
+ if alias == 'sqldb':
34
+ from garf.executors import sql_executor
35
+
36
+ gquery_executor = sql_executor.SqlAlchemyQueryExecutor(
37
+ **context.fetcher_parameters
38
+ )
39
+ elif alias == 'bq':
40
+ from garf.executors import bq_executor
41
+
42
+ gquery_executor = bq_executor.BigQueryExecutor(
43
+ **context.fetcher_parameters
44
+ )
45
+ else:
46
+ raise exceptions.GarfExecutorError(
47
+ f'Unsupported alias for gquery: {alias}'
48
+ )
49
+ with contextlib.suppress(query_editor.GarfResourceError):
50
+ query_spec = query_editor.QuerySpecification(
51
+ text=query, args=context.query_parameters
52
+ ).generate()
53
+ if len(columns := [c for c in query_spec.column_names if c != '_']) > 1:
54
+ raise exceptions.GarfExecutorError(
55
+ f'Multiple columns in gquery: {columns}'
56
+ )
57
+ res = gquery_executor.execute(
58
+ query=query, title='gquery', context=no_writer_context
59
+ )
60
+ context.fetcher_parameters[k] = res.to_list(row_type='scalar')
61
+ return context
@@ -0,0 +1,142 @@
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
+ import uuid
29
+
30
+ import pandas as pd
31
+ from garf.core import query_editor, report
32
+ from garf.executors import exceptions, execution_context, executor
33
+ from garf.executors.telemetry import tracer
34
+ from opentelemetry import trace
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class SqlAlchemyQueryExecutorError(exceptions.GarfExecutorError):
40
+ """Error when SqlAlchemyQueryExecutor fails to run query."""
41
+
42
+
43
+ class SqlAlchemyQueryExecutor(
44
+ executor.Executor, query_editor.TemplateProcessorMixin
45
+ ):
46
+ """Handles query execution via SqlAlchemy.
47
+
48
+ Attributes:
49
+ engine: Initialized Engine object to operated on a given database.
50
+ """
51
+
52
+ def __init__(
53
+ self, engine: sqlalchemy.engine.base.Engine | None = None, **kwargs: str
54
+ ) -> None:
55
+ """Initializes executor with a given engine.
56
+
57
+ Args:
58
+ engine: Initialized Engine object to operated on a given database.
59
+ """
60
+ self.engine = engine or sqlalchemy.create_engine('sqlite://')
61
+ super().__init__()
62
+
63
+ @classmethod
64
+ def from_connection_string(
65
+ cls, connection_string: str | None
66
+ ) -> SqlAlchemyQueryExecutor:
67
+ """Creates executor from SqlAlchemy connection string.
68
+
69
+ https://docs.sqlalchemy.org/en/20/core/engines.html
70
+ """
71
+ engine = sqlalchemy.create_engine(connection_string or 'sqlite://')
72
+ return cls(engine)
73
+
74
+ @tracer.start_as_current_span('sql.execute')
75
+ def execute(
76
+ self,
77
+ query: str,
78
+ title: str,
79
+ context: execution_context.ExecutionContext = (
80
+ execution_context.ExecutionContext()
81
+ ),
82
+ ) -> report.GarfReport:
83
+ """Executes query in a given database via SqlAlchemy.
84
+
85
+ Args:
86
+ query: Location of the query.
87
+ title: Name of the query.
88
+ context: Query execution context.
89
+
90
+ Returns:
91
+ Report with data if query returns some data otherwise empty Report.
92
+ """
93
+ span = trace.get_current_span()
94
+ logger.info('Executing script: %s', title)
95
+ query_text = self.replace_params_template(query, context.query_parameters)
96
+ with self.engine.begin() as conn:
97
+ if re.findall(r'(create|update) ', query_text.lower()):
98
+ try:
99
+ conn.connection.executescript(query_text)
100
+ results = report.GarfReport()
101
+ except Exception as e:
102
+ raise SqlAlchemyQueryExecutorError(
103
+ f'Failed to execute query {title}: Reason: {e}'
104
+ ) from e
105
+ else:
106
+ temp_table_name = f'temp_{uuid.uuid4().hex}'
107
+ query_text = f'CREATE TABLE {temp_table_name} AS {query_text}'
108
+ conn.connection.executescript(query_text)
109
+ try:
110
+ results = report.GarfReport.from_pandas(
111
+ pd.read_sql(f'SELECT * FROM {temp_table_name}', conn)
112
+ )
113
+ except Exception as e:
114
+ raise SqlAlchemyQueryExecutorError(
115
+ f'Failed to execute query {title}: Reason: {e}'
116
+ ) from e
117
+ finally:
118
+ conn.connection.execute(f'DROP TABLE {temp_table_name}')
119
+ if context.writer and results:
120
+ writer_clients = context.writer_clients
121
+ if not writer_clients:
122
+ logger.warning('No writers configured, skipping write operation')
123
+ else:
124
+ writing_results = []
125
+ for writer_client in writer_clients:
126
+ logger.debug(
127
+ 'Start writing data for query %s via %s writer',
128
+ title,
129
+ type(writer_client),
130
+ )
131
+ writing_result = writer_client.write(results, title)
132
+ logger.debug(
133
+ 'Finish writing data for query %s via %s writer',
134
+ title,
135
+ type(writer_client),
136
+ )
137
+ writing_results.append(writing_result)
138
+ logger.info('%s executed successfully', title)
139
+ # Return the last writer's result for backward compatibility
140
+ return writing_results[-1] if writing_results else None
141
+ span.set_attribute('execute.num_results', len(results))
142
+ return results
@@ -0,0 +1,20 @@
1
+ # Copyright 2025 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
+
15
+ # pylint: disable=C0330, g-bad-import-order, g-multiple-import
16
+ from opentelemetry import trace
17
+
18
+ tracer = trace.get_tracer(
19
+ instrumenting_module_name='garf.executors',
20
+ )
@@ -0,0 +1,109 @@
1
+ # Copyright 2026 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
+ from __future__ import annotations
15
+
16
+ import os
17
+ import pathlib
18
+
19
+ import pydantic
20
+ import smart_open
21
+ import yaml
22
+ from garf.executors import exceptions
23
+ from garf.executors.execution_context import ExecutionContext
24
+
25
+
26
+ class GarfWorkflowError(exceptions.GarfExecutorError):
27
+ """Workflow specific exception."""
28
+
29
+
30
+ class QueryFolder(pydantic.BaseModel):
31
+ """Path to folder with queries."""
32
+
33
+ folder: str
34
+
35
+
36
+ class QueryPath(pydantic.BaseModel):
37
+ """Path file with query."""
38
+
39
+ path: str
40
+
41
+
42
+ class QueryDefinition(pydantic.BaseModel):
43
+ """Definition of a query."""
44
+
45
+ query: Query
46
+
47
+
48
+ class Query(pydantic.BaseModel):
49
+ """Query elements.
50
+
51
+ Attributes:
52
+ text: Query text.
53
+ title: Name of the query.
54
+ """
55
+
56
+ text: str
57
+ title: str
58
+
59
+
60
+ class ExecutionStep(ExecutionContext):
61
+ """Common context for executing one or more queries.
62
+
63
+ Attributes:
64
+ fetcher: Name of a fetcher to get data from API.
65
+ alias: Optional alias to identify execution step.
66
+ queries: Queries to run for a particular fetcher.
67
+ context: Execution context for queries and fetcher.
68
+ """
69
+
70
+ fetcher: str | None = None
71
+ alias: str | None = pydantic.Field(default=None, pattern=r'^[a-zA-Z0-9_]+$')
72
+ queries: list[QueryPath | QueryDefinition | QueryFolder] | None = None
73
+
74
+ @property
75
+ def context(self) -> ExecutionContext:
76
+ return ExecutionContext(
77
+ writer=self.writer,
78
+ writer_parameters=self.writer_parameters,
79
+ query_parameters=self.query_parameters,
80
+ fetcher_parameters=self.fetcher_parameters,
81
+ )
82
+
83
+
84
+ class Workflow(pydantic.BaseModel):
85
+ """Orchestrates execution of queries for multiple fetchers.
86
+
87
+ Attributes:
88
+ steps: Contains one or several fetcher executions.
89
+ """
90
+
91
+ steps: list[ExecutionStep]
92
+
93
+ @classmethod
94
+ def from_file(cls, path: str | pathlib.Path | os.PathLike[str]) -> Workflow:
95
+ """Builds workflow from local or remote yaml file."""
96
+ with smart_open.open(path, 'r', encoding='utf-8') as f:
97
+ data = yaml.safe_load(f)
98
+ try:
99
+ return Workflow(**data)
100
+ except pydantic.ValidationError as e:
101
+ raise GarfWorkflowError(f'Incorrect workflow:\n {e}') from e
102
+
103
+ def save(self, path: str | pathlib.Path | os.PathLike[str]) -> str:
104
+ """Saves workflow to local or remote yaml file."""
105
+ with smart_open.open(path, 'w', encoding='utf-8') as f:
106
+ yaml.dump(
107
+ self.model_dump(exclude_none=True).get('steps'), f, encoding='utf-8'
108
+ )
109
+ return f'Workflow is saved to {str(path)}'
@@ -1,4 +1,4 @@
1
- # Copyright 2025 Google LLC
1
+ # Copyright 2026 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,50 +11,15 @@
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
- """Executors to fetch data from various APIs."""
15
14
 
16
- from __future__ import annotations
17
15
 
18
- import importlib
16
+ import warnings
19
17
 
20
- from garf_executors import executor, fetchers
21
- from garf_executors.api_executor import ApiExecutionContext, ApiQueryExecutor
22
- from garf_executors.telemetry import tracer
18
+ from garf.executors import *
23
19
 
24
-
25
- @tracer.start_as_current_span('setup_executor')
26
- def setup_executor(
27
- source: str,
28
- fetcher_parameters: dict[str, str | int | bool],
29
- enable_cache: bool = False,
30
- cache_ttl_seconds: int = 3600,
31
- ) -> type[executor.Executor]:
32
- """Initializes executors based on a source and parameters."""
33
- if source == 'bq':
34
- bq_executor = importlib.import_module('garf_executors.bq_executor')
35
- query_executor = bq_executor.BigQueryExecutor(**fetcher_parameters)
36
- elif source == 'sqldb':
37
- sql_executor = importlib.import_module('garf_executors.sql_executor')
38
- query_executor = (
39
- sql_executor.SqlAlchemyQueryExecutor.from_connection_string(
40
- fetcher_parameters.get('connection_string')
41
- )
42
- )
43
- else:
44
- concrete_api_fetcher = fetchers.get_report_fetcher(source)
45
- query_executor = ApiQueryExecutor(
46
- fetcher=concrete_api_fetcher(
47
- **fetcher_parameters,
48
- enable_cache=enable_cache,
49
- cache_ttl_seconds=cache_ttl_seconds,
50
- )
51
- )
52
- return query_executor
53
-
54
-
55
- __all__ = [
56
- 'ApiQueryExecutor',
57
- 'ApiExecutionContext',
58
- ]
59
-
60
- __version__ = '0.2.3'
20
+ warnings.warn(
21
+ "The 'garf_executors' namespace is deprecated. "
22
+ "Please use 'garf.executors' instead.",
23
+ DeprecationWarning,
24
+ stacklevel=2,
25
+ )
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2026 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,127 +11,15 @@
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 executing Garf queries and writing them to local/remote.
15
14
 
16
- ApiQueryExecutor performs fetching data from API in a form of
17
- GarfReport and saving it to local/remote storage.
18
- """
19
- # pylint: disable=C0330, g-bad-import-order, g-multiple-import
20
15
 
21
- from __future__ import annotations
16
+ import warnings
22
17
 
23
- import logging
18
+ from garf.executors.api_executor import *
24
19
 
25
- from garf_core import report_fetcher
26
- from opentelemetry import trace
27
-
28
- from garf_executors import exceptions, execution_context, executor, fetchers
29
- from garf_executors.telemetry import tracer
30
-
31
- logger = logging.getLogger(__name__)
32
-
33
-
34
- class ApiExecutionContext(execution_context.ExecutionContext):
35
- """Common context for executing one or more queries."""
36
-
37
- writer: str | list[str] = 'console'
38
-
39
-
40
- class ApiQueryExecutor(executor.Executor):
41
- """Gets data from API and writes them to local/remote storage.
42
-
43
- Attributes:
44
- api_client: a client used for connecting to API.
45
- """
46
-
47
- def __init__(self, fetcher: report_fetcher.ApiReportFetcher) -> None:
48
- """Initializes ApiQueryExecutor.
49
-
50
- Args:
51
- fetcher: Instantiated report fetcher.
52
- """
53
- self.fetcher = fetcher
54
- super().__init__(
55
- preprocessors=self.fetcher.preprocessors,
56
- postprocessors=self.fetcher.postprocessors,
57
- )
58
-
59
- @classmethod
60
- def from_fetcher_alias(
61
- cls,
62
- source: str,
63
- fetcher_parameters: dict[str, str] | None = None,
64
- enable_cache: bool = False,
65
- cache_ttl_seconds: int = 3600,
66
- ) -> ApiQueryExecutor:
67
- if not fetcher_parameters:
68
- fetcher_parameters = {}
69
- concrete_api_fetcher = fetchers.get_report_fetcher(source)
70
- return ApiQueryExecutor(
71
- fetcher=concrete_api_fetcher(
72
- **fetcher_parameters,
73
- enable_cache=enable_cache,
74
- cache_ttl_seconds=cache_ttl_seconds,
75
- )
76
- )
77
-
78
- @tracer.start_as_current_span('api.execute')
79
- def execute(
80
- self,
81
- query: str,
82
- title: str,
83
- context: ApiExecutionContext,
84
- ) -> str:
85
- """Reads query, extract results and stores them in a specified location.
86
-
87
- Args:
88
- query: Location of the query.
89
- title: Name of the query.
90
- context: Query execution context.
91
-
92
- Returns:
93
- Result of writing the report.
94
-
95
- Raises:
96
- GarfExecutorError: When failed to execute query.
97
- """
98
- span = trace.get_current_span()
99
- span.set_attribute('fetcher.class', self.fetcher.__class__.__name__)
100
- span.set_attribute(
101
- 'api.client.class', self.fetcher.api_client.__class__.__name__
102
- )
103
- try:
104
- span.set_attribute('query.title', title)
105
- span.set_attribute('query.text', query)
106
- logger.debug('starting query %s', query)
107
- results = self.fetcher.fetch(
108
- query_specification=query,
109
- args=context.query_parameters,
110
- **context.fetcher_parameters,
111
- )
112
- writer_clients = context.writer_clients
113
- if not writer_clients:
114
- logger.warning('No writers configured, skipping write operation')
115
- return None
116
- writing_results = []
117
- for writer_client in writer_clients:
118
- logger.debug(
119
- 'Start writing data for query %s via %s writer',
120
- title,
121
- type(writer_client),
122
- )
123
- result = writer_client.write(results, title)
124
- logger.debug(
125
- 'Finish writing data for query %s via %s writer',
126
- title,
127
- type(writer_client),
128
- )
129
- writing_results.append(result)
130
- logger.info('%s executed successfully', title)
131
- # Return the last writer's result for backward compatibility
132
- return writing_results[-1] if writing_results else None
133
- except Exception as e:
134
- logger.error('%s generated an exception: %s', title, str(e))
135
- raise exceptions.GarfExecutorError(
136
- '%s generated an exception: %s', title, str(e)
137
- ) from e
20
+ warnings.warn(
21
+ "The 'garf_executors' namespace is deprecated. "
22
+ "Please use 'garf.executors' instead.",
23
+ DeprecationWarning,
24
+ stacklevel=2,
25
+ )