google-evalbench 1.7.1__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.
- evalbench/__init__.py +12 -0
- evalbench/client/eval_client.py +149 -0
- evalbench/databases/__init__.py +42 -0
- evalbench/databases/alloydb.py +49 -0
- evalbench/databases/alloydb_omni.py +43 -0
- evalbench/databases/bigquery.py +297 -0
- evalbench/databases/bigtable.py +164 -0
- evalbench/databases/db.py +270 -0
- evalbench/databases/emulator_manager.py +133 -0
- evalbench/databases/mongodb.py +340 -0
- evalbench/databases/mysql.py +374 -0
- evalbench/databases/postgres.py +336 -0
- evalbench/databases/spanner.py +552 -0
- evalbench/databases/sqlite.py +297 -0
- evalbench/databases/sqlserver.py +320 -0
- evalbench/databases/util.py +162 -0
- evalbench/dataset/__init__.py +0 -0
- evalbench/dataset/cortadoinput.py +61 -0
- evalbench/dataset/dataset.py +326 -0
- evalbench/dataset/evalgeminicliinput.py +60 -0
- evalbench/dataset/evalinput.py +181 -0
- evalbench/dataset/evalinteractinput.py +123 -0
- evalbench/dataset/evalinteractoutput.py +9 -0
- evalbench/dataset/evaloutput.py +9 -0
- evalbench/eval_server.py +117 -0
- evalbench/eval_service.py +502 -0
- evalbench/evalbench.py +217 -0
- evalbench/evalproto/eval_config_pb2.py +39 -0
- evalbench/evalproto/eval_config_pb2.pyi +23 -0
- evalbench/evalproto/eval_config_pb2_grpc.py +24 -0
- evalbench/evalproto/eval_connect_pb2.py +37 -0
- evalbench/evalproto/eval_connect_pb2.pyi +15 -0
- evalbench/evalproto/eval_connect_pb2_grpc.py +24 -0
- evalbench/evalproto/eval_request_pb2.py +68 -0
- evalbench/evalproto/eval_request_pb2.pyi +165 -0
- evalbench/evalproto/eval_request_pb2_grpc.py +24 -0
- evalbench/evalproto/eval_response_pb2.py +37 -0
- evalbench/evalproto/eval_response_pb2.pyi +13 -0
- evalbench/evalproto/eval_response_pb2_grpc.py +24 -0
- evalbench/evalproto/eval_service_pb2.py +41 -0
- evalbench/evalproto/eval_service_pb2.pyi +8 -0
- evalbench/evalproto/eval_service_pb2_grpc.py +417 -0
- evalbench/evaluator/__init__.py +28 -0
- evalbench/evaluator/agentevaluator.py +263 -0
- evalbench/evaluator/agentorchestrator.py +55 -0
- evalbench/evaluator/cortadoevaluator.py +184 -0
- evalbench/evaluator/cortadoorchestrator.py +37 -0
- evalbench/evaluator/dataagentevaluator.py +205 -0
- evalbench/evaluator/dataagentorchestrator.py +228 -0
- evalbench/evaluator/dataagentvirtualuser.py +31 -0
- evalbench/evaluator/db_manager.py +116 -0
- evalbench/evaluator/evaluator.py +292 -0
- evalbench/evaluator/interactevaluator.py +190 -0
- evalbench/evaluator/interactorchestrator.py +229 -0
- evalbench/evaluator/oneshotorchestrator.py +286 -0
- evalbench/evaluator/orchestrator.py +71 -0
- evalbench/evaluator/progress_reporter.py +343 -0
- evalbench/evaluator/simulateduser.py +55 -0
- evalbench/evaluator/streamingorchestrator.py +205 -0
- evalbench/evaluator/virtualuser.py +31 -0
- evalbench/generators/models/__init__.py +49 -0
- evalbench/generators/models/alloydb_ai_nl.py +29 -0
- evalbench/generators/models/claude.py +43 -0
- evalbench/generators/models/claude_code.py +801 -0
- evalbench/generators/models/codex_cli.py +813 -0
- evalbench/generators/models/gemini.py +67 -0
- evalbench/generators/models/gemini_cli.py +1054 -0
- evalbench/generators/models/generator.py +33 -0
- evalbench/generators/models/grpc_proxy.py +123 -0
- evalbench/generators/models/passthrough.py +11 -0
- evalbench/generators/models/query_data_api.py +84 -0
- evalbench/generators/models/querydata.py +162 -0
- evalbench/generators/prompts/__init__.py +25 -0
- evalbench/generators/prompts/dataagentinteractuser.py +188 -0
- evalbench/generators/prompts/generator.py +17 -0
- evalbench/generators/prompts/interactsystem.py +90 -0
- evalbench/generators/prompts/interactuser.py +186 -0
- evalbench/generators/prompts/passthrough.py +13 -0
- evalbench/generators/prompts/simulateduser.py +46 -0
- evalbench/generators/prompts/sqlgenbase.py +276 -0
- evalbench/mp/__init__.py +0 -0
- evalbench/mp/mprunner.py +48 -0
- evalbench/reporting/__init__.py +21 -0
- evalbench/reporting/analyzer.py +210 -0
- evalbench/reporting/bqstore.py +200 -0
- evalbench/reporting/csv.py +41 -0
- evalbench/reporting/gcs_artifact.py +168 -0
- evalbench/reporting/report.py +48 -0
- evalbench/repository/__init__.py +5 -0
- evalbench/repository/base.py +10 -0
- evalbench/repository/nldRepo.py +39 -0
- evalbench/scorers/__init__.py +0 -0
- evalbench/scorers/behavioralmetrics.py +85 -0
- evalbench/scorers/binaryrubricscorer.py +87 -0
- evalbench/scorers/comparator.py +137 -0
- evalbench/scorers/dataformscorer.py +147 -0
- evalbench/scorers/dbtscorer.py +146 -0
- evalbench/scorers/endtoendlatency.py +89 -0
- evalbench/scorers/exact_match_consistency_comparator.py +55 -0
- evalbench/scorers/exactmatcher.py +50 -0
- evalbench/scorers/examples/sample_python_validator.py +50 -0
- evalbench/scorers/executablesql.py +50 -0
- evalbench/scorers/generatedqueryregexpmatcher.py +113 -0
- evalbench/scorers/goalcompletionrate.py +67 -0
- evalbench/scorers/llm_consistency_comparator.py +235 -0
- evalbench/scorers/llmrater.py +327 -0
- evalbench/scorers/multi_trial_comparator.py +50 -0
- evalbench/scorers/multi_trial_score.py +149 -0
- evalbench/scorers/parameteranalysis.py +67 -0
- evalbench/scorers/prompt/behavioralmetrics.py +23 -0
- evalbench/scorers/prompt/binaryrubricscorer.py +13 -0
- evalbench/scorers/prompt/goalcompletion.py +16 -0
- evalbench/scorers/prompt/parameteranalysis.py +34 -0
- evalbench/scorers/prompt/skillsbestpractices.py +64 -0
- evalbench/scorers/pythonscorer.py +93 -0
- evalbench/scorers/recallmatcher.py +123 -0
- evalbench/scorers/returnedsql.py +58 -0
- evalbench/scorers/score.py +203 -0
- evalbench/scorers/setmatcher.py +79 -0
- evalbench/scorers/skillsbestpractices.py +204 -0
- evalbench/scorers/skillstrajectorymatcher.py +108 -0
- evalbench/scorers/tokenconsumption.py +82 -0
- evalbench/scorers/toolcalllatency.py +80 -0
- evalbench/scorers/trajectorymatcher.py +138 -0
- evalbench/scorers/turncount.py +67 -0
- evalbench/scorers/util.py +63 -0
- evalbench/test/__init__.py +0 -0
- evalbench/test/alloydb_test.py +29 -0
- evalbench/test/bigtable_test.py +79 -0
- evalbench/test/binaryrubricscorer_test.py +103 -0
- evalbench/test/dbtscorer_test.py +172 -0
- evalbench/test/evalbench_test.py +84 -0
- evalbench/test/evaluator_test.py +211 -0
- evalbench/test/exact_match_consistency_comparator_test.py +75 -0
- evalbench/test/gcs_artifact_test.py +116 -0
- evalbench/test/llm_consistency_comparator_test.py +196 -0
- evalbench/test/llmrater_test.py +122 -0
- evalbench/test/mongodb_test.py +689 -0
- evalbench/test/multi_trial_score_test.py +228 -0
- evalbench/test/multi_trial_scorework_test.py +55 -0
- evalbench/test/oneshotorchestrator_test.py +157 -0
- evalbench/test/pythonscorer_test.py +92 -0
- evalbench/test/query_data_api_test.py +114 -0
- evalbench/test/robustness_test.py +197 -0
- evalbench/test/sessionmgr_test.py +52 -0
- evalbench/test/set_matcher_test.py +70 -0
- evalbench/test/spanner_test.py +49 -0
- evalbench/test/sqlgenwork_test.py +76 -0
- evalbench/test/test_db_cleanups.py +41 -0
- evalbench/test/test_spanner_cleanup.py +68 -0
- evalbench/test/trajectory_matcher_test.py +85 -0
- evalbench/util/__init__.py +9 -0
- evalbench/util/auth.py +112 -0
- evalbench/util/config.py +237 -0
- evalbench/util/context.py +3 -0
- evalbench/util/fake_mcp_server.py +81 -0
- evalbench/util/flags.py +7 -0
- evalbench/util/gcp.py +24 -0
- evalbench/util/interactutil.py +145 -0
- evalbench/util/loghandler.py +25 -0
- evalbench/util/progress.py +31 -0
- evalbench/util/rate_limit.py +41 -0
- evalbench/util/sanitizer.py +18 -0
- evalbench/util/scriptrunner.py +81 -0
- evalbench/util/service.py +17 -0
- evalbench/util/session.py +37 -0
- evalbench/util/sessionmgr.py +148 -0
- evalbench/util/setup_databases.py +83 -0
- evalbench/util/test_setup_databases.py +39 -0
- evalbench/work/__init__.py +5 -0
- evalbench/work/agentgenwork.py +67 -0
- evalbench/work/agentscorework.py +67 -0
- evalbench/work/dataagentvuserwork.py +32 -0
- evalbench/work/interactsqlexecwork.py +138 -0
- evalbench/work/multi_trial_scorework.py +52 -0
- evalbench/work/promptgenwork.py +31 -0
- evalbench/work/scorework.py +38 -0
- evalbench/work/sqlexecwork.py +193 -0
- evalbench/work/sqlgeninteractwork.py +35 -0
- evalbench/work/sqlgenquerydatawork.py +38 -0
- evalbench/work/sqlgenwork.py +61 -0
- evalbench/work/vuserwork.py +32 -0
- evalbench/work/work.py +21 -0
- google_evalbench-1.7.1.dist-info/METADATA +358 -0
- google_evalbench-1.7.1.dist-info/RECORD +189 -0
- google_evalbench-1.7.1.dist-info/WHEEL +5 -0
- google_evalbench-1.7.1.dist-info/entry_points.txt +2 -0
- google_evalbench-1.7.1.dist-info/licenses/LICENSE +202 -0
- google_evalbench-1.7.1.dist-info/top_level.txt +1 -0
evalbench/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Append package root to sys.path for legacy absolute imports.
|
|
5
|
+
# Using append (rather than insert) prevents namespace collisions in spawned child processes.
|
|
6
|
+
sys.path.append(os.path.dirname(__file__))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from . import reporting
|
|
10
|
+
from . import util
|
|
11
|
+
from . import dataset
|
|
12
|
+
from . import evaluator
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from aiologger import Logger
|
|
4
|
+
import grpc
|
|
5
|
+
from evalproto import eval_request_pb2, eval_connect_pb2, eval_config_pb2
|
|
6
|
+
from evalproto import eval_service_pb2_grpc
|
|
7
|
+
import random
|
|
8
|
+
import argparse
|
|
9
|
+
import google.oauth2.id_token
|
|
10
|
+
import google.auth.transport.requests
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_id_token(audience):
|
|
14
|
+
"""
|
|
15
|
+
Fetches an ID token for the specified audience.
|
|
16
|
+
"""
|
|
17
|
+
# The request object is used to make the HTTP call to fetch the token
|
|
18
|
+
request = google.auth.transport.requests.Request()
|
|
19
|
+
|
|
20
|
+
# This will search for credentials in:
|
|
21
|
+
# 1. Environment variables (GOOGLE_APPLICATION_CREDENTIALS)
|
|
22
|
+
# 2. Metadata server (if running on GCP)
|
|
23
|
+
token = google.oauth2.id_token.fetch_id_token(request, audience)
|
|
24
|
+
|
|
25
|
+
return token
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EvalbenchClient:
|
|
29
|
+
def __init__(self, endpoint: str):
|
|
30
|
+
self.endpoint = endpoint
|
|
31
|
+
if self.endpoint == "local":
|
|
32
|
+
host = os.getenv("EVALBENCH_HOST", "localhost")
|
|
33
|
+
port = os.getenv("PORT", "50051")
|
|
34
|
+
address = f"{host}:{port}"
|
|
35
|
+
if os.getenv("EVALBENCH_INSECURE", "").lower() == "true":
|
|
36
|
+
self.channel = grpc.aio.insecure_channel(address)
|
|
37
|
+
else:
|
|
38
|
+
channel_creds = grpc.alts_channel_credentials()
|
|
39
|
+
self.channel = grpc.aio.secure_channel(address, channel_creds)
|
|
40
|
+
else:
|
|
41
|
+
address = f"{self.endpoint}:443"
|
|
42
|
+
id_token = get_id_token(f"https://{self.endpoint}")
|
|
43
|
+
# 2. Create Call Credentialss
|
|
44
|
+
# This puts the token in the 'authorization: Bearer <token>' header
|
|
45
|
+
call_creds = grpc.access_token_call_credentials(id_token)
|
|
46
|
+
|
|
47
|
+
# 3. Create SSL Channel Credentials
|
|
48
|
+
# ID tokens require a secure (TLS) channel
|
|
49
|
+
channel_creds = grpc.ssl_channel_credentials()
|
|
50
|
+
|
|
51
|
+
# 4. Composite Credentials
|
|
52
|
+
# Combine the SSL channel with the token-based call credentials
|
|
53
|
+
composite_creds = grpc.composite_channel_credentials(
|
|
54
|
+
channel_creds, call_creds)
|
|
55
|
+
self.channel = grpc.aio.secure_channel(address, composite_creds)
|
|
56
|
+
|
|
57
|
+
self.stub = eval_service_pb2_grpc.EvalServiceStub(self.channel)
|
|
58
|
+
rpc_id = "{:032x}".format(random.getrandbits(128))
|
|
59
|
+
self.metadata = grpc.aio.Metadata(
|
|
60
|
+
("client-rpc-id", rpc_id),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def ping(self):
|
|
64
|
+
request = eval_request_pb2.PingRequest()
|
|
65
|
+
response = await self.stub.Ping(request, metadata=self.metadata)
|
|
66
|
+
return response
|
|
67
|
+
|
|
68
|
+
async def connect(self):
|
|
69
|
+
request = eval_connect_pb2.EvalConnectRequest()
|
|
70
|
+
request.client_id = "me"
|
|
71
|
+
response = await self.stub.Connect(request, metadata=self.metadata)
|
|
72
|
+
return response
|
|
73
|
+
|
|
74
|
+
async def set_evalconfig(self, experiment: str):
|
|
75
|
+
data = None
|
|
76
|
+
with open(experiment, "rb") as f:
|
|
77
|
+
data = f.read()
|
|
78
|
+
request = eval_config_pb2.EvalConfigRequest()
|
|
79
|
+
request.yaml_config = data
|
|
80
|
+
response = await self.stub.EvalConfig(request, metadata=self.metadata)
|
|
81
|
+
return response
|
|
82
|
+
|
|
83
|
+
async def get_evalinputs(self):
|
|
84
|
+
request = eval_request_pb2.EvalInputRequest()
|
|
85
|
+
get_evalinputs_stream = self.stub.ListEvalInputs(
|
|
86
|
+
request, metadata=self.metadata
|
|
87
|
+
)
|
|
88
|
+
while True:
|
|
89
|
+
response = await get_evalinputs_stream.read()
|
|
90
|
+
if response == grpc.aio.EOF:
|
|
91
|
+
break
|
|
92
|
+
yield response
|
|
93
|
+
|
|
94
|
+
async def eval(self, evalinputs):
|
|
95
|
+
eval_call = self.stub.Eval(metadata=self.metadata)
|
|
96
|
+
for eval_input in evalinputs:
|
|
97
|
+
await eval_call.write(eval_input)
|
|
98
|
+
await eval_call.done_writing()
|
|
99
|
+
response = await eval_call
|
|
100
|
+
return response
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def run(experiment: str, endpoint: str) -> None:
|
|
104
|
+
logger = Logger.with_default_handlers(name="evalbench-logger")
|
|
105
|
+
evalbenchclient = EvalbenchClient(endpoint)
|
|
106
|
+
response = await evalbenchclient.ping()
|
|
107
|
+
logger.info(f"ping Returned: {response.response}")
|
|
108
|
+
|
|
109
|
+
response = await evalbenchclient.connect()
|
|
110
|
+
logger.info(f"connect Returned: {response.response}")
|
|
111
|
+
|
|
112
|
+
response = await evalbenchclient.ping()
|
|
113
|
+
logger.info(f"ping Returned: {response.response}")
|
|
114
|
+
|
|
115
|
+
response = await evalbenchclient.set_evalconfig(experiment)
|
|
116
|
+
logger.info(f"get_evalinput Returned: {response.response}")
|
|
117
|
+
|
|
118
|
+
evalInputs = []
|
|
119
|
+
async for response in evalbenchclient.get_evalinputs():
|
|
120
|
+
evalInputs.append(response)
|
|
121
|
+
logger.info(f"evalInputs: {len(evalInputs)}")
|
|
122
|
+
response = await evalbenchclient.eval(evalInputs)
|
|
123
|
+
logger.info(f"eval Returned: {response.response}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def main():
|
|
127
|
+
logger = Logger.with_default_handlers(name="evalbench-logger")
|
|
128
|
+
parser = argparse.ArgumentParser()
|
|
129
|
+
parser.add_argument("--experiment", dest="experiment")
|
|
130
|
+
parser.add_argument("--endpoint", dest="endpoint")
|
|
131
|
+
known_args, _ = parser.parse_known_args()
|
|
132
|
+
|
|
133
|
+
await run(known_args.experiment, known_args.endpoint)
|
|
134
|
+
# get a set of all running tasks
|
|
135
|
+
all_tasks = asyncio.all_tasks()
|
|
136
|
+
# get the current tasks
|
|
137
|
+
current_task = asyncio.current_task()
|
|
138
|
+
# remove the current task from the list of all tasks
|
|
139
|
+
all_tasks.remove(current_task)
|
|
140
|
+
# report a message
|
|
141
|
+
print(f"Main waiting for {len(all_tasks)} tasks...")
|
|
142
|
+
# suspend until all tasks are completed
|
|
143
|
+
if len(all_tasks) > 0:
|
|
144
|
+
await asyncio.wait(all_tasks)
|
|
145
|
+
await logger.shutdown()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .postgres import PGDB
|
|
2
|
+
from .mysql import MySQLDB
|
|
3
|
+
from .sqlserver import SQLServerDB
|
|
4
|
+
from .sqlite import SQLiteDB
|
|
5
|
+
from .db import DB
|
|
6
|
+
from .bigquery import BQDB
|
|
7
|
+
from .bigtable import BigtableDB
|
|
8
|
+
from .alloydb import AlloyDB
|
|
9
|
+
from .alloydb_omni import AlloyDBOmni
|
|
10
|
+
from .spanner import SpannerDB
|
|
11
|
+
from .mongodb import MongoDB
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_database(db_config, db_name) -> DB:
|
|
15
|
+
# if db_name is provided:
|
|
16
|
+
# - It will override the provided default database_name
|
|
17
|
+
# - This is useful as the default db may be "postgres" or a default only used for setup
|
|
18
|
+
if db_name:
|
|
19
|
+
suffix = db_config.get("db_name_suffix", "")
|
|
20
|
+
db_config["database_name"] = f"{db_name}{suffix}"
|
|
21
|
+
|
|
22
|
+
if db_config["db_type"] == "postgres":
|
|
23
|
+
return PGDB(db_config)
|
|
24
|
+
if db_config["db_type"] == "spanner":
|
|
25
|
+
return SpannerDB(db_config)
|
|
26
|
+
if db_config["db_type"] == "mysql":
|
|
27
|
+
return MySQLDB(db_config)
|
|
28
|
+
if db_config["db_type"] == "sqlserver":
|
|
29
|
+
return SQLServerDB(db_config)
|
|
30
|
+
if db_config["db_type"] == "sqlite":
|
|
31
|
+
return SQLiteDB(db_config)
|
|
32
|
+
if db_config["db_type"] == "bigquery":
|
|
33
|
+
return BQDB(db_config)
|
|
34
|
+
if db_config["db_type"] == "alloydb":
|
|
35
|
+
return AlloyDB(db_config)
|
|
36
|
+
if db_config["db_type"] == "alloydb_omni":
|
|
37
|
+
return AlloyDBOmni(db_config)
|
|
38
|
+
if db_config["db_type"] == "bigtable":
|
|
39
|
+
return BigtableDB(db_config)
|
|
40
|
+
if db_config["db_type"] == "mongodb":
|
|
41
|
+
return MongoDB(db_config)
|
|
42
|
+
raise ValueError("DB Type not Supported")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
from .db import DB
|
|
3
|
+
from .postgres import PGDB
|
|
4
|
+
import sqlalchemy
|
|
5
|
+
from sqlalchemy.pool import NullPool
|
|
6
|
+
from google.cloud.alloydb.connector import Connector as AlloyDBConnector
|
|
7
|
+
from google.cloud.alloydb.connector import IPTypes as AlloyDBIPTypes
|
|
8
|
+
|
|
9
|
+
CONNECTOR = AlloyDBConnector()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AlloyDB(PGDB):
|
|
13
|
+
def __init__(self, db_config):
|
|
14
|
+
"""
|
|
15
|
+
Initializes the AlloyDB connection, overriding the PGDB's
|
|
16
|
+
default Google Cloud SQL connection mechanism.
|
|
17
|
+
"""
|
|
18
|
+
super().__init__(db_config)
|
|
19
|
+
self.nl_config = db_config['nl_config']
|
|
20
|
+
|
|
21
|
+
if 'api_endpoint' in db_config:
|
|
22
|
+
CONNECTOR._alloydb_api_endpoint = db_config['api_endpoint']
|
|
23
|
+
|
|
24
|
+
def get_conn_alloydb():
|
|
25
|
+
return CONNECTOR.connect(
|
|
26
|
+
self.db_path,
|
|
27
|
+
"pg8000",
|
|
28
|
+
user=self.username,
|
|
29
|
+
password=self.password,
|
|
30
|
+
db=self.db_name,
|
|
31
|
+
enable_iam_auth=self.use_adc, # handled in PGDB
|
|
32
|
+
ip_type=AlloyDBIPTypes.PUBLIC,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def get_engine_args_alloydb():
|
|
36
|
+
common_args = {
|
|
37
|
+
"creator": get_conn_alloydb,
|
|
38
|
+
"connect_args": {"command_timeout": 60},
|
|
39
|
+
}
|
|
40
|
+
if "is_tmp_db" in db_config:
|
|
41
|
+
common_args["poolclass"] = NullPool
|
|
42
|
+
else:
|
|
43
|
+
common_args["pool_size"] = 50
|
|
44
|
+
common_args["pool_recycle"] = 300
|
|
45
|
+
return common_args
|
|
46
|
+
|
|
47
|
+
self.engine = sqlalchemy.create_engine(
|
|
48
|
+
"postgresql+pg8000://", **get_engine_args_alloydb()
|
|
49
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
from .db import DB
|
|
3
|
+
from .postgres import PGDB
|
|
4
|
+
import sqlalchemy
|
|
5
|
+
import pg8000
|
|
6
|
+
from sqlalchemy.pool import NullPool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AlloyDBOmni(PGDB):
|
|
10
|
+
def __init__(self, db_config):
|
|
11
|
+
"""
|
|
12
|
+
Initializes the AlloyDB connection, overriding the PGDB's
|
|
13
|
+
default Google Cloud SQL connection mechanism.
|
|
14
|
+
"""
|
|
15
|
+
super().__init__(db_config)
|
|
16
|
+
self.nl_config = db_config['nl_config']
|
|
17
|
+
self.host = db_config.get("host", "localhost")
|
|
18
|
+
self.port = db_config.get("port", 5432)
|
|
19
|
+
|
|
20
|
+
def get_conn_alloydb():
|
|
21
|
+
return pg8000.connect(
|
|
22
|
+
user=self.username,
|
|
23
|
+
password=self.password,
|
|
24
|
+
host=self.host,
|
|
25
|
+
port=self.port,
|
|
26
|
+
database=self.db_name
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get_engine_args_alloydb():
|
|
30
|
+
common_args = {
|
|
31
|
+
"creator": get_conn_alloydb,
|
|
32
|
+
"connect_args": {"command_timeout": 60},
|
|
33
|
+
}
|
|
34
|
+
if "is_tmp_db" in db_config:
|
|
35
|
+
common_args["poolclass"] = NullPool
|
|
36
|
+
else:
|
|
37
|
+
common_args["pool_size"] = 50
|
|
38
|
+
common_args["pool_recycle"] = 300
|
|
39
|
+
return common_args
|
|
40
|
+
|
|
41
|
+
self.engine = sqlalchemy.create_engine(
|
|
42
|
+
"postgresql+pg8000://", **get_engine_args_alloydb()
|
|
43
|
+
)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from google.cloud import bigquery
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from .db import DB
|
|
5
|
+
from .util import with_cache_execute, DatabaseSchema
|
|
6
|
+
from util.rate_limit import rate_limit, ResourceExhaustedError
|
|
7
|
+
from typing import List, Optional, Tuple, Any, Dict
|
|
8
|
+
import json
|
|
9
|
+
import sqlparse
|
|
10
|
+
from google.cloud.bigquery import QueryJobConfig, ConnectionProperty
|
|
11
|
+
from util.gcp import get_gcp_project
|
|
12
|
+
from google.api_core.exceptions import GoogleAPICallError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BQDB(DB):
|
|
16
|
+
|
|
17
|
+
#####################################################
|
|
18
|
+
#####################################################
|
|
19
|
+
# Database Connection Setup Logic
|
|
20
|
+
#####################################################
|
|
21
|
+
#####################################################
|
|
22
|
+
|
|
23
|
+
def __init__(self, db_config):
|
|
24
|
+
super().__init__(db_config)
|
|
25
|
+
self.project_id = get_gcp_project("")
|
|
26
|
+
self.location = db_config.get("location", "US")
|
|
27
|
+
self.client = bigquery.Client(project=self.project_id)
|
|
28
|
+
self.tmp_users = []
|
|
29
|
+
|
|
30
|
+
#####################################################
|
|
31
|
+
#####################################################
|
|
32
|
+
# Database Specific Execution Logic and Handling
|
|
33
|
+
#####################################################
|
|
34
|
+
#####################################################
|
|
35
|
+
|
|
36
|
+
def _execute_queries(self, query: str, job_config: Optional[bigquery.QueryJobConfig] = None) -> List:
|
|
37
|
+
result: List = []
|
|
38
|
+
for sub_query in sqlparse.split(query):
|
|
39
|
+
if sub_query:
|
|
40
|
+
resultset = self.client.query(sub_query, job_config)
|
|
41
|
+
rows = resultset.result()
|
|
42
|
+
if rows:
|
|
43
|
+
for row in rows:
|
|
44
|
+
result.append(dict(row))
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
def batch_execute(self, commands: list[str]):
|
|
48
|
+
for command in commands:
|
|
49
|
+
self.execute(command)
|
|
50
|
+
|
|
51
|
+
def execute(
|
|
52
|
+
self, query: str, eval_query: Optional[str] = None, use_cache=False, rollback=False
|
|
53
|
+
) -> Tuple[Any, Any, Any]:
|
|
54
|
+
if query.strip() == "":
|
|
55
|
+
return None, None, None
|
|
56
|
+
if not use_cache or not self.cache_client or eval_query:
|
|
57
|
+
return self._execute(query, eval_query, rollback)
|
|
58
|
+
return with_cache_execute(
|
|
59
|
+
query, f"{self.project_id}.{self.db_name}", self._execute, self.cache_client
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _execute(
|
|
63
|
+
self, query: str, eval_query: Optional[str] = None, rollback=False
|
|
64
|
+
) -> Tuple[Any, Any, Any]:
|
|
65
|
+
def _run_execute(query: str, eval_query: Optional[str] = None, rollback=False):
|
|
66
|
+
result: List = []
|
|
67
|
+
eval_result: List = []
|
|
68
|
+
error = None
|
|
69
|
+
query_replaced = query.replace("{{dataset}}", self.db_name)
|
|
70
|
+
if eval_query is not None:
|
|
71
|
+
eval_query_replaced = eval_query.replace(
|
|
72
|
+
"{{dataset}}", self.db_name)
|
|
73
|
+
try:
|
|
74
|
+
if rollback:
|
|
75
|
+
try:
|
|
76
|
+
initial_query = "SELECT 1;"
|
|
77
|
+
job_config = QueryJobConfig(create_session=True)
|
|
78
|
+
init_job = self.client.query(
|
|
79
|
+
initial_query, job_config=job_config)
|
|
80
|
+
init_job.result()
|
|
81
|
+
session_id = init_job.session_info.session_id
|
|
82
|
+
conn_props = [ConnectionProperty(
|
|
83
|
+
key="session_id", value=session_id)]
|
|
84
|
+
|
|
85
|
+
self.client.query(
|
|
86
|
+
"BEGIN TRANSACTION;",
|
|
87
|
+
job_config=QueryJobConfig(
|
|
88
|
+
connection_properties=conn_props)
|
|
89
|
+
).result()
|
|
90
|
+
|
|
91
|
+
result = self._execute_queries(
|
|
92
|
+
query_replaced, job_config=QueryJobConfig(connection_properties=conn_props))
|
|
93
|
+
|
|
94
|
+
if eval_query:
|
|
95
|
+
eval_result = self._execute_queries(
|
|
96
|
+
eval_query_replaced, job_config=QueryJobConfig(connection_properties=conn_props))
|
|
97
|
+
|
|
98
|
+
self.client.query(
|
|
99
|
+
"ROLLBACK TRANSACTION;",
|
|
100
|
+
job_config=QueryJobConfig(
|
|
101
|
+
connection_properties=conn_props)
|
|
102
|
+
).result()
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
error = str(e)
|
|
106
|
+
print(f"Error: {error}")
|
|
107
|
+
|
|
108
|
+
finally:
|
|
109
|
+
if 'session_id' in locals():
|
|
110
|
+
self.client.query(
|
|
111
|
+
"CALL BQ.ABORT_SESSION();",
|
|
112
|
+
job_config=QueryJobConfig(
|
|
113
|
+
connection_properties=conn_props)
|
|
114
|
+
).result()
|
|
115
|
+
if not rollback:
|
|
116
|
+
result = self._execute_queries(query_replaced)
|
|
117
|
+
|
|
118
|
+
if eval_query and not rollback:
|
|
119
|
+
eval_result = self._execute_queries(eval_query_replaced)
|
|
120
|
+
|
|
121
|
+
except (GoogleAPICallError, Exception) as e:
|
|
122
|
+
error = str(e)
|
|
123
|
+
if "resources exceeded" in error:
|
|
124
|
+
raise ResourceExhaustedError(
|
|
125
|
+
f"BigQuery resources exhausted: {e}") from e
|
|
126
|
+
elif "quota exceeded" in error:
|
|
127
|
+
raise ResourceExhaustedError(
|
|
128
|
+
f"BigQuery quota exceeded: {e}") from e
|
|
129
|
+
else:
|
|
130
|
+
print(error)
|
|
131
|
+
|
|
132
|
+
return result, eval_result, error
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
return rate_limit(
|
|
136
|
+
(query, eval_query, rollback),
|
|
137
|
+
_run_execute,
|
|
138
|
+
self.execs_per_minute,
|
|
139
|
+
self.semaphore,
|
|
140
|
+
self.max_attempts,
|
|
141
|
+
)
|
|
142
|
+
except ResourceExhaustedError as e:
|
|
143
|
+
logging.info(
|
|
144
|
+
"Resource Exhausted on Postgres DB. Giving up execution. Try reducing execs_per_minute."
|
|
145
|
+
)
|
|
146
|
+
return None, None, None
|
|
147
|
+
|
|
148
|
+
def get_metadata(self) -> dict:
|
|
149
|
+
metadata = {}
|
|
150
|
+
try:
|
|
151
|
+
for table in self.client.list_tables(self.db_name):
|
|
152
|
+
schema = self.client.get_table(table.reference).schema
|
|
153
|
+
metadata[table.table_id] = [
|
|
154
|
+
{"name": f.name, "type": f.field_type} for f in schema]
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(
|
|
157
|
+
f"Error while fetching metadata for dataset '{self.db_name}': {e}")
|
|
158
|
+
return metadata
|
|
159
|
+
|
|
160
|
+
#####################################################
|
|
161
|
+
#####################################################
|
|
162
|
+
# Setup / Teardown of temporary databases
|
|
163
|
+
#####################################################
|
|
164
|
+
#####################################################
|
|
165
|
+
|
|
166
|
+
def generate_ddl(self, schema: DatabaseSchema) -> List[str]:
|
|
167
|
+
ddl_statements = []
|
|
168
|
+
try:
|
|
169
|
+
for table in schema.tables:
|
|
170
|
+
columns = ", ".join(
|
|
171
|
+
[f"{col.name} {col.type}" for col in table.columns])
|
|
172
|
+
ddl_statements.append(
|
|
173
|
+
f"CREATE TABLE `{self.project_id}.{self.db_name}.{table.name}` ({columns})"
|
|
174
|
+
)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
print(f"Error generating DDL statements: {e}")
|
|
177
|
+
return ddl_statements
|
|
178
|
+
|
|
179
|
+
def create_tmp_database(self, database_name: str):
|
|
180
|
+
dataset_ref = bigquery.Dataset(f"{self.project_id}.{database_name}")
|
|
181
|
+
dataset_ref.location = self.location
|
|
182
|
+
self.client.create_dataset(dataset_ref, exists_ok=True)
|
|
183
|
+
self.tmp_dbs.append(database_name)
|
|
184
|
+
|
|
185
|
+
def drop_tmp_database(self, database_name: str):
|
|
186
|
+
try:
|
|
187
|
+
self.client.delete_dataset(
|
|
188
|
+
dataset=database_name,
|
|
189
|
+
delete_contents=True,
|
|
190
|
+
not_found_ok=True,
|
|
191
|
+
)
|
|
192
|
+
if database_name in self.tmp_dbs:
|
|
193
|
+
self.tmp_dbs.remove(database_name)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logging.warning(f"Failed to drop dataset {database_name}: {e}")
|
|
196
|
+
|
|
197
|
+
def drop_all_tables(self):
|
|
198
|
+
try:
|
|
199
|
+
tables = list(self.client.list_tables(self.db_name))
|
|
200
|
+
|
|
201
|
+
if tables:
|
|
202
|
+
for table in tables:
|
|
203
|
+
full_table_id = f"{self.project_id}.{self.db_name}.{table.table_id}"
|
|
204
|
+
self.client.delete_table(full_table_id)
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Failed to drop tables in dataset {self.db_name}: {e}")
|
|
209
|
+
|
|
210
|
+
def _is_float(self, value) -> bool:
|
|
211
|
+
try:
|
|
212
|
+
float(value)
|
|
213
|
+
except ValueError:
|
|
214
|
+
return False
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def _get_column_name_to_type_mapping(self, sql_statements: List[str]) -> Dict[str, Dict[str, str]]:
|
|
218
|
+
schema_mapping = {}
|
|
219
|
+
|
|
220
|
+
for statement in sql_statements:
|
|
221
|
+
table_match = re.search(
|
|
222
|
+
r'CREATE TABLE\s+`{{dataset}}\.(\w+)`', statement)
|
|
223
|
+
if not table_match:
|
|
224
|
+
continue
|
|
225
|
+
table_name = table_match.group(1)
|
|
226
|
+
column_section_match = re.search(
|
|
227
|
+
r'\(\n(.*?)\n\)', statement, re.DOTALL)
|
|
228
|
+
if not column_section_match:
|
|
229
|
+
continue
|
|
230
|
+
columns_raw = column_section_match.group(1).split(",\n")
|
|
231
|
+
column_type_mapping = {}
|
|
232
|
+
for col in columns_raw:
|
|
233
|
+
if col.strip().startswith("PRIMARY KEY"):
|
|
234
|
+
continue
|
|
235
|
+
col_parts = col.strip().split()
|
|
236
|
+
if len(col_parts) >= 2:
|
|
237
|
+
column_name = col_parts[0]
|
|
238
|
+
column_type = col_parts[1]
|
|
239
|
+
column_type_mapping[column_name] = column_type
|
|
240
|
+
|
|
241
|
+
schema_mapping[table_name] = column_type_mapping
|
|
242
|
+
|
|
243
|
+
return schema_mapping
|
|
244
|
+
|
|
245
|
+
def insert_data(self, data: dict[str, List[str]], setup: Optional[List[str]] = None):
|
|
246
|
+
if not data:
|
|
247
|
+
return
|
|
248
|
+
schema_mapping = self._get_column_name_to_type_mapping(setup)
|
|
249
|
+
insertion_statements = []
|
|
250
|
+
|
|
251
|
+
for table_name in data:
|
|
252
|
+
column_names = list(schema_mapping[table_name].keys())
|
|
253
|
+
for row in data[table_name]:
|
|
254
|
+
formatted_values = []
|
|
255
|
+
|
|
256
|
+
for index, value in enumerate(row):
|
|
257
|
+
col_name = column_names[index]
|
|
258
|
+
col_type = schema_mapping[table_name][col_name].upper()
|
|
259
|
+
|
|
260
|
+
if col_type == 'BOOL':
|
|
261
|
+
if value == "'1'":
|
|
262
|
+
formatted_values.append("TRUE")
|
|
263
|
+
elif value == "'0'":
|
|
264
|
+
formatted_values.append("FALSE")
|
|
265
|
+
else:
|
|
266
|
+
formatted_values.append(f"{value}")
|
|
267
|
+
elif self._is_float(value):
|
|
268
|
+
formatted_values.append(f"{value}")
|
|
269
|
+
elif col_type == 'JSON':
|
|
270
|
+
formatted_values.append(f"PARSE_JSON({value})")
|
|
271
|
+
else:
|
|
272
|
+
escaped_value = value.replace("''", "\\'")
|
|
273
|
+
formatted_values.append(f"{escaped_value}")
|
|
274
|
+
|
|
275
|
+
inline_columns = ", ".join(formatted_values)
|
|
276
|
+
insertion_statements.append(
|
|
277
|
+
f"INSERT INTO `{self.project_id}.{self.db_name}.{table_name}` VALUES ({inline_columns});"
|
|
278
|
+
)
|
|
279
|
+
try:
|
|
280
|
+
self.batch_execute(insertion_statements)
|
|
281
|
+
except RuntimeError as error:
|
|
282
|
+
raise RuntimeError(f"Could not insert data into database: {error}")
|
|
283
|
+
|
|
284
|
+
#####################################################
|
|
285
|
+
#####################################################
|
|
286
|
+
# Database User Management
|
|
287
|
+
#####################################################
|
|
288
|
+
#####################################################
|
|
289
|
+
|
|
290
|
+
def close_connections(self):
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
def create_tmp_users(self, dql_user: str, dml_user: str, tmp_password: str):
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
def delete_tmp_user(self, username: str):
|
|
297
|
+
pass
|