garf-executors 0.0.6__py3-none-any.whl → 0.1.4__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.
@@ -13,25 +13,63 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import inspect
16
+ import sys
16
17
  from importlib.metadata import entry_points
17
18
 
18
19
  from garf_core import report_fetcher
20
+ from opentelemetry import trace
19
21
 
22
+ from garf_executors.telemetry import tracer
20
23
 
21
- def get_report_fetchers() -> dict[str, report_fetcher.ApiReportFetcher]:
22
- fetchers = entry_points(group='garf')
23
- found_fetchers = {}
24
- for fetcher in fetchers:
24
+
25
+ @tracer.start_as_current_span('find_fetchers')
26
+ def find_fetchers() -> set[str]:
27
+ """Identifiers all available report fetchers."""
28
+ if entrypoints := _get_entrypoints('garf'):
29
+ return {fetcher.name for fetcher in entrypoints}
30
+ return set()
31
+
32
+
33
+ @tracer.start_as_current_span('get_report_fetcher')
34
+ def get_report_fetcher(source: str) -> type[report_fetcher.ApiReportFetcher]:
35
+ """Loads report fetcher for a given source.
36
+
37
+ Args:
38
+ source: Alias for a source associated with a fetcher.
39
+
40
+ Returns:
41
+ Class for a found report fetcher.
42
+
43
+ Raises:
44
+ ApiReportFetcherError: When fetcher cannot be loaded.
45
+ MissingApiReportFetcherError: When fetcher not found.
46
+ """
47
+ if source not in find_fetchers():
48
+ raise report_fetcher.MissingApiReportFetcherError(source)
49
+ for fetcher in _get_entrypoints('garf'):
50
+ if fetcher.name == source:
51
+ try:
52
+ with tracer.start_as_current_span('load_fetcher_module') as span:
53
+ fetcher_module = fetcher.load()
54
+ span.set_attribute('loaded_module', fetcher_module.__name__)
55
+ for name, obj in inspect.getmembers(fetcher_module):
56
+ if inspect.isclass(obj) and issubclass(
57
+ obj, report_fetcher.ApiReportFetcher
58
+ ):
59
+ return getattr(fetcher_module, name)
60
+ except ModuleNotFoundError:
61
+ continue
62
+ raise report_fetcher.ApiReportFetcherError(
63
+ f'No fetcher available for the source "{source}"'
64
+ )
65
+
66
+
67
+ def _get_entrypoints(group='garf'):
68
+ if sys.version_info.major == 3 and sys.version_info.minor == 9:
25
69
  try:
26
- fetcher_module = fetcher.load()
27
- for name, obj in inspect.getmembers(fetcher_module):
28
- if inspect.isclass(obj) and issubclass(
29
- obj, report_fetcher.ApiReportFetcher
30
- ):
31
- found_fetchers[fetcher.name] = getattr(fetcher_module, name)
32
- except ModuleNotFoundError:
33
- continue
34
- return found_fetchers
35
-
36
-
37
- FETCHERS = get_report_fetchers()
70
+ fetchers = entry_points()[group]
71
+ except KeyError:
72
+ fetchers = []
73
+ else:
74
+ fetchers = entry_points(group=group)
75
+ return fetchers
@@ -25,14 +25,24 @@ except ImportError as e:
25
25
 
26
26
  import logging
27
27
  import re
28
- from typing import Any
29
28
 
30
29
  import pandas as pd
30
+ from garf_core import query_editor, report
31
+ from opentelemetry import trace
31
32
 
32
- from garf_core import query_editor
33
+ from garf_executors import exceptions, execution_context, executor
34
+ from garf_executors.telemetry import tracer
33
35
 
36
+ logger = logging.getLogger(__name__)
34
37
 
35
- class SqlAlchemyQueryExecutor(query_editor.TemplateProcessorMixin):
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
+ ):
36
46
  """Handles query execution via SqlAlchemy.
37
47
 
38
48
  Attributes:
@@ -51,36 +61,63 @@ class SqlAlchemyQueryExecutor(query_editor.TemplateProcessorMixin):
51
61
  def from_connection_string(
52
62
  cls, connection_string: str
53
63
  ) -> SqlAlchemyQueryExecutor:
64
+ """Creates executor from SqlAlchemy connection string.
65
+
66
+ https://docs.sqlalchemy.org/en/20/core/engines.html
67
+ """
54
68
  engine = sqlalchemy.create_engine(connection_string)
55
69
  return cls(engine)
56
70
 
71
+ @tracer.start_as_current_span('sql.execute')
57
72
  def execute(
58
73
  self,
59
- script_name: str | None,
60
- query_text: str,
61
- params: dict[str, Any] | None = None,
62
- ) -> pd.DataFrame:
74
+ query: str,
75
+ title: str,
76
+ context: execution_context.ExecutionContext = (
77
+ execution_context.ExecutionContext()
78
+ ),
79
+ ) -> report.GarfReport:
63
80
  """Executes query in a given database via SqlAlchemy.
