garf-executors 1.0.2__py3-none-any.whl → 1.2.0__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 +1 -36
- garf/executors/api_executor.py +91 -7
- garf/executors/bq_executor.py +53 -25
- garf/executors/entrypoints/cli.py +48 -58
- garf/executors/entrypoints/grpc_server.py +21 -7
- garf/executors/entrypoints/server.py +64 -7
- garf/executors/entrypoints/tracer.py +29 -4
- garf/executors/execution_context.py +12 -28
- garf/executors/fetchers.py +52 -2
- garf/executors/garf_pb2.py +17 -11
- garf/executors/garf_pb2_grpc.py +45 -2
- garf/executors/query_processor.py +36 -18
- garf/executors/setup.py +76 -0
- garf/executors/sql_executor.py +22 -9
- garf/executors/workflows/__init__.py +0 -0
- garf/executors/workflows/gcp_workflow.yaml +49 -0
- garf/executors/{workflow.py → workflows/workflow.py} +60 -3
- garf/executors/workflows/workflow_runner.py +176 -0
- {garf_executors-1.0.2.dist-info → garf_executors-1.2.0.dist-info}/METADATA +8 -1
- {garf_executors-1.0.2.dist-info → garf_executors-1.2.0.dist-info}/RECORD +23 -19
- {garf_executors-1.0.2.dist-info → garf_executors-1.2.0.dist-info}/WHEEL +1 -1
- {garf_executors-1.0.2.dist-info → garf_executors-1.2.0.dist-info}/entry_points.txt +0 -0
- {garf_executors-1.0.2.dist-info → garf_executors-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -14,15 +14,20 @@
|
|
|
14
14
|
|
|
15
15
|
import os
|
|
16
16
|
|
|
17
|
-
from opentelemetry import trace
|
|
17
|
+
from opentelemetry import metrics, trace
|
|
18
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
|
|
19
|
+
OTLPMetricExporter,
|
|
20
|
+
)
|
|
18
21
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
19
22
|
OTLPSpanExporter,
|
|
20
23
|
)
|
|
24
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
25
|
+
from opentelemetry.sdk.metrics.export import (
|
|
26
|
+
PeriodicExportingMetricReader,
|
|
27
|
+
)
|
|
21
28
|
from opentelemetry.sdk.resources import Resource
|
|
22
29
|
from opentelemetry.sdk.trace import TracerProvider
|
|
23
|
-
from opentelemetry.sdk.trace.export import
|
|
24
|
-
BatchSpanProcessor,
|
|
25
|
-
)
|
|
30
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
26
31
|
|
|
27
32
|
DEFAULT_SERVICE_NAME = 'garf'
|
|
28
33
|
|
|
@@ -55,3 +60,23 @@ def initialize_tracer():
|
|
|
55
60
|
tracer_provider.add_span_processor(otlp_processor)
|
|
56
61
|
|
|
57
62
|
trace.set_tracer_provider(tracer_provider)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def initialize_meter():
|
|
66
|
+
resource = Resource.create(
|
|
67
|
+
{'service.name': os.getenv('OTLP_SERVICE_NAME', DEFAULT_SERVICE_NAME)}
|
|
68
|
+
)
|
|
69
|
+
meter_provider = MeterProvider(resource=resource)
|
|
70
|
+
|
|
71
|
+
if otel_endpoint := os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT'):
|
|
72
|
+
otlp_metric_exporter = OTLPMetricExporter(
|
|
73
|
+
endpoint=f'{otel_endpoint}/v1/metrics'
|
|
74
|
+
)
|
|
75
|
+
metric_reader = PeriodicExportingMetricReader(otlp_metric_exporter)
|
|
76
|
+
meter_provider = MeterProvider(
|
|
77
|
+
resource=resource, metric_readers=[metric_reader]
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
meter_provider = MeterProvider(resource=resource)
|
|
81
|
+
metrics.set_meter_provider(meter_provider)
|
|
82
|
+
return meter_provider
|
|
@@ -36,8 +36,8 @@ class ExecutionContext(pydantic.BaseModel):
|
|
|
36
36
|
Attributes:
|
|
37
37
|
query_parameters: Parameters to dynamically change query text.
|
|
38
38
|
fetcher_parameters: Parameters to specify fetching setup.
|
|
39
|
-
writer: Type of writer to use.
|
|
40
|
-
writer_parameters: Optional parameters to setup writer.
|
|
39
|
+
writer: Type of writer(s) to use.
|
|
40
|
+
writer_parameters: Optional parameters to setup writer(s).
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
43
|
query_parameters: query_editor.GarfQueryParameters | None = pydantic.Field(
|
|
@@ -77,41 +77,25 @@ class ExecutionContext(pydantic.BaseModel):
|
|
|
77
77
|
@property
|
|
78
78
|
def writer_client(self) -> abs_writer.AbsWriter:
|
|
79
79
|
"""Returns single writer client."""
|
|
80
|
+
if not self.writer:
|
|
81
|
+
raise writer.GarfIoWriterError('No available writer')
|
|
80
82
|
if isinstance(self.writer, list) and len(self.writer) > 0:
|
|
81
83
|
writer_type = self.writer[0]
|
|
82
84
|
else:
|
|
83
85
|
writer_type = self.writer
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
86
|
+
writer_clients = writer.setup_writers(
|
|
87
|
+
writers=[writer_type], writer_parameters=self.writer_parameters
|
|
88
|
+
)
|
|
89
|
+
return writer_clients[0]
|
|
96
90
|
|
|
97
91
|
@property
|
|
98
92
|
def writer_clients(self) -> list[abs_writer.AbsWriter]:
|
|
99
93
|
"""Returns list of writer clients."""
|
|
100
94
|
if not self.writer:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# Convert single writer to list for uniform processing
|
|
95
|
+
raise writer.GarfIoWriterError('No available writer')
|
|
104
96
|
writers_to_use = (
|
|
105
97
|
self.writer if isinstance(self.writer, list) else [self.writer]
|
|
106
98
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
99
|
+
return writer.setup_writers(
|
|
100
|
+
writers=writers_to_use, writer_parameters=self.writer_parameters
|
|
101
|
+
)
|
garf/executors/fetchers.py
CHANGED
|
@@ -17,7 +17,7 @@ import logging
|
|
|
17
17
|
import sys
|
|
18
18
|
from importlib.metadata import entry_points
|
|
19
19
|
|
|
20
|
-
from garf.core import report_fetcher
|
|
20
|
+
from garf.core import report_fetcher, simulator
|
|
21
21
|
from garf.executors.telemetry import tracer
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(name='garf.executors.fetchers')
|
|
@@ -31,6 +31,14 @@ def find_fetchers() -> set[str]:
|
|
|
31
31
|
return set()
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
@tracer.start_as_current_span('find_simulators')
|
|
35
|
+
def find_simulators() -> set[str]:
|
|
36
|
+
"""Identifiers all available report simulators."""
|
|
37
|
+
if entrypoints := _get_entrypoints('garf_simulator'):
|
|
38
|
+
return {simulator.name for simulator in entrypoints}
|
|
39
|
+
return set()
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
@tracer.start_as_current_span('get_report_fetcher')
|
|
35
43
|
def get_report_fetcher(source: str) -> type[report_fetcher.ApiReportFetcher]:
|
|
36
44
|
"""Loads report fetcher for a given source.
|
|
@@ -57,7 +65,10 @@ def get_report_fetcher(source: str) -> type[report_fetcher.ApiReportFetcher]:
|
|
|
57
65
|
if inspect.isclass(obj) and issubclass(
|
|
58
66
|
obj, report_fetcher.ApiReportFetcher
|
|
59
67
|
):
|
|
60
|
-
|
|
68
|
+
if not hasattr(obj, 'alias'):
|
|
69
|
+
return getattr(fetcher_module, name)
|
|
70
|
+
if obj.alias == fetcher.name:
|
|
71
|
+
return getattr(fetcher_module, name)
|
|
61
72
|
except ModuleNotFoundError as e:
|
|
62
73
|
raise report_fetcher.ApiReportFetcherError(
|
|
63
74
|
f'Failed to load fetcher for source {source}, reason: {e}'
|
|
@@ -67,6 +78,45 @@ def get_report_fetcher(source: str) -> type[report_fetcher.ApiReportFetcher]:
|
|
|
67
78
|
)
|
|
68
79
|
|
|
69
80
|
|
|
81
|
+
@tracer.start_as_current_span('get_report_simulator')
|
|
82
|
+
def get_report_simulator(source: str) -> type[simulator.ApiReportSimulator]:
|
|
83
|
+
"""Loads report simulator for a given source.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
source: Alias for a source associated with a simulator.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Class for a found report simulator.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
GarfApiReportSimulatorError: When simulator cannot be loaded.
|
|
93
|
+
MissingApiReportSimulatorError: When simulator not found.
|
|
94
|
+
"""
|
|
95
|
+
if source not in find_simulators():
|
|
96
|
+
raise simulator.MissingApiReportSimulatorError(source)
|
|
97
|
+
for sim in _get_entrypoints('garf_simulator'):
|
|
98
|
+
if sim.name == source:
|
|
99
|
+
try:
|
|
100
|
+
with tracer.start_as_current_span('load_simulator_module') as span:
|
|
101
|
+
simulator_module = sim.load()
|
|
102
|
+
span.set_attribute('loaded_module', simulator_module.__name__)
|
|
103
|
+
for name, obj in inspect.getmembers(simulator_module):
|
|
104
|
+
if inspect.isclass(obj) and issubclass(
|
|
105
|
+
obj, simulator.ApiReportSimulator
|
|
106
|
+
):
|
|
107
|
+
if not hasattr(obj, 'alias'):
|
|
108
|
+
return getattr(simulator_module, name)
|
|
109
|
+
if obj.alias == sim.name:
|
|
110
|
+
return getattr(simulator_module, name)
|
|
111
|
+
except ModuleNotFoundError as e:
|
|
112
|
+
raise simulator.GarfApiReportSimulatorError(
|
|
113
|
+
f'Failed to load simulator for source {source}, reason: {e}'
|
|
114
|
+
)
|
|
115
|
+
raise simulator.GarfApiReportSimulatorError(
|
|
116
|
+
f'No simulator available for the source "{source}"'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
70
120
|
def _get_entrypoints(group='garf'):
|
|
71
121
|
if sys.version_info.major == 3 and sys.version_info.minor == 9:
|
|
72
122
|
try:
|
garf/executors/garf_pb2.py
CHANGED
|
@@ -25,21 +25,27 @@ _sym_db = _symbol_database.Default()
|
|
|
25
25
|
from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ngarf.proto\x12\x04garf\x1a\x1cgoogle/protobuf/struct.proto\"g\n\x0e\x45xecuteRequest\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\r\n\x05query\x18\x03 \x01(\t\x12\'\n\x07\x63ontext\x18\x04 \x01(\x0b\x32\x16.garf.ExecutionContext\"\xbc\x01\n\x10\x45xecutionContext\x12/\n\x10query_parameters\x18\x01 \x01(\x0b\x32\x15.garf.QueryParameters\x12\x33\n\x12\x66\x65tcher_parameters\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0e\n\x06writer\x18\x03 \x01(\t\x12\x32\n\x11writer_parameters\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"d\n\x0fQueryParameters\x12&\n\x05macro\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08template\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\"\n\x0f\x45xecuteResponse\x12\x0f\n\x07results\x18\x01 \x03(\
|
|
28
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ngarf.proto\x12\x04garf\x1a\x1cgoogle/protobuf/struct.proto\"a\n\x0c\x46\x65tchRequest\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\r\n\x05query\x18\x03 \x01(\t\x12#\n\x07\x63ontext\x18\x04 \x01(\x0b\x32\x12.garf.FetchContext\"G\n\rFetchResponse\x12\x0f\n\x07\x63olumns\x18\x01 \x03(\t\x12%\n\x04rows\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\"t\n\x0c\x46\x65tchContext\x12/\n\x10query_parameters\x18\x01 \x01(\x0b\x32\x15.garf.QueryParameters\x12\x33\n\x12\x66\x65tcher_parameters\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"g\n\x0e\x45xecuteRequest\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\r\n\x05query\x18\x03 \x01(\t\x12\'\n\x07\x63ontext\x18\x04 \x01(\x0b\x32\x16.garf.ExecutionContext\"\xbc\x01\n\x10\x45xecutionContext\x12/\n\x10query_parameters\x18\x01 \x01(\x0b\x32\x15.garf.QueryParameters\x12\x33\n\x12\x66\x65tcher_parameters\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0e\n\x06writer\x18\x03 \x01(\t\x12\x32\n\x11writer_parameters\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"d\n\x0fQueryParameters\x12&\n\x05macro\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08template\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\"\n\x0f\x45xecuteResponse\x12\x0f\n\x07results\x18\x01 \x03(\t2{\n\x0bGarfService\x12\x38\n\x07\x45xecute\x12\x14.garf.ExecuteRequest\x1a\x15.garf.ExecuteResponse\"\x00\x12\x32\n\x05\x46\x65tch\x12\x12.garf.FetchRequest\x1a\x13.garf.FetchResponse\"\x00\x62\x06proto3')
|
|
29
29
|
|
|
30
30
|
_globals = globals()
|
|
31
31
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
32
32
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'garf_pb2', _globals)
|
|
33
33
|
if not _descriptor._USE_C_DESCRIPTORS:
|
|
34
34
|
DESCRIPTOR._loaded_options = None
|
|
35
|
-
_globals['
|
|
36
|
-
_globals['
|
|
37
|
-
_globals['
|
|
38
|
-
_globals['
|
|
39
|
-
_globals['
|
|
40
|
-
_globals['
|
|
41
|
-
_globals['
|
|
42
|
-
_globals['
|
|
43
|
-
_globals['
|
|
44
|
-
_globals['
|
|
35
|
+
_globals['_FETCHREQUEST']._serialized_start=50
|
|
36
|
+
_globals['_FETCHREQUEST']._serialized_end=147
|
|
37
|
+
_globals['_FETCHRESPONSE']._serialized_start=149
|
|
38
|
+
_globals['_FETCHRESPONSE']._serialized_end=220
|
|
39
|
+
_globals['_FETCHCONTEXT']._serialized_start=222
|
|
40
|
+
_globals['_FETCHCONTEXT']._serialized_end=338
|
|
41
|
+
_globals['_EXECUTEREQUEST']._serialized_start=340
|
|
42
|
+
_globals['_EXECUTEREQUEST']._serialized_end=443
|
|
43
|
+
_globals['_EXECUTIONCONTEXT']._serialized_start=446
|
|
44
|
+
_globals['_EXECUTIONCONTEXT']._serialized_end=634
|
|
45
|
+
_globals['_QUERYPARAMETERS']._serialized_start=636
|
|
46
|
+
_globals['_QUERYPARAMETERS']._serialized_end=736
|
|
47
|
+
_globals['_EXECUTERESPONSE']._serialized_start=738
|
|
48
|
+
_globals['_EXECUTERESPONSE']._serialized_end=772
|
|
49
|
+
_globals['_GARFSERVICE']._serialized_start=774
|
|
50
|
+
_globals['_GARFSERVICE']._serialized_end=897
|
|
45
51
|
# @@protoc_insertion_point(module_scope)
|
garf/executors/garf_pb2_grpc.py
CHANGED
|
@@ -5,7 +5,7 @@ import warnings
|
|
|
5
5
|
|
|
6
6
|
from . import garf_pb2 as garf__pb2
|
|
7
7
|
|
|
8
|
-
GRPC_GENERATED_VERSION = '1.
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.76.0'
|
|
9
9
|
GRPC_VERSION = grpc.__version__
|
|
10
10
|
_version_not_supported = False
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ except ImportError:
|
|
|
18
18
|
if _version_not_supported:
|
|
19
19
|
raise RuntimeError(
|
|
20
20
|
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
-
+
|
|
21
|
+
+ ' but the generated code in garf_pb2_grpc.py depends on'
|
|
22
22
|
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
23
|
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
24
|
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
@@ -39,6 +39,11 @@ class GarfServiceStub(object):
|
|
|
39
39
|
request_serializer=garf__pb2.ExecuteRequest.SerializeToString,
|
|
40
40
|
response_deserializer=garf__pb2.ExecuteResponse.FromString,
|
|
41
41
|
_registered_method=True)
|
|
42
|
+
self.Fetch = channel.unary_unary(
|
|
43
|
+
'/garf.GarfService/Fetch',
|
|
44
|
+
request_serializer=garf__pb2.FetchRequest.SerializeToString,
|
|
45
|
+
response_deserializer=garf__pb2.FetchResponse.FromString,
|
|
46
|
+
_registered_method=True)
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
class GarfServiceServicer(object):
|
|
@@ -50,6 +55,12 @@ class GarfServiceServicer(object):
|
|
|
50
55
|
context.set_details('Method not implemented!')
|
|
51
56
|
raise NotImplementedError('Method not implemented!')
|
|
52
57
|
|
|
58
|
+
def Fetch(self, request, context):
|
|
59
|
+
"""Missing associated documentation comment in .proto file."""
|
|
60
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
61
|
+
context.set_details('Method not implemented!')
|
|
62
|
+
raise NotImplementedError('Method not implemented!')
|
|
63
|
+
|
|
53
64
|
|
|
54
65
|
def add_GarfServiceServicer_to_server(servicer, server):
|
|
55
66
|
rpc_method_handlers = {
|
|
@@ -58,6 +69,11 @@ def add_GarfServiceServicer_to_server(servicer, server):
|
|
|
58
69
|
request_deserializer=garf__pb2.ExecuteRequest.FromString,
|
|
59
70
|
response_serializer=garf__pb2.ExecuteResponse.SerializeToString,
|
|
60
71
|
),
|
|
72
|
+
'Fetch': grpc.unary_unary_rpc_method_handler(
|
|
73
|
+
servicer.Fetch,
|
|
74
|
+
request_deserializer=garf__pb2.FetchRequest.FromString,
|
|
75
|
+
response_serializer=garf__pb2.FetchResponse.SerializeToString,
|
|
76
|
+
),
|
|
61
77
|
}
|
|
62
78
|
generic_handler = grpc.method_handlers_generic_handler(
|
|
63
79
|
'garf.GarfService', rpc_method_handlers)
|
|
@@ -95,3 +111,30 @@ class GarfService(object):
|
|
|
95
111
|
timeout,
|
|
96
112
|
metadata,
|
|
97
113
|
_registered_method=True)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def Fetch(request,
|
|
117
|
+
target,
|
|
118
|
+
options=(),
|
|
119
|
+
channel_credentials=None,
|
|
120
|
+
call_credentials=None,
|
|
121
|
+
insecure=False,
|
|
122
|
+
compression=None,
|
|
123
|
+
wait_for_ready=None,
|
|
124
|
+
timeout=None,
|
|
125
|
+
metadata=None):
|
|
126
|
+
return grpc.experimental.unary_unary(
|
|
127
|
+
request,
|
|
128
|
+
target,
|
|
129
|
+
'/garf.GarfService/Fetch',
|
|
130
|
+
garf__pb2.FetchRequest.SerializeToString,
|
|
131
|
+
garf__pb2.FetchResponse.FromString,
|
|
132
|
+
options,
|
|
133
|
+
channel_credentials,
|
|
134
|
+
insecure,
|
|
135
|
+
call_credentials,
|
|
136
|
+
compression,
|
|
137
|
+
wait_for_ready,
|
|
138
|
+
timeout,
|
|
139
|
+
metadata,
|
|
140
|
+
_registered_method=True)
|
|
@@ -12,29 +12,39 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
"""qQuery can be used as a parameter in garf queries."""
|
|
16
|
+
|
|
15
17
|
import contextlib
|
|
16
18
|
|
|
17
|
-
from garf.core import query_editor
|
|
18
|
-
from garf.executors import
|
|
19
|
+
from garf.core import query_editor, query_parser
|
|
20
|
+
from garf.executors import execution_context
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
class GqueryError(query_parser.GarfQueryError):
|
|
24
|
+
"""Errors on incorrect qQuery syntax."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _handle_sub_context(context, sub_context):
|
|
28
|
+
for k, v in sub_context.items():
|
|
25
29
|
if isinstance(v, str) and v.startswith('gquery'):
|
|
26
30
|
no_writer_context = context.model_copy(update={'writer': None})
|
|
27
31
|
try:
|
|
28
|
-
_, alias, query = v.split(':', maxsplit=3)
|
|
32
|
+
_, alias, *query = v.split(':', maxsplit=3)
|
|
29
33
|
except ValueError:
|
|
30
|
-
raise
|
|
34
|
+
raise GqueryError(
|
|
31
35
|
f'Incorrect gquery format, should be gquery:alias:query, got {v}'
|
|
32
36
|
)
|
|
37
|
+
if not alias:
|
|
38
|
+
raise GqueryError(f'Missing alias in gquery: {v}')
|
|
39
|
+
if not query:
|
|
40
|
+
raise GqueryError(f'Missing query text in gquery: {v}')
|
|
33
41
|
if alias == 'sqldb':
|
|
34
42
|
from garf.executors import sql_executor
|
|
35
43
|
|
|
36
|
-
gquery_executor =
|
|
37
|
-
|
|
44
|
+
gquery_executor = (
|
|
45
|
+
sql_executor.SqlAlchemyQueryExecutor.from_connection_string(
|
|
46
|
+
context.fetcher_parameters.get('connection_string')
|
|
47
|
+
)
|
|
38
48
|
)
|
|
39
49
|
elif alias == 'bq':
|
|
40
50
|
from garf.executors import bq_executor
|
|
@@ -43,19 +53,27 @@ def process_gquery(
|
|
|
43
53
|
**context.fetcher_parameters
|
|
44
54
|
)
|
|
45
55
|
else:
|
|
46
|
-
raise
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
raise GqueryError(f'Unsupported alias {alias} for gquery: {v}')
|
|
57
|
+
with contextlib.suppress(
|
|
58
|
+
query_editor.GarfResourceError, query_parser.GarfVirtualColumnError
|
|
59
|
+
):
|
|
60
|
+
query = ':'.join(query)
|
|
50
61
|
query_spec = query_editor.QuerySpecification(
|
|
51
62
|
text=query, args=context.query_parameters
|
|
52
63
|
).generate()
|
|
53
64
|
if len(columns := [c for c in query_spec.column_names if c != '_']) > 1:
|
|
54
|
-
raise
|
|
55
|
-
f'Multiple columns in gquery: {columns}'
|
|
56
|
-
)
|
|
65
|
+
raise GqueryError(f'Multiple columns in gquery definition: {columns}')
|
|
57
66
|
res = gquery_executor.execute(
|
|
58
67
|
query=query, title='gquery', context=no_writer_context
|
|
59
68
|
)
|
|
60
|
-
|
|
69
|
+
if len(columns := [c for c in res.column_names if c != '_']) > 1:
|
|
70
|
+
raise GqueryError(f'Multiple columns in gquery result: {columns}')
|
|
71
|
+
sub_context[k] = res.to_list(row_type='scalar')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def process_gquery(
|
|
75
|
+
context: execution_context.ExecutionContext,
|
|
76
|
+
) -> execution_context.ExecutionContext:
|
|
77
|
+
_handle_sub_context(context, context.fetcher_parameters)
|
|
78
|
+
_handle_sub_context(context, context.query_parameters.macro)
|
|
61
79
|
return context
|
garf/executors/setup.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
"""Bootstraps executor based on provided parameters."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from garf.executors import executor, fetchers
|
|
23
|
+
from garf.executors.api_executor import ApiQueryExecutor
|
|
24
|
+
from garf.executors.telemetry import tracer
|
|
25
|
+
from garf.io import writer
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger('garf.executors.setup')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@tracer.start_as_current_span('setup_executor')
|
|
31
|
+
def setup_executor(
|
|
32
|
+
source: str,
|
|
33
|
+
fetcher_parameters: dict[str, str | int | bool],
|
|
34
|
+
enable_cache: bool = False,
|
|
35
|
+
cache_ttl_seconds: int = 3600,
|
|
36
|
+
simulate: bool = False,
|
|
37
|
+
writers: str | list[str] | None = None,
|
|
38
|
+
writer_parameters: dict[str, Any] | None = None,
|
|
39
|
+
) -> type[executor.Executor]:
|
|
40
|
+
"""Initializes executors based on a source and parameters."""
|
|
41
|
+
if simulate and enable_cache:
|
|
42
|
+
logger.warning('Simulating API responses. Disabling cache.')
|
|
43
|
+
enable_cache = False
|
|
44
|
+
if writers:
|
|
45
|
+
writer_clients = writer.setup_writers(writers, writer_parameters)
|
|
46
|
+
else:
|
|
47
|
+
writer_clients = None
|
|
48
|
+
if source == 'bq':
|
|
49
|
+
bq_executor = importlib.import_module('garf.executors.bq_executor')
|
|
50
|
+
query_executor = bq_executor.BigQueryExecutor(
|
|
51
|
+
**fetcher_parameters, writers=writer_clients
|
|
52
|
+
)
|
|
53
|
+
elif source == 'sqldb':
|
|
54
|
+
sql_executor = importlib.import_module('garf.executors.sql_executor')
|
|
55
|
+
query_executor = (
|
|
56
|
+
sql_executor.SqlAlchemyQueryExecutor.from_connection_string(
|
|
57
|
+
connection_string=fetcher_parameters.get('connection_string'),
|
|
58
|
+
writers=writer_clients,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
concrete_api_fetcher = fetchers.get_report_fetcher(source)
|
|
63
|
+
if simulate:
|
|
64
|
+
concrete_simulator = fetchers.get_report_simulator(source)()
|
|
65
|
+
else:
|
|
66
|
+
concrete_simulator = None
|
|
67
|
+
query_executor = ApiQueryExecutor(
|
|
68
|
+
fetcher=concrete_api_fetcher(
|
|
69
|
+
**fetcher_parameters,
|
|
70
|
+
enable_cache=enable_cache,
|
|
71
|
+
cache_ttl_seconds=cache_ttl_seconds,
|
|
72
|
+
),
|
|
73
|
+
report_simulator=concrete_simulator,
|
|
74
|
+
writers=writer_clients,
|
|
75
|
+
)
|
|
76
|
+
return query_executor
|
garf/executors/sql_executor.py
CHANGED
|
@@ -31,6 +31,7 @@ import pandas as pd
|
|
|
31
31
|
from garf.core import query_editor, report
|
|
32
32
|
from garf.executors import exceptions, execution_context, executor
|
|
33
33
|
from garf.executors.telemetry import tracer
|
|
34
|
+
from garf.io.writers import abs_writer
|
|
34
35
|
from opentelemetry import trace
|
|
35
36
|
|
|
36
37
|
logger = logging.getLogger(__name__)
|
|
@@ -40,9 +41,7 @@ class SqlAlchemyQueryExecutorError(exceptions.GarfExecutorError):
|
|
|
40
41
|
"""Error when SqlAlchemyQueryExecutor fails to run query."""
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
class SqlAlchemyQueryExecutor(
|
|
44
|
-
executor.Executor, query_editor.TemplateProcessorMixin
|
|
45
|
-
):
|
|
44
|
+
class SqlAlchemyQueryExecutor(executor.Executor):
|
|
46
45
|
"""Handles query execution via SqlAlchemy.
|
|
47
46
|
|
|
48
47
|
Attributes:
|
|
@@ -50,7 +49,10 @@ class SqlAlchemyQueryExecutor(
|
|
|
50
49
|
"""
|
|
51
50
|
|
|
52
51
|
def __init__(
|
|
53
|
-
self,
|
|
52
|
+
self,
|
|
53
|
+
engine: sqlalchemy.engine.base.Engine | None = None,
|
|
54
|
+
writers: list[abs_writer.AbsWriter] | None = None,
|
|
55
|
+
**kwargs: str,
|
|
54
56
|
) -> None:
|
|
55
57
|
"""Initializes executor with a given engine.
|
|
56
58
|
|
|
@@ -58,18 +60,19 @@ class SqlAlchemyQueryExecutor(
|
|
|
58
60
|
engine: Initialized Engine object to operated on a given database.
|
|
59
61
|
"""
|
|
60
62
|
self.engine = engine or sqlalchemy.create_engine('sqlite://')
|
|
63
|
+
self.writers = writers
|
|
61
64
|
super().__init__()
|
|
62
65
|
|
|
63
66
|
@classmethod
|
|
64
67
|
def from_connection_string(
|
|
65
|
-
cls, connection_string: str | None
|
|
68
|
+
cls, connection_string: str | None, writers: list[str] | None = None
|
|
66
69
|
) -> SqlAlchemyQueryExecutor:
|
|
67
70
|
"""Creates executor from SqlAlchemy connection string.
|
|
68
71
|
|
|
69
72
|
https://docs.sqlalchemy.org/en/20/core/engines.html
|
|
70
73
|
"""
|
|
71
74
|
engine = sqlalchemy.create_engine(connection_string or 'sqlite://')
|
|
72
|
-
return cls(engine)
|
|
75
|
+
return cls(engine=engine, writers=writers)
|
|
73
76
|
|
|
74
77
|
@tracer.start_as_current_span('sql.execute')
|
|
75
78
|
def execute(
|
|
@@ -91,8 +94,18 @@ class SqlAlchemyQueryExecutor(
|
|
|
91
94
|
Report with data if query returns some data otherwise empty Report.
|
|
92
95
|
"""
|
|
93
96
|
span = trace.get_current_span()
|
|
97
|
+
query_spec = (
|
|
98
|
+
query_editor.QuerySpecification(
|
|
99
|
+
text=query, title=title, args=context.query_parameters
|
|
100
|
+
)
|
|
101
|
+
.remove_comments()
|
|
102
|
+
.expand()
|
|
103
|
+
)
|
|
104
|
+
query_text = query_spec.query.text
|
|
105
|
+
title = query_spec.query.title
|
|
106
|
+
span.set_attribute('query.title', title)
|
|
107
|
+
span.set_attribute('query.text', query_text)
|
|
94
108
|
logger.info('Executing script: %s', title)
|
|
95
|
-
query_text = self.replace_params_template(query, context.query_parameters)
|
|
96
109
|
with self.engine.begin() as conn:
|
|
97
110
|
if re.findall(r'(create|update) ', query_text.lower()):
|
|
98
111
|
try:
|
|
@@ -116,8 +129,8 @@ class SqlAlchemyQueryExecutor(
|
|
|
116
129
|
) from e
|
|
117
130
|
finally:
|
|
118
131
|
conn.connection.execute(f'DROP TABLE {temp_table_name}')
|
|
119
|
-
if context.writer
|
|
120
|
-
writer_clients = context.writer_clients
|
|
132
|
+
if results and (self.writers or context.writer):
|
|
133
|
+
writer_clients = self.writers or context.writer_clients
|
|
121
134
|
if not writer_clients:
|
|
122
135
|
logger.warning('No writers configured, skipping write operation')
|
|
123
136
|
else:
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
run:
|
|
2
|
+
for:
|
|
3
|
+
value: pair
|
|
4
|
+
in: ${pairs}
|
|
5
|
+
steps:
|
|
6
|
+
- log_source:
|
|
7
|
+
call: sys.log
|
|
8
|
+
args:
|
|
9
|
+
data: ${pair.alias}
|
|
10
|
+
- execute_queries:
|
|
11
|
+
parallel:
|
|
12
|
+
for:
|
|
13
|
+
value: query
|
|
14
|
+
in: ${pair.queries}
|
|
15
|
+
steps:
|
|
16
|
+
- log_query:
|
|
17
|
+
call: sys.log
|
|
18
|
+
args:
|
|
19
|
+
data: ${pair}
|
|
20
|
+
- execute_single_query:
|
|
21
|
+
try:
|
|
22
|
+
call: http.post
|
|
23
|
+
args:
|
|
24
|
+
url: ${sys.get_env("GARF_ENDPOINT") + "/api/execute"}
|
|
25
|
+
auth:
|
|
26
|
+
type: OIDC
|
|
27
|
+
body:
|
|
28
|
+
source: ${pair.fetcher}
|
|
29
|
+
# query_path: ${query.path}
|
|
30
|
+
title: ${query.query.title}
|
|
31
|
+
query: ${query.query.text}
|
|
32
|
+
context:
|
|
33
|
+
fetcher_parameters: ${pair.fetcher_parameters}
|
|
34
|
+
writer: ${pair.writer}
|
|
35
|
+
writer_parameters: ${pair.writer_parameters}
|
|
36
|
+
query_parameters:
|
|
37
|
+
macro: ${pair.query_parameters.macro}
|
|
38
|
+
template: ${pair.query_parameters.template}
|
|
39
|
+
result: task_resp
|
|
40
|
+
except:
|
|
41
|
+
as: e
|
|
42
|
+
assign:
|
|
43
|
+
- task_resp:
|
|
44
|
+
status: "failed"
|
|
45
|
+
error: ${e.message}
|
|
46
|
+
- log_result:
|
|
47
|
+
call: sys.log
|
|
48
|
+
args:
|
|
49
|
+
data: ${task_resp}
|