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.
@@ -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. Can be a single writer string or list of writers.
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
- 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
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
- return []
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
- 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
99
+ return writer.setup_writers(
100
+ writers=writers_to_use, writer_parameters=self.writer_parameters
101
+ )
@@ -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
- return getattr(fetcher_module, name)
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:
@@ -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(\t2G\n\x0bGarfService\x12\x38\n\x07\x45xecute\x12\x14.garf.ExecuteRequest\x1a\x15.garf.ExecuteResponse\"\x00\x62\x06proto3')
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['_EXECUTEREQUEST']._serialized_start=50
36
- _globals['_EXECUTEREQUEST']._serialized_end=153
37
- _globals['_EXECUTIONCONTEXT']._serialized_start=156
38
- _globals['_EXECUTIONCONTEXT']._serialized_end=344
39
- _globals['_QUERYPARAMETERS']._serialized_start=346
40
- _globals['_QUERYPARAMETERS']._serialized_end=446
41
- _globals['_EXECUTERESPONSE']._serialized_start=448
42
- _globals['_EXECUTERESPONSE']._serialized_end=482
43
- _globals['_GARFSERVICE']._serialized_start=484
44
- _globals['_GARFSERVICE']._serialized_end=555
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)
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from . import garf_pb2 as garf__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.75.0'
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
- + f' but the generated code in garf_pb2_grpc.py depends on'
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 exceptions, execution_context
19
+ from garf.core import query_editor, query_parser
20
+ from garf.executors import execution_context
19
21
 
20
22
 
21
- def process_gquery(
22
- context: execution_context.ExecutionContext,
23
- ) -> execution_context.ExecutionContext:
24
- for k, v in context.fetcher_parameters.items():
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 exceptions.GarfExecutorError(
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 = sql_executor.SqlAlchemyQueryExecutor(
37
- **context.fetcher_parameters
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 exceptions.GarfExecutorError(
47
- f'Unsupported alias for gquery: {alias}'
48
- )
49
- with contextlib.suppress(query_editor.GarfResourceError):
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 exceptions.GarfExecutorError(
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
- context.fetcher_parameters[k] = res.to_list(row_type='scalar')
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
@@ -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
@@ -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, engine: sqlalchemy.engine.base.Engine | None = None, **kwargs: str
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 and results:
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}