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.
- 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/entrypoints/grpc_server.py +67 -0
- 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/garf_pb2.py +45 -0
- garf/executors/garf_pb2_grpc.py +97 -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 -99
- garf_executors/bq_executor.py +9 -144
- garf_executors/config.py +9 -35
- garf_executors/entrypoints/__init__.py +25 -0
- garf_executors/entrypoints/cli.py +9 -116
- garf_executors/entrypoints/grcp_server.py +25 -0
- garf_executors/entrypoints/server.py +9 -92
- garf_executors/entrypoints/tracer.py +9 -26
- garf_executors/entrypoints/utils.py +9 -124
- garf_executors/exceptions.py +11 -3
- garf_executors/execution_context.py +9 -67
- garf_executors/executor.py +9 -71
- garf_executors/fetchers.py +9 -59
- garf_executors/sql_executor.py +9 -107
- garf_executors/telemetry.py +10 -5
- garf_executors/workflow.py +25 -0
- {garf_executors-0.1.4.dist-info → garf_executors-1.0.2.dist-info}/METADATA +17 -7
- 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.1.4.dist-info → garf_executors-1.0.2.dist-info}/top_level.txt +1 -0
- garf_executors-0.1.4.dist-info/RECORD +0 -20
- garf_executors-0.1.4.dist-info/entry_points.txt +0 -2
- {garf_executors-0.1.4.dist-info → garf_executors-1.0.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from . import garf_pb2 as garf__pb2
|
|
7
|
+
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.75.0'
|
|
9
|
+
GRPC_VERSION = grpc.__version__
|
|
10
|
+
_version_not_supported = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from grpc._utilities import first_version_is_lower
|
|
14
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_version_not_supported = True
|
|
17
|
+
|
|
18
|
+
if _version_not_supported:
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
+
+ f' but the generated code in garf_pb2_grpc.py depends on'
|
|
22
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GarfServiceStub(object):
|
|
29
|
+
"""Missing associated documentation comment in .proto file."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, channel):
|
|
32
|
+
"""Constructor.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
channel: A grpc.Channel.
|
|
36
|
+
"""
|
|
37
|
+
self.Execute = channel.unary_unary(
|
|
38
|
+
'/garf.GarfService/Execute',
|
|
39
|
+
request_serializer=garf__pb2.ExecuteRequest.SerializeToString,
|
|
40
|
+
response_deserializer=garf__pb2.ExecuteResponse.FromString,
|
|
41
|
+
_registered_method=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GarfServiceServicer(object):
|
|
45
|
+
"""Missing associated documentation comment in .proto file."""
|
|
46
|
+
|
|
47
|
+
def Execute(self, request, context):
|
|
48
|
+
"""Missing associated documentation comment in .proto file."""
|
|
49
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
50
|
+
context.set_details('Method not implemented!')
|
|
51
|
+
raise NotImplementedError('Method not implemented!')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def add_GarfServiceServicer_to_server(servicer, server):
|
|
55
|
+
rpc_method_handlers = {
|
|
56
|
+
'Execute': grpc.unary_unary_rpc_method_handler(
|
|
57
|
+
servicer.Execute,
|
|
58
|
+
request_deserializer=garf__pb2.ExecuteRequest.FromString,
|
|
59
|
+
response_serializer=garf__pb2.ExecuteResponse.SerializeToString,
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
63
|
+
'garf.GarfService', rpc_method_handlers)
|
|
64
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
65
|
+
server.add_registered_method_handlers('garf.GarfService', rpc_method_handlers)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# This class is part of an EXPERIMENTAL API.
|
|
69
|
+
class GarfService(object):
|
|
70
|
+
"""Missing associated documentation comment in .proto file."""
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def Execute(request,
|
|
74
|
+
target,
|
|
75
|
+
options=(),
|
|
76
|
+
channel_credentials=None,
|
|
77
|
+
call_credentials=None,
|
|
78
|
+
insecure=False,
|
|
79
|
+
compression=None,
|
|
80
|
+
wait_for_ready=None,
|
|
81
|
+
timeout=None,
|
|
82
|
+
metadata=None):
|
|
83
|
+
return grpc.experimental.unary_unary(
|
|
84
|
+
request,
|
|
85
|
+
target,
|
|
86
|
+
'/garf.GarfService/Execute',
|
|
87
|
+
garf__pb2.ExecuteRequest.SerializeToString,
|
|
88
|
+
garf__pb2.ExecuteResponse.FromString,
|
|
89
|
+
options,
|
|
90
|
+
channel_credentials,
|
|
91
|
+
insecure,
|
|
92
|
+
call_credentials,
|
|
93
|
+
compression,
|
|
94
|
+
wait_for_ready,
|
|
95
|
+
timeout,
|
|
96
|
+
metadata,
|
|
97
|
+
_registered_method=True)
|
|
@@ -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)}'
|
garf_executors/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
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
|
|
16
|
+
import warnings
|
|
19
17
|
|
|
20
|
-
from
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
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.1.4'
|
|
20
|
+
warnings.warn(
|
|
21
|
+
"The 'garf_executors' namespace is deprecated. "
|
|
22
|
+
"Please use 'garf.executors' instead.",
|
|
23
|
+
DeprecationWarning,
|
|
24
|
+
stacklevel=2,
|
|
25
|
+
)
|
garf_executors/api_executor.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
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,105 +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
|
-
|
|
16
|
+
import warnings
|
|
22
17
|
|
|
23
|
-
import
|
|
24
|
-
import logging
|
|
18
|
+
from garf.executors.api_executor import *
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
logger = logging.getLogger(__name__)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ApiExecutionContext(execution_context.ExecutionContext):
|
|
36
|
-
"""Common context for executing one or more queries."""
|
|
37
|
-
|
|
38
|
-
writer: str = 'console'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ApiQueryExecutor(executor.Executor):
|
|
42
|
-
"""Gets data from API and writes them to local/remote storage.
|
|
43
|
-
|
|
44
|
-
Attributes:
|
|
45
|
-
api_client: a client used for connecting to API.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
def __init__(self, fetcher: report_fetcher.ApiReportFetcher) -> None:
|
|
49
|
-
"""Initializes ApiQueryExecutor.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
fetcher: Instantiated report fetcher.
|
|
53
|
-
"""
|
|
54
|
-
self.fetcher = fetcher
|
|
55
|
-
|
|
56
|
-
@classmethod
|
|
57
|
-
def from_fetcher_alias(
|
|
58
|
-
cls, source: str, fetcher_parameters: dict[str, str] | None = None
|
|
59
|
-
) -> ApiQueryExecutor:
|
|
60
|
-
if not fetcher_parameters:
|
|
61
|
-
fetcher_parameters = {}
|
|
62
|
-
concrete_api_fetcher = fetchers.get_report_fetcher(source)
|
|
63
|
-
return ApiQueryExecutor(concrete_api_fetcher(**fetcher_parameters))
|
|
64
|
-
|
|
65
|
-
@tracer.start_as_current_span('api.execute')
|
|
66
|
-
def execute(
|
|
67
|
-
self,
|
|
68
|
-
query: str,
|
|
69
|
-
title: str,
|
|
70
|
-
context: ApiExecutionContext,
|
|
71
|
-
) -> str:
|
|
72
|
-
"""Reads query, extract results and stores them in a specified location.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
query: Location of the query.
|
|
76
|
-
title: Name of the query.
|
|
77
|
-
context: Query execution context.
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
Result of writing the report.
|
|
81
|
-
|
|
82
|
-
Raises:
|
|
83
|
-
GarfExecutorError: When failed to execute query.
|
|
84
|
-
"""
|
|
85
|
-
span = trace.get_current_span()
|
|
86
|
-
span.set_attribute('fetcher', self.fetcher.__class__.__name__)
|
|
87
|
-
span.set_attribute('api_client', self.fetcher.api_client.__class__.__name__)
|
|
88
|
-
try:
|
|
89
|
-
span.set_attribute('query_title', title)
|
|
90
|
-
span.set_attribute('query_text', query)
|
|
91
|
-
logger.debug('starting query %s', query)
|
|
92
|
-
results = self.fetcher.fetch(
|
|
93
|
-
query_specification=query,
|
|
94
|
-
args=context.query_parameters,
|
|
95
|
-
**context.fetcher_parameters,
|
|
96
|
-
)
|
|
97
|
-
writer_client = context.writer_client
|
|
98
|
-
logger.debug(
|
|
99
|
-
'Start writing data for query %s via %s writer',
|
|
100
|
-
title,
|
|
101
|
-
type(writer_client),
|
|
102
|
-
)
|
|
103
|
-
result = writer_client.write(results, title)
|
|
104
|
-
logger.debug(
|
|
105
|
-
'Finish writing data for query %s via %s writer',
|
|
106
|
-
title,
|
|
107
|
-
type(writer_client),
|
|
108
|
-
)
|
|
109
|
-
logger.info('%s executed successfully', title)
|
|
110
|
-
return result
|
|
111
|
-
except Exception as e:
|
|
112
|
-
logger.error('%s generated an exception: %s', title, str(e))
|
|
113
|
-
raise exceptions.GarfExecutorError(
|
|
114
|
-
'%s generated an exception: %s', title, str(e)
|
|
115
|
-
) from e
|
|
20
|
+
warnings.warn(
|
|
21
|
+
"The 'garf_executors' namespace is deprecated. "
|
|
22
|
+
"Please use 'garf.executors' instead.",
|
|
23
|
+
DeprecationWarning,
|
|
24
|
+
stacklevel=2,
|
|
25
|
+
)
|