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.
- garf/executors/__init__.py +60 -0
- garf/executors/api_executor.py +143 -0
- garf/executors/bq_executor.py +177 -0
- garf/executors/config.py +52 -0
- garf/executors/entrypoints/__init__.py +0 -0
- garf/executors/entrypoints/cli.py +177 -0
- {garf_executors → garf/executors}/entrypoints/grpc_server.py +5 -6
- garf/executors/entrypoints/server.py +117 -0
- garf/executors/entrypoints/tracer.py +57 -0
- garf/executors/entrypoints/utils.py +140 -0
- garf/executors/exceptions.py +17 -0
- garf/executors/execution_context.py +117 -0
- garf/executors/executor.py +124 -0
- garf/executors/fetchers.py +78 -0
- garf/executors/query_processor.py +61 -0
- garf/executors/sql_executor.py +142 -0
- garf/executors/telemetry.py +20 -0
- garf/executors/workflow.py +109 -0
- garf_executors/__init__.py +9 -44
- garf_executors/api_executor.py +9 -121
- garf_executors/bq_executor.py +9 -161
- garf_executors/config.py +9 -37
- garf_executors/entrypoints/__init__.py +25 -0
- garf_executors/entrypoints/cli.py +9 -148
- garf_executors/entrypoints/grcp_server.py +25 -0
- garf_executors/entrypoints/server.py +9 -102
- garf_executors/entrypoints/tracer.py +8 -40
- garf_executors/entrypoints/utils.py +9 -124
- garf_executors/exceptions.py +11 -3
- garf_executors/execution_context.py +9 -100
- garf_executors/executor.py +9 -108
- garf_executors/fetchers.py +9 -63
- garf_executors/sql_executor.py +9 -125
- garf_executors/telemetry.py +10 -5
- garf_executors/workflow.py +8 -79
- {garf_executors-0.2.3.dist-info → garf_executors-1.0.2.dist-info}/METADATA +11 -5
- garf_executors-1.0.2.dist-info/RECORD +42 -0
- garf_executors-1.0.2.dist-info/entry_points.txt +2 -0
- {garf_executors-0.2.3.dist-info → garf_executors-1.0.2.dist-info}/top_level.txt +1 -0
- garf_executors-0.2.3.dist-info/RECORD +0 -24
- garf_executors-0.2.3.dist-info/entry_points.txt +0 -2
- {garf_executors → garf/executors}/garf_pb2.py +0 -0
- {garf_executors → garf/executors}/garf_pb2_grpc.py +0 -0
- {garf_executors-0.2.3.dist-info → garf_executors-1.0.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
"""FastAPI endpoint for executing queries."""
|
|
16
|
+
|
|
17
|
+
from typing import Optional, Union
|
|
18
|
+
|
|
19
|
+
import fastapi
|
|
20
|
+
import garf.executors
|
|
21
|
+
import pydantic
|
|
22
|
+
import typer
|
|
23
|
+
import uvicorn
|
|
24
|
+
from garf.executors import exceptions
|
|
25
|
+
from garf.executors.entrypoints.tracer import initialize_tracer
|
|
26
|
+
from garf.io import reader
|
|
27
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
28
|
+
from typing_extensions import Annotated
|
|
29
|
+
|
|
30
|
+
initialize_tracer()
|
|
31
|
+
app = fastapi.FastAPI()
|
|
32
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
33
|
+
typer_app = typer.Typer()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ApiExecutorRequest(pydantic.BaseModel):
|
|
37
|
+
"""Request for executing a query.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
source: Type of API to interact with.
|
|
41
|
+
title: Name of the query used as an output for writing.
|
|
42
|
+
query: Query to execute.
|
|
43
|
+
query_path: Local or remote path to query.
|
|
44
|
+
context: Execution context.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
source: str
|
|
48
|
+
title: Optional[str] = None
|
|
49
|
+
query: Optional[str] = None
|
|
50
|
+
query_path: Optional[Union[str, list[str]]] = None
|
|
51
|
+
context: garf.executors.api_executor.ApiExecutionContext
|
|
52
|
+
|
|
53
|
+
@pydantic.model_validator(mode='after')
|
|
54
|
+
def check_query_specified(self):
|
|
55
|
+
if not self.query_path and not self.query:
|
|
56
|
+
raise exceptions.GarfExecutorError(
|
|
57
|
+
'Missing one of required parameters: query, query_path'
|
|
58
|
+
)
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def model_post_init(self, __context__) -> None:
|
|
62
|
+
if self.query_path and isinstance(self.query_path, str):
|
|
63
|
+
self.query = reader.FileReader().read(self.query_path)
|
|
64
|
+
if not self.title:
|
|
65
|
+
self.title = str(self.query_path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ApiExecutorResponse(pydantic.BaseModel):
|
|
69
|
+
"""Response after executing a query.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
results: Results of query execution.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
results: list[str]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.get('/api/version')
|
|
79
|
+
async def version() -> str:
|
|
80
|
+
return garf.executors.__version__
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.get('/api/fetchers')
|
|
84
|
+
async def get_fetchers() -> list[str]:
|
|
85
|
+
"""Shows all available API sources."""
|
|
86
|
+
return list(garf.executors.fetchers.find_fetchers())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.post('/api/execute')
|
|
90
|
+
async def execute(request: ApiExecutorRequest) -> ApiExecutorResponse:
|
|
91
|
+
query_executor = garf.executors.setup_executor(
|
|
92
|
+
request.source, request.context.fetcher_parameters
|
|
93
|
+
)
|
|
94
|
+
result = query_executor.execute(request.query, request.title, request.context)
|
|
95
|
+
return ApiExecutorResponse(results=[result])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.post('/api/execute:batch')
|
|
99
|
+
def execute_batch(request: ApiExecutorRequest) -> ApiExecutorResponse:
|
|
100
|
+
query_executor = garf.executors.setup_executor(
|
|
101
|
+
request.source, request.context.fetcher_parameters
|
|
102
|
+
)
|
|
103
|
+
reader_client = reader.FileReader()
|
|
104
|
+
batch = {query: reader_client.read(query) for query in request.query_path}
|
|
105
|
+
results = query_executor.execute_batch(batch, request.context)
|
|
106
|
+
return ApiExecutorResponse(results=results)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@typer_app.command()
|
|
110
|
+
def main(
|
|
111
|
+
port: Annotated[int, typer.Option(help='Port to start the server')] = 8000,
|
|
112
|
+
):
|
|
113
|
+
uvicorn.run(app, port=port)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == '__main__':
|
|
117
|
+
typer_app()
|
|
@@ -0,0 +1,57 @@
|
|
|
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 os
|
|
16
|
+
|
|
17
|
+
from opentelemetry import trace
|
|
18
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
19
|
+
OTLPSpanExporter,
|
|
20
|
+
)
|
|
21
|
+
from opentelemetry.sdk.resources import Resource
|
|
22
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
23
|
+
from opentelemetry.sdk.trace.export import (
|
|
24
|
+
BatchSpanProcessor,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
DEFAULT_SERVICE_NAME = 'garf'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def initialize_tracer():
|
|
31
|
+
resource = Resource.create(
|
|
32
|
+
{'service.name': os.getenv('OTLP_SERVICE_NAME', DEFAULT_SERVICE_NAME)}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
36
|
+
|
|
37
|
+
if otel_endpoint := os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT'):
|
|
38
|
+
if gcp_project_id := os.getenv('OTEL_EXPORTER_GCP_PROJECT_ID'):
|
|
39
|
+
try:
|
|
40
|
+
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
|
41
|
+
except ImportError as e:
|
|
42
|
+
raise ImportError(
|
|
43
|
+
'Please install garf-executors with GCP support '
|
|
44
|
+
'- `pip install garf-executors[gcp]`'
|
|
45
|
+
) from e
|
|
46
|
+
|
|
47
|
+
cloud_span_processor = BatchSpanProcessor(
|
|
48
|
+
CloudTraceSpanExporter(project_id=gcp_project_id)
|
|
49
|
+
)
|
|
50
|
+
tracer_provider.add_span_processor(cloud_span_processor)
|
|
51
|
+
else:
|
|
52
|
+
otlp_processor = BatchSpanProcessor(
|
|
53
|
+
OTLPSpanExporter(endpoint=otel_endpoint, insecure=True)
|
|
54
|
+
)
|
|
55
|
+
tracer_provider.add_span_processor(otlp_processor)
|
|
56
|
+
|
|
57
|
+
trace.set_tracer_provider(tracer_provider)
|
|
@@ -0,0 +1,140 @@
|
|
|
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 enum
|
|
19
|
+
import logging
|
|
20
|
+
import sys
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from rich import logging as rich_logging
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ParamsParser:
|
|
28
|
+
def __init__(self, identifiers: Sequence[str]) -> None:
|
|
29
|
+
self.identifiers = identifiers
|
|
30
|
+
|
|
31
|
+
def parse(self, params: Sequence) -> dict[str, dict | None]:
|
|
32
|
+
return {
|
|
33
|
+
identifier: self._parse_params(identifier, params)
|
|
34
|
+
for identifier in self.identifiers
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def _parse_params(self, identifier: str, params: Sequence[Any]) -> dict:
|
|
38
|
+
parsed_params = {}
|
|
39
|
+
if params:
|
|
40
|
+
raw_params = [param.split('=', maxsplit=1) for param in params]
|
|
41
|
+
for param in raw_params:
|
|
42
|
+
param_pair = self._identify_param_pair(identifier, param)
|
|
43
|
+
if param_pair:
|
|
44
|
+
parsed_params.update(param_pair)
|
|
45
|
+
return parsed_params
|
|
46
|
+
|
|
47
|
+
def _identify_param_pair(
|
|
48
|
+
self, identifier: str, param: Sequence[str]
|
|
49
|
+
) -> dict[str, Any] | None:
|
|
50
|
+
key = param[0]
|
|
51
|
+
if not identifier or identifier not in key:
|
|
52
|
+
return None
|
|
53
|
+
provided_identifier, *keys = key.split('.')
|
|
54
|
+
if not keys:
|
|
55
|
+
return None
|
|
56
|
+
if len(keys) > 1:
|
|
57
|
+
raise GarfParamsException(
|
|
58
|
+
f'{key} is invalid format,'
|
|
59
|
+
f'`--{identifier}.key=value` or `--{identifier}.key` '
|
|
60
|
+
'are the correct formats'
|
|
61
|
+
)
|
|
62
|
+
provided_identifier = provided_identifier.replace('--', '')
|
|
63
|
+
if provided_identifier not in self.identifiers:
|
|
64
|
+
supported_arguments = ', '.join(self.identifiers)
|
|
65
|
+
raise GarfParamsException(
|
|
66
|
+
f'CLI argument {provided_identifier} is not supported'
|
|
67
|
+
f', supported arguments {supported_arguments}'
|
|
68
|
+
)
|
|
69
|
+
if provided_identifier != identifier:
|
|
70
|
+
return None
|
|
71
|
+
key = keys[0].replace('-', '_')
|
|
72
|
+
if not key:
|
|
73
|
+
raise GarfParamsException(
|
|
74
|
+
f'{identifier} {key} is invalid,'
|
|
75
|
+
f'`--{identifier}.key=value` or `--{identifier}.key` '
|
|
76
|
+
'are the correct formats'
|
|
77
|
+
)
|
|
78
|
+
if len(param) == 2:
|
|
79
|
+
return {key: param[1]}
|
|
80
|
+
if len(param) == 1:
|
|
81
|
+
return {key: True}
|
|
82
|
+
raise GarfParamsException(
|
|
83
|
+
f'{identifier} {key} is invalid,'
|
|
84
|
+
f'`--{identifier}.key=value` or `--{identifier}.key` '
|
|
85
|
+
'are the correct formats'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class GarfParamsException(Exception):
|
|
90
|
+
"""Defines exception for incorrect parameters."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LoggerEnum(str, enum.Enum):
|
|
94
|
+
local = 'local'
|
|
95
|
+
rich = 'rich'
|
|
96
|
+
gcloud = 'gcloud'
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def init_logging(
|
|
100
|
+
loglevel: str = 'INFO',
|
|
101
|
+
logger_type: str | LoggerEnum = 'local',
|
|
102
|
+
name: str = __name__,
|
|
103
|
+
) -> logging.Logger:
|
|
104
|
+
loglevel = getattr(logging, loglevel)
|
|
105
|
+
if logger_type == 'rich':
|
|
106
|
+
logging.basicConfig(
|
|
107
|
+
format='%(message)s',
|
|
108
|
+
level=loglevel,
|
|
109
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
110
|
+
handlers=[
|
|
111
|
+
rich_logging.RichHandler(rich_tracebacks=True),
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
elif logger_type == 'gcloud':
|
|
115
|
+
try:
|
|
116
|
+
import google.cloud.logging as glogging
|
|
117
|
+
except ImportError as e:
|
|
118
|
+
raise ImportError(
|
|
119
|
+
'Please install garf-executors with Cloud logging support - '
|
|
120
|
+
'`pip install garf-executors[bq]`'
|
|
121
|
+
) from e
|
|
122
|
+
|
|
123
|
+
client = glogging.Client()
|
|
124
|
+
handler = glogging.handlers.CloudLoggingHandler(client, name=name)
|
|
125
|
+
handler.close()
|
|
126
|
+
glogging.handlers.setup_logging(handler, log_level=loglevel)
|
|
127
|
+
logging.basicConfig(
|
|
128
|
+
level=loglevel,
|
|
129
|
+
handlers=[handler],
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
logging.basicConfig(
|
|
133
|
+
format='[%(asctime)s][%(name)s][%(levelname)s] %(message)s',
|
|
134
|
+
stream=sys.stdout,
|
|
135
|
+
level=loglevel,
|
|
136
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
137
|
+
)
|
|
138
|
+
logging.getLogger('smart_open.smart_open_lib').setLevel(logging.WARNING)
|
|
139
|
+
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
|
140
|
+
return logging.getLogger(name)
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
|
|
16
|
+
class GarfExecutorError(Exception):
|
|
17
|
+
"""Base class for garf executor exceptions."""
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
"""Captures parameters for fetching data from APIs."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import pathlib
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import pydantic
|
|
26
|
+
import smart_open
|
|
27
|
+
import yaml
|
|
28
|
+
from garf.core import query_editor
|
|
29
|
+
from garf.io import writer
|
|
30
|
+
from garf.io.writers import abs_writer
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ExecutionContext(pydantic.BaseModel):
|
|
34
|
+
"""Common context for executing one or more queries.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
query_parameters: Parameters to dynamically change query text.
|
|
38
|
+
fetcher_parameters: Parameters to specify fetching setup.
|
|
39
|
+
writer: Type of writer to use. Can be a single writer string or list of writers.
|
|
40
|
+
writer_parameters: Optional parameters to setup writer.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
query_parameters: query_editor.GarfQueryParameters | None = pydantic.Field(
|
|
44
|
+
default_factory=dict
|
|
45
|
+
)
|
|
46
|
+
fetcher_parameters: dict[str, Any] | None = pydantic.Field(
|
|
47
|
+
default_factory=dict
|
|
48
|
+
)
|
|
49
|
+
writer: str | list[str] | None = None
|
|
50
|
+
writer_parameters: dict[str, str] | None = pydantic.Field(
|
|
51
|
+
default_factory=dict
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def model_post_init(self, __context__) -> None:
|
|
55
|
+
if self.fetcher_parameters is None:
|
|
56
|
+
self.fetcher_parameters = {}
|
|
57
|
+
if self.writer_parameters is None:
|
|
58
|
+
self.writer_parameters = {}
|
|
59
|
+
if not self.query_parameters:
|
|
60
|
+
self.query_parameters = query_editor.GarfQueryParameters()
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_file(
|
|
64
|
+
cls, path: str | pathlib.Path | os.PathLike[str]
|
|
65
|
+
) -> ExecutionContext:
|
|
66
|
+
"""Builds context from local or remote yaml file."""
|
|
67
|
+
with smart_open.open(path, 'r', encoding='utf-8') as f:
|
|
68
|
+
data = yaml.safe_load(f)
|
|
69
|
+
return ExecutionContext(**data)
|
|
70
|
+
|
|
71
|
+
def save(self, path: str | pathlib.Path | os.PathLike[str]) -> str:
|
|
72
|
+
"""Saves context to local or remote yaml file."""
|
|
73
|
+
with smart_open.open(path, 'w', encoding='utf-8') as f:
|
|
74
|
+
yaml.dump(self.model_dump(), f, encoding='utf-8')
|
|
75
|
+
return f'ExecutionContext is saved to {str(path)}'
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def writer_client(self) -> abs_writer.AbsWriter:
|
|
79
|
+
"""Returns single writer client."""
|
|
80
|
+
if isinstance(self.writer, list) and len(self.writer) > 0:
|
|
81
|
+
writer_type = self.writer[0]
|
|
82
|
+
else:
|
|
83
|
+
writer_type = self.writer
|
|
84
|
+
|
|
85
|
+
writer_params = self.writer_parameters or {}
|
|
86
|
+
|
|
87
|
+
if not writer_type:
|
|
88
|
+
raise ValueError('No writer specified')
|
|
89
|
+
|
|
90
|
+
writer_client = writer.create_writer(writer_type, **writer_params)
|
|
91
|
+
if writer_type == 'bq':
|
|
92
|
+
_ = writer_client.create_or_get_dataset()
|
|
93
|
+
if writer_type == 'sheet':
|
|
94
|
+
writer_client.init_client()
|
|
95
|
+
return writer_client
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def writer_clients(self) -> list[abs_writer.AbsWriter]:
|
|
99
|
+
"""Returns list of writer clients."""
|
|
100
|
+
if not self.writer:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
# Convert single writer to list for uniform processing
|
|
104
|
+
writers_to_use = (
|
|
105
|
+
self.writer if isinstance(self.writer, list) else [self.writer]
|
|
106
|
+
)
|
|
107
|
+
writer_params = self.writer_parameters or {}
|
|
108
|
+
|
|
109
|
+
clients = []
|
|
110
|
+
for writer_type in writers_to_use:
|
|
111
|
+
writer_client = writer.create_writer(writer_type, **writer_params)
|
|
112
|
+
if writer_type == 'bq':
|
|
113
|
+
_ = writer_client.create_or_get_dataset()
|
|
114
|
+
if writer_type == 'sheet':
|
|
115
|
+
writer_client.init_client()
|
|
116
|
+
clients.append(writer_client)
|
|
117
|
+
return clients
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
"""Defines common functionality between executors."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import inspect
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from garf.core import report_fetcher
|
|
22
|
+
from garf.executors import execution_context, query_processor
|
|
23
|
+
from garf.executors.telemetry import tracer
|
|
24
|
+
from opentelemetry import trace
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Executor:
|
|
28
|
+
"""Defines common functionality between executors."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
preprocessors: Optional[dict[str, report_fetcher.Processor]] = None,
|
|
33
|
+
postprocessors: Optional[dict[str, report_fetcher.Processor]] = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.preprocessors = preprocessors or {}
|
|
36
|
+
self.postprocessors = postprocessors or {}
|
|
37
|
+
|
|
38
|
+
@tracer.start_as_current_span('api.execute_batch')
|
|
39
|
+
def execute_batch(
|
|
40
|
+
self,
|
|
41
|
+
batch: dict[str, str],
|
|
42
|
+
context: execution_context.ExecutionContext,
|
|
43
|
+
parallel_threshold: int = 10,
|
|
44
|
+
) -> list[str]:
|
|
45
|
+
"""Executes batch of queries for a common context.
|
|
46
|
+
|
|
47
|
+
If an executor has any pre/post processors, executes them first while
|
|
48
|
+
modifying the context.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
batch: Mapping between query_title and its text.
|
|
52
|
+
context: Execution context.
|
|
53
|
+
parallel_threshold: Number of queries to execute in parallel.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Results of execution.
|
|
57
|
+
"""
|
|
58
|
+
span = trace.get_current_span()
|
|
59
|
+
span.set_attribute('api.parallel_threshold', parallel_threshold)
|
|
60
|
+
_handle_processors(processors=self.preprocessors, context=context)
|
|
61
|
+
results = asyncio.run(
|
|
62
|
+
self._run(
|
|
63
|
+
batch=batch, context=context, parallel_threshold=parallel_threshold
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
_handle_processors(processors=self.postprocessors, context=context)
|
|
67
|
+
return results
|
|
68
|
+
|
|
69
|
+
def add_preprocessor(
|
|
70
|
+
self, preprocessors: dict[str, report_fetcher.Processor]
|
|
71
|
+
) -> None:
|
|
72
|
+
self.preprocessors.update(preprocessors)
|
|
73
|
+
|
|
74
|
+
async def aexecute(
|
|
75
|
+
self,
|
|
76
|
+
query: str,
|
|
77
|
+
title: str,
|
|
78
|
+
context: execution_context.ExecutionContext,
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Performs query execution asynchronously.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
query: Location of the query.
|
|
84
|
+
title: Name of the query.
|
|
85
|
+
context: Query execution context.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Result of writing the report.
|
|
89
|
+
"""
|
|
90
|
+
return await asyncio.to_thread(self.execute, query, title, context)
|
|
91
|
+
|
|
92
|
+
async def _run(
|
|
93
|
+
self,
|
|
94
|
+
batch: dict[str, str],
|
|
95
|
+
context: execution_context.ExecutionContext,
|
|
96
|
+
parallel_threshold: int,
|
|
97
|
+
):
|
|
98
|
+
semaphore = asyncio.Semaphore(value=parallel_threshold)
|
|
99
|
+
|
|
100
|
+
async def run_with_semaphore(fn):
|
|
101
|
+
async with semaphore:
|
|
102
|
+
return await fn
|
|
103
|
+
|
|
104
|
+
tasks = [
|
|
105
|
+
self.aexecute(query=query, title=title, context=context)
|
|
106
|
+
for title, query in batch.items()
|
|
107
|
+
]
|
|
108
|
+
return await asyncio.gather(*(run_with_semaphore(task) for task in tasks))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _handle_processors(
|
|
112
|
+
processors: dict[str, report_fetcher.Processor],
|
|
113
|
+
context: execution_context.ExecutionContext,
|
|
114
|
+
) -> None:
|
|
115
|
+
context = query_processor.process_gquery(context)
|
|
116
|
+
for k, processor in processors.items():
|
|
117
|
+
processor_signature = list(inspect.signature(processor).parameters.keys())
|
|
118
|
+
if k in context.fetcher_parameters:
|
|
119
|
+
processor_parameters = {
|
|
120
|
+
k: v
|
|
121
|
+
for k, v in context.fetcher_parameters.items()
|
|
122
|
+
if k in processor_signature
|
|
123
|
+
}
|
|
124
|
+
context.fetcher_parameters[k] = processor(**processor_parameters)
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
import inspect
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
from importlib.metadata import entry_points
|
|
19
|
+
|
|
20
|
+
from garf.core import report_fetcher
|
|
21
|
+
from garf.executors.telemetry import tracer
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(name='garf.executors.fetchers')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@tracer.start_as_current_span('find_fetchers')
|
|
27
|
+
def find_fetchers() -> set[str]:
|
|
28
|
+
"""Identifiers all available report fetchers."""
|
|
29
|
+
if entrypoints := _get_entrypoints('garf'):
|
|
30
|
+
return {fetcher.name for fetcher in entrypoints}
|
|
31
|
+
return set()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@tracer.start_as_current_span('get_report_fetcher')
|
|
35
|
+
def get_report_fetcher(source: str) -> type[report_fetcher.ApiReportFetcher]:
|
|
36
|
+
"""Loads report fetcher for a given source.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
source: Alias for a source associated with a fetcher.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Class for a found report fetcher.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ApiReportFetcherError: When fetcher cannot be loaded.
|
|
46
|
+
MissingApiReportFetcherError: When fetcher not found.
|
|
47
|
+
"""
|
|
48
|
+
if source not in find_fetchers():
|
|
49
|
+
raise report_fetcher.MissingApiReportFetcherError(source)
|
|
50
|
+
for fetcher in _get_entrypoints('garf'):
|
|
51
|
+
if fetcher.name == source:
|
|
52
|
+
try:
|
|
53
|
+
with tracer.start_as_current_span('load_fetcher_module') as span:
|
|
54
|
+
fetcher_module = fetcher.load()
|
|
55
|
+
span.set_attribute('loaded_module', fetcher_module.__name__)
|
|
56
|
+
for name, obj in inspect.getmembers(fetcher_module):
|
|
57
|
+
if inspect.isclass(obj) and issubclass(
|
|
58
|
+
obj, report_fetcher.ApiReportFetcher
|
|
59
|
+
):
|
|
60
|
+
return getattr(fetcher_module, name)
|
|
61
|
+
except ModuleNotFoundError as e:
|
|
62
|
+
raise report_fetcher.ApiReportFetcherError(
|
|
63
|
+
f'Failed to load fetcher for source {source}, reason: {e}'
|
|
64
|
+
)
|
|
65
|
+
raise report_fetcher.ApiReportFetcherError(
|
|
66
|
+
f'No fetcher available for the source "{source}"'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_entrypoints(group='garf'):
|
|
71
|
+
if sys.version_info.major == 3 and sys.version_info.minor == 9:
|
|
72
|
+
try:
|
|
73
|
+
fetchers = entry_points()[group]
|
|
74
|
+
except KeyError:
|
|
75
|
+
fetchers = []
|
|
76
|
+
else:
|
|
77
|
+
fetchers = entry_points(group=group)
|
|
78
|
+
return fetchers
|