64
81
 
65
82
  Args:
66
- script_name: Script identifier.
67
- query_text: Query to be executed.
68
- params: Optional parameters to be replaced in query text.
83
+ query: Location of the query.
84
+ title: Name of the query.
85
+ context: Query execution context.
69
86
 
70
87
  Returns:
71
- DataFrame if query returns some data otherwise empty DataFrame.
88
+ Report with data if query returns some data otherwise empty Report.
72
89
  """
73
- logging.info('Executing script: %s', script_name)
74
- query_text = self.replace_params_template(query_text, params)
90
+ span = trace.get_current_span()
91
+ logging.info('Executing script: %s', title)
92
+ query_text = self.replace_params_template(query, context.query_parameters)
75
93
  with self.engine.begin() as conn:
76
94
  if re.findall(r'(create|update) ', query_text.lower()):
77
95
  conn.connection.executescript(query_text)
78
- return pd.DataFrame()
79
- temp_table_name = f'temp_{script_name}'.replace('.', '_')
80
- query_text = f'CREATE TABLE {temp_table_name} AS {query_text}'
81
- conn.connection.executescript(query_text)
82
- try:
83
- result = pd.read_sql(f'SELECT * FROM {temp_table_name}', conn)
84
- finally:
85
- conn.connection.execute(f'DROP TABLE {temp_table_name}')
86
- return result
96
+ results = report.GarfReport()
97
+ else:
98
+ temp_table_name = f'temp_{title}'.replace('.', '_')
99
+ query_text = f'CREATE TABLE {temp_table_name} AS {query_text}'
100
+ conn.connection.executescript(query_text)
101
+ try:
102
+ results = report.GarfReport.from_pandas(
103
+ pd.read_sql(f'SELECT * FROM {temp_table_name}', conn)
104
+ )
105
+ finally:
106
+ conn.connection.execute(f'DROP TABLE {temp_table_name}')
107
+ if context.writer and results:
108
+ writer_client = context.writer_client
109
+ logger.debug(
110
+ 'Start writing data for query %s via %s writer',
111
+ title,
112
+ type(writer_client),
113
+ )
114
+ writing_result = writer_client.write(results, title)
115
+ logger.debug(
116
+ 'Finish writing data for query %s via %s writer',
117
+ title,
118
+ type(writer_client),
119
+ )
120
+ logger.info('%s executed successfully', title)
121
+ return writing_result
122
+ span.set_attribute('execute.num_results', len(results))
123
+ 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
+ )
@@ -1,34 +1,39 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: garf-executors
3
- Version: 0.0.6
3
+ Version: 0.1.4
4
4
  Summary: Executes queries against API and writes data to local/remote storage.
5
5
  Author-email: "Google Inc. (gTech gPS CSE team)" <no-reply@google.com>, Andrei Markin <andrey.markin.ppc@gmail.com>
6
6
  License: Apache 2.0
7
7
  Classifier: Programming Language :: Python :: 3 :: Only
8
- Classifier: Programming Language :: Python :: 3.8
9
8
  Classifier: Programming Language :: Python :: 3.9
10
9
  Classifier: Programming Language :: Python :: 3.10
11
10
  Classifier: Programming Language :: Python :: 3.11
12
11
  Classifier: Programming Language :: Python :: 3.12
13
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: License :: OSI Approved :: Apache Software License
18
- Requires-Python: >=3.8
18
+ Requires-Python: >=3.9
19
19
  Description-Content-Type: text/markdown
20
20
  Requires-Dist: garf-core
21
21
  Requires-Dist: garf-io
22
22
  Requires-Dist: pyyaml
23
23
  Requires-Dist: pydantic
24
+ Requires-Dist: opentelemetry-api
25
+ Requires-Dist: opentelemetry-sdk
24
26
  Provides-Extra: bq
25
27
  Requires-Dist: garf-io[bq]; extra == "bq"
26
28
  Requires-Dist: pandas; extra == "bq"
29
+ Requires-Dist: google-cloud-logging; extra == "bq"
27
30
  Provides-Extra: sql
28
31
  Requires-Dist: garf-io[sqlalchemy]; extra == "sql"
29
32
  Requires-Dist: pandas; extra == "sql"
30
33
  Provides-Extra: server
31
34
  Requires-Dist: fastapi[standard]; extra == "server"
35
+ Requires-Dist: opentelemetry-instrumentation-fastapi; extra == "server"
36
+ Requires-Dist: opentelemetry-exporter-otlp; extra == "server"
32
37
  Provides-Extra: all
33
38
  Requires-Dist: garf-executors[bq,server,sql]; extra == "all"
34
39
 
@@ -0,0 +1,20 @@
1
+ garf_executors/__init__.py,sha256=4BZv9zb3tjlpF4kQSdTj1L5IdR-BiNQwtejg5dPTTcY,1933
2
+ garf_executors/api_executor.py,sha256=TxHtdnXjXjfBDU0z13yCulqF0XcEqAoOdVeGczdTSXs,3590
3
+ garf_executors/bq_executor.py,sha256=LOKNitigaMk4U-UjBZTHy4vG092nw6suEbgo2rrHCTI,5002
4
+ garf_executors/config.py,sha256=TqCzijm1PRvL4p-9Zl-kPkcC1SFKjhgTfKMJFmJW3fQ,1688
5
+ garf_executors/exceptions.py,sha256=U_7Q2ZMOUf89gzZd2pw7y3g7i1NeByPPKfpZ3q7p3ZU,662
6
+ garf_executors/execution_context.py,sha256=X4Wm_rE1mnnN2FuC_9bL05a8h8ko7qraeGY955ijNJc,2800
7
+ garf_executors/executor.py,sha256=_Nj6CKgyhzwFOxneODDhV1bvLjrMEvIu93W8YF9-sXo,2481
8
+ garf_executors/fetchers.py,sha256=HQqnMb0wlasVfXmAA7cnsd73POXPEGPxaC5mpEOnQk4,2443
9
+ garf_executors/sql_executor.py,sha256=_4oVPZKTd3lrDE0SM6uQ_bl13Ay9uhQuD-PHO9247WM,3920
10
+ garf_executors/telemetry.py,sha256=P75klGEoYgJ_-pR-izUIQ7B88ufskQ4vmW1rETg63Nc,747
11
+ garf_executors/entrypoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ garf_executors/entrypoints/cli.py,sha256=Qbg10LLWHEMBjjsOfEMDZQtjWpUwh6WJKSnqiXOzF6A,4765
13
+ garf_executors/entrypoints/server.py,sha256=b9blyBvN774RiTHUCZkfE5kNVnrTaANrETI4WMDHJeQ,3255
14
+ garf_executors/entrypoints/tracer.py,sha256=A_nolmGuMT3wOZJsoPORjfdtPO2lXdbr6CZt5BW0RTY,1374
15
+ garf_executors/entrypoints/utils.py,sha256=5XiGR2IOxdzAOY0lEWUeUV7tIpKBGRnQaIwBYvzQB7c,4337
16
+ garf_executors-0.1.4.dist-info/METADATA,sha256=3Z0plyqxqwCKYOm2PlXIfvxGo0lAVkdIaLD0s0pgZzQ,2900
17
+ garf_executors-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ garf_executors-0.1.4.dist-info/entry_points.txt,sha256=LskWNFIw8j0WJuI18-32OZrlASXAMg1XtrRYwsKBz2E,61
19
+ garf_executors-0.1.4.dist-info/top_level.txt,sha256=sP4dCXOENPn1hDFAunjMV8Js4NND_KGeO_gQWuaT0EY,15
20
+ garf_executors-0.1.4.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- garf_executors/__init__.py,sha256=bcb29OEvsx2XNTpbUW0LvKxoYJt5BSX3S2gqQLdRIqU,955
2
- garf_executors/api_executor.py,sha256=udrlMiYUmKh5NsIuJkNowqCenvtf5O925FPFawXSXbM,4021
3
- garf_executors/bq_executor.py,sha256=JBPxbDRYgUgpJv6SqYiFPypTFjZGIZ-SOOb6dS2sZQY,3822
4
- garf_executors/exceptions.py,sha256=U_7Q2ZMOUf89gzZd2pw7y3g7i1NeByPPKfpZ3q7p3ZU,662
5
- garf_executors/fetchers.py,sha256=gkAKHsDPzJySg4wYLZeCmNINtk6f17-jFzOP7tE82r8,1226
6
- garf_executors/sql_executor.py,sha256=6tpsd1Ive5igAlQuhCSkli-tZHp58uWAU86JWGvdVpE,2722
7
- garf_executors/entrypoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- garf_executors/entrypoints/cli.py,sha256=mWvPQkaqarDj5byHRvNAweVbUQiHZLXrC-35zY7l4fs,4043
9
- garf_executors/entrypoints/server.py,sha256=rJ29VKWKaYJci1BLxZx-0LSILmUMf5BK8G1RRjRS2ts,1836
10
- garf_executors/entrypoints/utils.py,sha256=ZZJFe2N4KwgzPRvak9gW_B25qESnzOyuF-qYZ2wW2_M,14974
11
- garf_executors-0.0.6.dist-info/METADATA,sha256=35dBABJ8cVH2nI0NonZ5VGO6W4IF0gtiiw-ZFZqZhgs,2648
12
- garf_executors-0.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- garf_executors-0.0.6.dist-info/entry_points.txt,sha256=LskWNFIw8j0WJuI18-32OZrlASXAMg1XtrRYwsKBz2E,61
14
- garf_executors-0.0.6.dist-info/top_level.txt,sha256=sP4dCXOENPn1hDFAunjMV8Js4NND_KGeO_gQWuaT0EY,15
15
- garf_executors-0.0.6.dist-info/RECORD,,