garf-executors 0.1.4__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/entrypoints/grpc_server.py +67 -0
  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/garf_pb2.py +45 -0
  16. garf/executors/garf_pb2_grpc.py +97 -0
  17. garf/executors/query_processor.py +61 -0
  18. garf/executors/sql_executor.py +142 -0
  19. garf/executors/telemetry.py +20 -0
  20. garf/executors/workflow.py +109 -0
  21. garf_executors/__init__.py +9 -44
  22. garf_executors/api_executor.py +9 -99
  23. garf_executors/bq_executor.py +9 -144
  24. garf_executors/config.py +9 -35
  25. garf_executors/entrypoints/__init__.py +25 -0
  26. garf_executors/entrypoints/cli.py +9 -116
  27. garf_executors/entrypoints/grcp_server.py +25 -0
  28. garf_executors/entrypoints/server.py +9 -92
  29. garf_executors/entrypoints/tracer.py +9 -26
  30. garf_executors/entrypoints/utils.py +9 -124
  31. garf_executors/exceptions.py +11 -3
  32. garf_executors/execution_context.py +9 -67
  33. garf_executors/executor.py +9 -71
  34. garf_executors/fetchers.py +9 -59
  35. garf_executors/sql_executor.py +9 -107
  36. garf_executors/telemetry.py +10 -5
  37. garf_executors/workflow.py +25 -0
  38. {garf_executors-0.1.4.dist-info → garf_executors-1.0.2.dist-info}/METADATA +17 -7
  39. garf_executors-1.0.2.dist-info/RECORD +42 -0
  40. garf_executors-1.0.2.dist-info/entry_points.txt +2 -0
  41. {garf_executors-0.1.4.dist-info → garf_executors-1.0.2.dist-info}/top_level.txt +1 -0
  42. garf_executors-0.1.4.dist-info/RECORD +0 -20
  43. garf_executors-0.1.4.dist-info/entry_points.txt +0 -2
  44. {garf_executors-0.1.4.dist-info → garf_executors-1.0.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,60 @@
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
+ """Executors to fetch data from various APIs."""
15
+
16
+ from __future__ import annotations
17
+
18
+ import importlib
19
+
20
+ from garf.executors import executor, fetchers
21
+ from garf.executors.api_executor import ApiExecutionContext, ApiQueryExecutor
22
+ from garf.executors.telemetry import tracer
23
+
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__ = '1.0.2'
@@ -0,0 +1,143 @@
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
+ """Module for executing Garf queries and writing them to local/remote.
15
+
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
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+
25
+ from garf.core import report_fetcher
26
+ from garf.executors import (
27
+ exceptions,
28
+ execution_context,
29
+ executor,
30
+ fetchers,
31
+ query_processor,
32
+ )
33
+ from garf.executors.telemetry import tracer
34
+ from opentelemetry import trace
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class ApiExecutionContext(execution_context.ExecutionContext):
40
+ """Common context for executing one or more queries."""
41
+
42
+ writer: str | list[str] = 'console'
43
+
44
+
45
+ class ApiQueryExecutor(executor.Executor):
46
+ """Gets data from API and writes them to local/remote storage.
47
+
48
+ Attributes:
49
+ api_client: a client used for connecting to API.
50
+ """
51
+
52
+ def __init__(self, fetcher: report_fetcher.ApiReportFetcher) -> None:
53
+ """Initializes ApiQueryExecutor.
54
+
55
+ Args:
56
+ fetcher: Instantiated report fetcher.
57
+ """
58
+ self.fetcher = fetcher
59
+ super().__init__(
60
+ preprocessors=self.fetcher.preprocessors,
61
+ postprocessors=self.fetcher.postprocessors,
62
+ )
63
+
64
+ @classmethod
65
+ def from_fetcher_alias(
66
+ cls,
67
+ source: str,
68
+ fetcher_parameters: dict[str, str] | None = None,
69
+ enable_cache: bool = False,
70
+ cache_ttl_seconds: int = 3600,
71
+ ) -> ApiQueryExecutor:
72
+ if not fetcher_parameters:
73
+ fetcher_parameters = {}
74
+ concrete_api_fetcher = fetchers.get_report_fetcher(source)
75
+ return ApiQueryExecutor(
76
+ fetcher=concrete_api_fetcher(
77
+ **fetcher_parameters,
78
+ enable_cache=enable_cache,
79
+ cache_ttl_seconds=cache_ttl_seconds,
80
+ )
81
+ )
82
+
83
+ @tracer.start_as_current_span('api.execute')
84
+ def execute(
85
+ self,
86
+ query: str,
87
+ title: str,
88
+ context: ApiExecutionContext,
89
+ ) -> str:
90
+ """Reads query, extract results and stores them in a specified location.
91
+
92
+ Args:
93
+ query: Location of the query.
94
+ title: Name of the query.
95
+ context: Query execution context.
96
+
97
+ Returns:
98
+ Result of writing the report.
99
+
100
+ Raises:
101
+ GarfExecutorError: When failed to execute query.
102
+ """
103
+ context = query_processor.process_gquery(context)
104
+ span = trace.get_current_span()
105
+ span.set_attribute('fetcher.class', self.fetcher.__class__.__name__)
106
+ span.set_attribute(
107
+ 'api.client.class', self.fetcher.api_client.__class__.__name__
108
+ )
109
+ try:
110
+ span.set_attribute('query.title', title)
111
+ span.set_attribute('query.text', query)
112
+ logger.debug('starting query %s', query)
113
+ results = self.fetcher.fetch(
114
+ query_specification=query,
115
+ args=context.query_parameters,
116
+ **context.fetcher_parameters,
117
+ )
118
+ writer_clients = context.writer_clients
119
+ if not writer_clients:
120
+ logger.warning('No writers configured, skipping write operation')
121
+ return None
122
+ writing_results = []
123
+ for writer_client in writer_clients:
124
+ logger.debug(
125
+ 'Start writing data for query %s via %s writer',
126
+ title,
127
+ type(writer_client),
128
+ )
129
+ result = writer_client.write(results, title)
130
+ logger.debug(
131
+ 'Finish writing data for query %s via %s writer',
132
+ title,
133
+ type(writer_client),
134
+ )
135
+ writing_results.append(result)
136
+ logger.info('%s executed successfully', title)
137
+ # Return the last writer's result for backward compatibility
138
+ return writing_results[-1] if writing_results else None
139
+ except Exception as e:
140
+ logger.error('%s generated an exception: %s', title, str(e))
141
+ raise exceptions.GarfExecutorError(
142
+ '%s generated an exception: %s', title, str(e)
143
+ ) from e
@@ -0,0 +1,177 @@
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
+ """Executes queries in BigQuery."""
15
+
16
+ from __future__ import annotations
17
+
18
+ import contextlib
19
+ import os
20
+
21
+ try:
22
+ from google.cloud import bigquery # type: ignore
23
+ except ImportError as e:
24
+ raise ImportError(
25
+ 'Please install garf-executors with BigQuery support '
26
+ '- `pip install garf-executors[bq]`'
27
+ ) from e
28
+
29
+ import logging
30
+
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 google.cloud import exceptions as google_cloud_exceptions
35
+ from opentelemetry import trace
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class BigQueryExecutorError(exceptions.GarfExecutorError):
41
+ """Error when BigQueryExecutor fails to run query."""
42
+
43
+
44
+ class BigQueryExecutor(executor.Executor, query_editor.TemplateProcessorMixin):
45
+ """Handles query execution in BigQuery.
46
+
47
+ Attributes:
48
+ project_id: Google Cloud project id.
49
+ location: BigQuery dataset location.
50
+ client: BigQuery client.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ project_id: str | None = os.getenv('GOOGLE_CLOUD_PROJECT'),
56
+ location: str | None = None,
57
+ **kwargs: str,
58
+ ) -> None:
59
+ """Initializes BigQueryExecutor.
60
+
61
+ Args:
62
+ project_id: Google Cloud project id.
63
+ location: BigQuery dataset location.
64
+ """
65
+ if not project_id:
66
+ raise BigQueryExecutorError(
67
+ 'project_id is required. Either provide it as project_id parameter '
68
+ 'or GOOGLE_CLOUD_PROJECT env variable.'
69
+ )
70
+ self.project_id = project_id
71
+ self.location = location
72
+ super().__init__()
73
+
74
+ @property
75
+ def client(self) -> bigquery.Client:
76
+ """Instantiates bigquery client."""
77
+ return bigquery.Client(self.project_id)
78
+
79
+ @tracer.start_as_current_span('bq.execute')
80
+ def execute(
81
+ self,
82
+ query: str,
83
+ title: str,
84
+ context: execution_context.ExecutionContext = (
85
+ execution_context.ExecutionContext()
86
+ ),
87
+ ) -> report.GarfReport:
88
+ """Executes query in BigQuery.
89
+
90
+ Args:
91
+ query: Location of the query.
92
+ title: Name of the query.
93
+ context: Query execution context.
94
+
95
+ Returns:
96
+ Report with data if query returns some data otherwise empty Report.
97
+ """
98
+ span = trace.get_current_span()
99
+ logger.info('Executing script: %s', title)
100
+ query_text = self.replace_params_template(query, context.query_parameters)
101
+ self.create_datasets(context.query_parameters.macro)
102
+ job = self.client.query(query_text)
103
+ try:
104
+ result = job.result()
105
+ except google_cloud_exceptions.GoogleCloudError as e:
106
+ raise BigQueryExecutorError(
107
+ f'Failed to execute query {title}: Reason: {e}'
108
+ ) from e
109
+ logger.debug('%s launched successfully', title)
110
+ if result.total_rows:
111
+ results = report.GarfReport.from_pandas(result.to_dataframe())
112
+ else:
113
+ results = report.GarfReport()
114
+ if context.writer and results:
115
+ writer_clients = context.writer_clients
116
+ if not writer_clients:
117
+ logger.warning('No writers configured, skipping write operation')
118
+ else:
119
+ writing_results = []
120
+ for writer_client in writer_clients:
121
+ logger.debug(
122
+ 'Start writing data for query %s via %s writer',
123
+ title,
124
+ type(writer_client),
125
+ )
126
+ writing_result = writer_client.write(results, title)
127
+ logger.debug(
128
+ 'Finish writing data for query %s via %s writer',
129
+ title,
130
+ type(writer_client),
131
+ )
132
+ writing_results.append(writing_result)
133
+ # Return the last writer's result for backward compatibility
134
+ logger.info('%s executed successfully', title)
135
+ return writing_results[-1] if writing_results else None
136
+ logger.info('%s executed successfully', title)
137
+ span.set_attribute('execute.num_results', len(results))
138
+ return results
139
+
140
+ @tracer.start_as_current_span('bq.create_datasets')
141
+ def create_datasets(self, macros: dict | None) -> None:
142
+ """Creates datasets in BQ based on values in a dict.
143
+
144
+ If dict contains keys with 'dataset' in them, then values for such keys
145
+ are treated as dataset names.
146
+
147
+ Args:
148
+ macros: Mapping containing data for query execution.
149
+ """
150
+ if macros and (datasets := extract_datasets(macros)):
151
+ for dataset in datasets:
152
+ dataset_id = f'{self.project_id}.{dataset}'
153
+ try:
154
+ self.client.get_dataset(dataset_id)
155
+ except google_cloud_exceptions.NotFound:
156
+ bq_dataset = bigquery.Dataset(dataset_id)
157
+ bq_dataset.location = self.location
158
+ with contextlib.suppress(google_cloud_exceptions.Conflict):
159
+ self.client.create_dataset(bq_dataset, timeout=30)
160
+ logger.info('Created new dataset %s', dataset_id)
161
+
162
+
163
+ def extract_datasets(macros: dict | None) -> list[str]:
164
+ """Finds dataset-related keys based on values in a dict.
165
+
166
+ If dict contains keys with 'dataset' in them, then values for such keys
167
+ are treated as dataset names.
168
+
169
+ Args:
170
+ macros: Mapping containing data for query execution.
171
+
172
+ Returns:
173
+ Possible names of datasets.
174
+ """
175
+ if not macros:
176
+ return []
177
+ return [value for macro, value in macros.items() if 'dataset' in macro]
@@ -0,0 +1,52 @@
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
+
17
+ """Stores mapping between API aliases and their execution context."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import pathlib
23
+
24
+ import pydantic
25
+ import smart_open
26
+ import yaml
27
+ from garf.executors.execution_context import ExecutionContext
28
+
29
+
30
+ class Config(pydantic.BaseModel):
31
+ """Stores necessary parameters for one or multiple API sources.
32
+
33
+ Attributes:
34
+ source: Mapping between API source alias and execution parameters.
35
+ """
36
+
37
+ sources: dict[str, ExecutionContext]
38
+
39
+ @classmethod
40
+ def from_file(cls, path: str | pathlib.Path | os.PathLike[str]) -> Config:
41
+ """Builds config from local or remote yaml file."""
42
+ with smart_open.open(path, 'r', encoding='utf-8') as f:
43
+ data = yaml.safe_load(f)
44
+ return Config(sources=data)
45
+
46
+ def save(self, path: str | pathlib.Path | os.PathLike[str]) -> str:
47
+ """Saves config to local or remote yaml file."""
48
+ with smart_open.open(path, 'w', encoding='utf-8') as f:
49
+ yaml.dump(
50
+ self.model_dump(exclude_none=True).get('sources'), f, encoding='utf-8'
51
+ )
52
+ return f'Config is saved to {str(path)}'
File without changes
@@ -0,0 +1,177 @@
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
+ """Module for defining `garf` CLI utility.
15
+
16
+ `garf` allows to execute queries and store results in local/remote
17
+ storage.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import logging
24
+ import pathlib
25
+ import sys
26
+
27
+ import garf.executors
28
+ from garf.executors import config, exceptions, workflow
29
+ from garf.executors.entrypoints import utils
30
+ from garf.executors.entrypoints.tracer import initialize_tracer
31
+ from garf.executors.telemetry import tracer
32
+ from garf.io import reader
33
+ from opentelemetry import trace
34
+
35
+ initialize_tracer()
36
+
37
+
38
+ @tracer.start_as_current_span('garf.entrypoints.cli')
39
+ def main():
40
+ parser = argparse.ArgumentParser()
41
+ parser.add_argument('query', nargs='*')
42
+ parser.add_argument('-c', '--config', dest='config', default=None)
43
+ parser.add_argument('-w', '--workflow', dest='workflow', default=None)
44
+ parser.add_argument('--source', dest='source', default=None)
45
+ parser.add_argument('--output', dest='output', default='console')
46
+ parser.add_argument('--input', dest='input', default='file')
47
+ parser.add_argument('--log', '--loglevel', dest='loglevel', default='info')
48
+ parser.add_argument('--logger', dest='logger', default='local')
49
+ parser.add_argument('--log-name', dest='log_name', default='garf')
50
+ parser.add_argument(
51
+ '--parallel-queries', dest='parallel_queries', action='store_true'
52
+ )
53
+ parser.add_argument(
54
+ '--no-parallel-queries', dest='parallel_queries', action='store_false'
55
+ )
56
+ parser.add_argument('--dry-run', dest='dry_run', action='store_true')
57
+ parser.add_argument('-v', '--version', dest='version', action='store_true')
58
+ parser.add_argument(
59
+ '--parallel-threshold', dest='parallel_threshold', default=10, type=int
60
+ )
61
+ parser.add_argument(
62
+ '--enable-cache', dest='enable_cache', action='store_true'
63
+ )
64
+ parser.add_argument(
65
+ '--cache-ttl-seconds',
66
+ dest='cache_ttl_seconds',
67
+ default=3600,
68
+ type=int,
69
+ )
70
+ parser.set_defaults(parallel_queries=True)
71
+ parser.set_defaults(enable_cache=False)
72
+ parser.set_defaults(dry_run=False)
73
+ args, kwargs = parser.parse_known_args()
74
+
75
+ span = trace.get_current_span()
76
+ command_args = ' '.join(sys.argv[1:])
77
+ span.set_attribute('cli.command', f'garf {command_args}')
78
+ if args.version:
79
+ print(garf.executors.__version__)
80
+ sys.exit()
81
+ logger = utils.init_logging(
82
+ loglevel=args.loglevel.upper(), logger_type=args.logger, name=args.log_name
83
+ )
84
+ reader_client = reader.create_reader(args.input)
85
+ if workflow_file := args.workflow:
86
+ wf_parent = pathlib.Path.cwd() / pathlib.Path(workflow_file).parent
87
+ execution_workflow = workflow.Workflow.from_file(workflow_file)
88
+ for i, step in enumerate(execution_workflow.steps, 1):
89
+ with tracer.start_as_current_span(f'{i}-{step.fetcher}'):
90
+ query_executor = garf.executors.setup_executor(
91
+ source=step.fetcher,
92
+ fetcher_parameters=step.fetcher_parameters,
93
+ enable_cache=args.enable_cache,
94
+ cache_ttl_seconds=args.cache_ttl_seconds,
95
+ )
96
+ batch = {}
97
+ if not (queries := step.queries):
98
+ logger.error('Please provide one or more queries to run')
99
+ raise exceptions.GarfExecutorError(
100
+ 'Please provide one or more queries to run'
101
+ )
102
+ for query in queries:
103
+ if isinstance(query, garf.executors.workflow.QueryPath):
104
+ query_path = wf_parent / pathlib.Path(query.path)
105
+ if not query_path.exists():
106
+ raise workflow.GarfWorkflowError(f'Query: {query_path} not found')
107
+ batch[query.path] = reader_client.read(query_path)
108
+ elif isinstance(query, garf.executors.workflow.QueryFolder):
109
+ query_path = wf_parent / pathlib.Path(query.folder)
110
+ if not query_path.exists():
111
+ raise workflow.GarfWorkflowError(
112
+ f'Folder: {query_path} not found'
113
+ )
114
+ for p in query_path.rglob('*'):
115
+ if p.suffix == '.sql':
116
+ batch[p.stem] = reader_client.read(p)
117
+ else:
118
+ batch[query.query.title] = query.query.text
119
+ query_executor.execute_batch(
120
+ batch, step.context, args.parallel_threshold
121
+ )
122
+ sys.exit()
123
+
124
+ if not args.query:
125
+ logger.error('Please provide one or more queries to run')
126
+ raise exceptions.GarfExecutorError(
127
+ 'Please provide one or more queries to run'
128
+ )
129
+ if config_file := args.config:
130
+ execution_config = config.Config.from_file(config_file)
131
+ if not (context := execution_config.sources.get(args.source)):
132
+ raise exceptions.GarfExecutorError(
133
+ f'No execution context found for source {args.source} in {config_file}'
134
+ )
135
+ else:
136
+ param_types = ['source', 'macro', 'template']
137
+ outputs = args.output.split(',')
138
+ extra_parameters = utils.ParamsParser([*param_types, *outputs]).parse(
139
+ kwargs
140
+ )
141
+ source_parameters = extra_parameters.get('source', {})
142
+ writer_parameters = {}
143
+ for output in outputs:
144
+ writer_parameters.update(extra_parameters.get(output))
145
+
146
+ context = garf.executors.api_executor.ApiExecutionContext(
147
+ query_parameters={
148
+ 'macro': extra_parameters.get('macro'),
149
+ 'template': extra_parameters.get('template'),
150
+ },
151
+ writer=outputs,
152
+ writer_parameters=writer_parameters,
153
+ fetcher_parameters=source_parameters,
154
+ )
155
+ query_executor = garf.executors.setup_executor(
156
+ source=args.source,
157
+ fetcher_parameters=context.fetcher_parameters,
158
+ enable_cache=args.enable_cache,
159
+ cache_ttl_seconds=args.cache_ttl_seconds,
160
+ )
161
+ batch = {query: reader_client.read(query) for query in args.query}
162
+ if args.parallel_queries and len(args.query) > 1:
163
+ logger.info('Running queries in parallel')
164
+ batch = {query: reader_client.read(query) for query in args.query}
165
+ query_executor.execute_batch(batch, context, args.parallel_threshold)
166
+ else:
167
+ if len(args.query) > 1:
168
+ logger.info('Running queries sequentially')
169
+ for query in args.query:
170
+ query_executor.execute(
171
+ query=reader_client.read(query), title=query, context=context
172
+ )
173
+ logging.shutdown()
174
+
175
+
176
+ if __name__ == '__main__':
177
+ main()
@@ -0,0 +1,67 @@
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
+ """gRPC endpoint for garf."""
16
+
17
+ import argparse
18
+ import logging
19
+ from concurrent import futures
20
+
21
+ import garf.executors
22
+ import grpc
23
+ from garf.executors import garf_pb2, garf_pb2_grpc
24
+ from garf.executors.entrypoints.tracer import initialize_tracer
25
+ from google.protobuf.json_format import MessageToDict
26
+ from grpc_reflection.v1alpha import reflection
27
+
28
+
29
+ class GarfService(garf_pb2_grpc.GarfService):
30
+ def Execute(self, request, context):
31
+ query_executor = garf.executors.setup_executor(
32
+ request.source, request.context.fetcher_parameters
33
+ )
34
+ execution_context = garf.executors.execution_context.ExecutionContext(
35
+ **MessageToDict(request.context, preserving_proto_field_name=True)
36
+ )
37
+ result = query_executor.execute(
38
+ query=request.query,
39
+ title=request.title,
40
+ context=execution_context,
41
+ )
42
+ return garf_pb2.ExecuteResponse(results=[result])
43
+
44
+
45
+ if __name__ == '__main__':
46
+ parser = argparse.ArgumentParser()
47
+ parser.add_argument('--port', dest='port', default=50051, type=int)
48
+ parser.add_argument(
49
+ '--parallel-threshold', dest='parallel_threshold', default=10, type=int
50
+ )
51
+ args, _ = parser.parse_known_args()
52
+ initialize_tracer()
53
+ server = grpc.server(
54
+ futures.ThreadPoolExecutor(max_workers=args.parallel_threshold)
55
+ )
56
+
57
+ service = GarfService()
58
+ garf_pb2_grpc.add_GarfServiceServicer_to_server(service, server)
59
+ SERVICE_NAMES = (
60
+ garf_pb2.DESCRIPTOR.services_by_name['GarfService'].full_name,
61
+ reflection.SERVICE_NAME,
62
+ )
63
+ reflection.enable_server_reflection(SERVICE_NAMES, server)
64
+ server.add_insecure_port(f'[::]:{args.port}')
65
+ server.start()
66
+ logging.info('Garf service started, listening on port %d', 50051)
67
+ server.wait_for_termination()