altimate-engine 0.1.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.
- altimate_engine/__init__.py +3 -0
- altimate_engine/__main__.py +5 -0
- altimate_engine/app/__init__.py +0 -0
- altimate_engine/ci/__init__.py +0 -0
- altimate_engine/ci/cost_gate.py +162 -0
- altimate_engine/connections.py +281 -0
- altimate_engine/connectors/__init__.py +21 -0
- altimate_engine/connectors/base.py +46 -0
- altimate_engine/connectors/bigquery.py +188 -0
- altimate_engine/connectors/databricks.py +200 -0
- altimate_engine/connectors/duckdb.py +91 -0
- altimate_engine/connectors/mysql.py +129 -0
- altimate_engine/connectors/postgres.py +109 -0
- altimate_engine/connectors/redshift.py +106 -0
- altimate_engine/connectors/snowflake.py +150 -0
- altimate_engine/connectors/sqlserver.py +201 -0
- altimate_engine/credential_store.py +123 -0
- altimate_engine/dbt/__init__.py +1 -0
- altimate_engine/dbt/lineage.py +168 -0
- altimate_engine/dbt/manifest.py +112 -0
- altimate_engine/dbt/profiles.py +164 -0
- altimate_engine/dbt/runner.py +68 -0
- altimate_engine/docker_discovery.py +118 -0
- altimate_engine/finops/__init__.py +0 -0
- altimate_engine/finops/credit_analyzer.py +346 -0
- altimate_engine/finops/query_history.py +218 -0
- altimate_engine/finops/role_access.py +255 -0
- altimate_engine/finops/unused_resources.py +226 -0
- altimate_engine/finops/warehouse_advisor.py +245 -0
- altimate_engine/local/__init__.py +1 -0
- altimate_engine/local/schema_sync.py +242 -0
- altimate_engine/local/test_local.py +74 -0
- altimate_engine/models.py +1082 -0
- altimate_engine/py.typed +0 -0
- altimate_engine/schema/__init__.py +1 -0
- altimate_engine/schema/cache.py +394 -0
- altimate_engine/schema/inspector.py +122 -0
- altimate_engine/schema/pii_detector.py +234 -0
- altimate_engine/schema/tags.py +151 -0
- altimate_engine/server.py +973 -0
- altimate_engine/sql/__init__.py +1 -0
- altimate_engine/sql/autocomplete.py +152 -0
- altimate_engine/sql/diff.py +63 -0
- altimate_engine/sql/executor.py +116 -0
- altimate_engine/sql/explainer.py +116 -0
- altimate_engine/sql/feedback_store.py +392 -0
- altimate_engine/sql/guard.py +657 -0
- altimate_engine/ssh_tunnel.py +108 -0
- altimate_engine-0.1.0.dist-info/METADATA +76 -0
- altimate_engine-0.1.0.dist-info/RECORD +51 -0
- altimate_engine-0.1.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""CI cost gate — scan changed SQL files for critical issues.
|
|
2
|
+
|
|
3
|
+
Reads SQL files, runs lint analysis, and returns
|
|
4
|
+
pass/fail based on whether CRITICAL severity issues are found.
|
|
5
|
+
|
|
6
|
+
Skips:
|
|
7
|
+
- Jinja templates ({{ }}, {% %})
|
|
8
|
+
- Parse errors (likely Jinja or non-standard SQL)
|
|
9
|
+
- Non-SQL files
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from altimate_engine.sql.guard import guard_lint
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Jinja pattern: {{ ... }} or {% ... %} or {# ... #}
|
|
22
|
+
_JINJA_PATTERN = re.compile(r"\{\{.*?\}\}|\{%.*?%\}|\{#.*?#\}", re.DOTALL)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _has_jinja(sql: str) -> bool:
|
|
26
|
+
"""Check if SQL contains Jinja template syntax."""
|
|
27
|
+
return bool(_JINJA_PATTERN.search(sql))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _split_statements(sql: str) -> list[str]:
|
|
31
|
+
"""Split SQL on semicolons, filtering empty statements."""
|
|
32
|
+
statements = []
|
|
33
|
+
for stmt in sql.split(";"):
|
|
34
|
+
stmt = stmt.strip()
|
|
35
|
+
if stmt:
|
|
36
|
+
statements.append(stmt)
|
|
37
|
+
return statements
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def scan_files(
|
|
41
|
+
file_paths: list[str],
|
|
42
|
+
dialect: str = "snowflake",
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
"""Scan SQL files for critical issues.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
file_paths: List of SQL file paths to scan.
|
|
48
|
+
dialect: SQL dialect for analysis (default: snowflake).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict with pass/fail status, per-file results, and summary.
|
|
52
|
+
"""
|
|
53
|
+
file_results: list[dict[str, Any]] = []
|
|
54
|
+
total_issues = 0
|
|
55
|
+
critical_count = 0
|
|
56
|
+
files_scanned = 0
|
|
57
|
+
files_skipped = 0
|
|
58
|
+
|
|
59
|
+
for path in file_paths:
|
|
60
|
+
# Skip non-SQL files
|
|
61
|
+
if not path.endswith(".sql"):
|
|
62
|
+
files_skipped += 1
|
|
63
|
+
file_results.append({
|
|
64
|
+
"file": path,
|
|
65
|
+
"status": "skipped",
|
|
66
|
+
"reason": "not a SQL file",
|
|
67
|
+
"issues": [],
|
|
68
|
+
})
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# Read file
|
|
72
|
+
if not os.path.isfile(path):
|
|
73
|
+
files_skipped += 1
|
|
74
|
+
file_results.append({
|
|
75
|
+
"file": path,
|
|
76
|
+
"status": "skipped",
|
|
77
|
+
"reason": "file not found",
|
|
78
|
+
"issues": [],
|
|
79
|
+
})
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
84
|
+
content = f.read()
|
|
85
|
+
except Exception as e:
|
|
86
|
+
files_skipped += 1
|
|
87
|
+
file_results.append({
|
|
88
|
+
"file": path,
|
|
89
|
+
"status": "skipped",
|
|
90
|
+
"reason": f"read error: {e}",
|
|
91
|
+
"issues": [],
|
|
92
|
+
})
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Skip Jinja templates
|
|
96
|
+
if _has_jinja(content):
|
|
97
|
+
files_skipped += 1
|
|
98
|
+
file_results.append({
|
|
99
|
+
"file": path,
|
|
100
|
+
"status": "skipped",
|
|
101
|
+
"reason": "contains Jinja templates",
|
|
102
|
+
"issues": [],
|
|
103
|
+
})
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Split and analyze each statement
|
|
107
|
+
statements = _split_statements(content)
|
|
108
|
+
if not statements:
|
|
109
|
+
files_skipped += 1
|
|
110
|
+
file_results.append({
|
|
111
|
+
"file": path,
|
|
112
|
+
"status": "skipped",
|
|
113
|
+
"reason": "empty file",
|
|
114
|
+
"issues": [],
|
|
115
|
+
})
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
files_scanned += 1
|
|
119
|
+
file_issues: list[dict[str, Any]] = []
|
|
120
|
+
|
|
121
|
+
for stmt in statements:
|
|
122
|
+
# Run lint analysis
|
|
123
|
+
lint_result = guard_lint(stmt)
|
|
124
|
+
if lint_result.get("error"):
|
|
125
|
+
# Parse error — skip this statement (likely incomplete SQL)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
for finding in lint_result.get("findings", lint_result.get("issues", [])):
|
|
129
|
+
severity = finding.get("severity", "warning")
|
|
130
|
+
file_issues.append({
|
|
131
|
+
"type": finding.get("rule", finding.get("type", "UNKNOWN")),
|
|
132
|
+
"severity": severity,
|
|
133
|
+
"message": finding.get("message", ""),
|
|
134
|
+
"source": "lint",
|
|
135
|
+
})
|
|
136
|
+
total_issues += 1
|
|
137
|
+
if severity in ("error", "critical"):
|
|
138
|
+
critical_count += 1
|
|
139
|
+
|
|
140
|
+
status = "fail" if any(
|
|
141
|
+
i["severity"] in ("error", "critical") for i in file_issues
|
|
142
|
+
) else "pass"
|
|
143
|
+
|
|
144
|
+
file_results.append({
|
|
145
|
+
"file": path,
|
|
146
|
+
"status": status,
|
|
147
|
+
"issues": file_issues,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
passed = critical_count == 0
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"success": True,
|
|
154
|
+
"passed": passed,
|
|
155
|
+
"exit_code": 0 if passed else 1,
|
|
156
|
+
"files_scanned": files_scanned,
|
|
157
|
+
"files_skipped": files_skipped,
|
|
158
|
+
"total_issues": total_issues,
|
|
159
|
+
"critical_count": critical_count,
|
|
160
|
+
"file_results": file_results,
|
|
161
|
+
"error": None,
|
|
162
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from altimate_engine.connectors.base import Connector
|
|
9
|
+
from altimate_engine.credential_store import resolve_config
|
|
10
|
+
from altimate_engine.ssh_tunnel import start, stop
|
|
11
|
+
|
|
12
|
+
SSH_FIELDS = {
|
|
13
|
+
"ssh_host",
|
|
14
|
+
"ssh_port",
|
|
15
|
+
"ssh_user",
|
|
16
|
+
"ssh_auth_type",
|
|
17
|
+
"ssh_key_path",
|
|
18
|
+
"ssh_password",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConnectionRegistry:
|
|
23
|
+
_connections: dict[str, dict[str, Any]] = {}
|
|
24
|
+
_loaded: bool = False
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def load(cls) -> None:
|
|
28
|
+
if cls._loaded:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
global_config = Path.home() / ".altimate-code" / "connections.json"
|
|
32
|
+
if global_config.exists():
|
|
33
|
+
with open(global_config) as f:
|
|
34
|
+
cls._connections.update(json.load(f))
|
|
35
|
+
|
|
36
|
+
project_config = Path.cwd() / ".altimate-code" / "connections.json"
|
|
37
|
+
if project_config.exists():
|
|
38
|
+
with open(project_config) as f:
|
|
39
|
+
cls._connections.update(json.load(f))
|
|
40
|
+
|
|
41
|
+
for key, value in os.environ.items():
|
|
42
|
+
if key.startswith("ALTIMATE_CODE_CONN_"):
|
|
43
|
+
name = key[len("ALTIMATE_CODE_CONN_") :].lower()
|
|
44
|
+
try:
|
|
45
|
+
cls._connections[name] = json.loads(value)
|
|
46
|
+
except json.JSONDecodeError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
cls._loaded = True
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get(cls, name: str) -> Connector:
|
|
53
|
+
cls.load()
|
|
54
|
+
|
|
55
|
+
if name not in cls._connections:
|
|
56
|
+
raise ValueError(f"Connection '{name}' not found in registry")
|
|
57
|
+
|
|
58
|
+
config = dict(cls._connections[name])
|
|
59
|
+
config = resolve_config(name, config)
|
|
60
|
+
|
|
61
|
+
ssh_host = config.get("ssh_host")
|
|
62
|
+
if ssh_host:
|
|
63
|
+
if config.get("connection_string"):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"SSH tunneling requires explicit host/port — "
|
|
66
|
+
"cannot be used with connection_string"
|
|
67
|
+
)
|
|
68
|
+
ssh_config = {
|
|
69
|
+
k: config.pop(k) for k in list(config.keys()) if k in SSH_FIELDS
|
|
70
|
+
}
|
|
71
|
+
local_port = start(
|
|
72
|
+
name=name,
|
|
73
|
+
ssh_host=ssh_config.get("ssh_host", ""),
|
|
74
|
+
remote_host=config.get("host", "localhost"),
|
|
75
|
+
remote_port=config.get("port", 5432),
|
|
76
|
+
ssh_port=ssh_config.get("ssh_port", 22),
|
|
77
|
+
ssh_user=ssh_config.get("ssh_user"),
|
|
78
|
+
ssh_auth_type=ssh_config.get("ssh_auth_type", "key"),
|
|
79
|
+
ssh_key_path=ssh_config.get("ssh_key_path"),
|
|
80
|
+
ssh_password=ssh_config.get("ssh_password"),
|
|
81
|
+
)
|
|
82
|
+
config["host"] = "127.0.0.1"
|
|
83
|
+
config["port"] = local_port
|
|
84
|
+
|
|
85
|
+
dialect = config.get("type", "duckdb")
|
|
86
|
+
|
|
87
|
+
if dialect == "duckdb":
|
|
88
|
+
from altimate_engine.connectors.duckdb import DuckDBConnector
|
|
89
|
+
|
|
90
|
+
return DuckDBConnector(
|
|
91
|
+
path=config.get("path", ":memory:"),
|
|
92
|
+
**{k: v for k, v in config.items() if k not in ("type", "path")},
|
|
93
|
+
)
|
|
94
|
+
elif dialect == "postgres":
|
|
95
|
+
from altimate_engine.connectors.postgres import PostgresConnector
|
|
96
|
+
|
|
97
|
+
return PostgresConnector(
|
|
98
|
+
connection_string=config.get("connection_string", ""),
|
|
99
|
+
**{
|
|
100
|
+
k: v
|
|
101
|
+
for k, v in config.items()
|
|
102
|
+
if k not in ("type", "connection_string")
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
elif dialect == "snowflake":
|
|
106
|
+
from altimate_engine.connectors.snowflake import SnowflakeConnector
|
|
107
|
+
|
|
108
|
+
_snowflake_keys = {
|
|
109
|
+
"type",
|
|
110
|
+
"account",
|
|
111
|
+
"user",
|
|
112
|
+
"password",
|
|
113
|
+
"private_key_path",
|
|
114
|
+
"private_key_passphrase",
|
|
115
|
+
"warehouse",
|
|
116
|
+
"database",
|
|
117
|
+
"schema",
|
|
118
|
+
"role",
|
|
119
|
+
}
|
|
120
|
+
return SnowflakeConnector(
|
|
121
|
+
account=config.get("account", ""),
|
|
122
|
+
user=config.get("user", ""),
|
|
123
|
+
password=config.get("password"),
|
|
124
|
+
private_key_path=config.get("private_key_path"),
|
|
125
|
+
private_key_passphrase=config.get("private_key_passphrase"),
|
|
126
|
+
warehouse=config.get("warehouse"),
|
|
127
|
+
database=config.get("database"),
|
|
128
|
+
schema=config.get("schema"),
|
|
129
|
+
role=config.get("role"),
|
|
130
|
+
**{k: v for k, v in config.items() if k not in _snowflake_keys},
|
|
131
|
+
)
|
|
132
|
+
elif dialect == "bigquery":
|
|
133
|
+
from altimate_engine.connectors.bigquery import BigQueryConnector
|
|
134
|
+
|
|
135
|
+
_bigquery_keys = {"type", "project", "credentials_path", "location"}
|
|
136
|
+
return BigQueryConnector(
|
|
137
|
+
project=config.get("project", ""),
|
|
138
|
+
credentials_path=config.get("credentials_path"),
|
|
139
|
+
location=config.get("location", "US"),
|
|
140
|
+
**{k: v for k, v in config.items() if k not in _bigquery_keys},
|
|
141
|
+
)
|
|
142
|
+
elif dialect == "databricks":
|
|
143
|
+
from altimate_engine.connectors.databricks import DatabricksConnector
|
|
144
|
+
|
|
145
|
+
_databricks_keys = {
|
|
146
|
+
"type",
|
|
147
|
+
"server_hostname",
|
|
148
|
+
"http_path",
|
|
149
|
+
"access_token",
|
|
150
|
+
"catalog",
|
|
151
|
+
"schema",
|
|
152
|
+
}
|
|
153
|
+
return DatabricksConnector(
|
|
154
|
+
server_hostname=config.get("server_hostname", ""),
|
|
155
|
+
http_path=config.get("http_path", ""),
|
|
156
|
+
access_token=config.get("access_token"),
|
|
157
|
+
catalog=config.get("catalog"),
|
|
158
|
+
schema=config.get("schema"),
|
|
159
|
+
**{k: v for k, v in config.items() if k not in _databricks_keys},
|
|
160
|
+
)
|
|
161
|
+
elif dialect == "redshift":
|
|
162
|
+
from altimate_engine.connectors.redshift import RedshiftConnector
|
|
163
|
+
|
|
164
|
+
_redshift_keys = {
|
|
165
|
+
"type",
|
|
166
|
+
"host",
|
|
167
|
+
"port",
|
|
168
|
+
"database",
|
|
169
|
+
"user",
|
|
170
|
+
"password",
|
|
171
|
+
"connection_string",
|
|
172
|
+
"iam_role",
|
|
173
|
+
"region",
|
|
174
|
+
"cluster_identifier",
|
|
175
|
+
}
|
|
176
|
+
return RedshiftConnector(
|
|
177
|
+
host=config.get("host", ""),
|
|
178
|
+
port=config.get("port", 5439),
|
|
179
|
+
database=config.get("database", "dev"),
|
|
180
|
+
user=config.get("user"),
|
|
181
|
+
password=config.get("password"),
|
|
182
|
+
connection_string=config.get("connection_string"),
|
|
183
|
+
iam_role=config.get("iam_role"),
|
|
184
|
+
region=config.get("region"),
|
|
185
|
+
cluster_identifier=config.get("cluster_identifier"),
|
|
186
|
+
**{k: v for k, v in config.items() if k not in _redshift_keys},
|
|
187
|
+
)
|
|
188
|
+
elif dialect == "mysql":
|
|
189
|
+
from altimate_engine.connectors.mysql import MySQLConnector
|
|
190
|
+
|
|
191
|
+
_mysql_keys = {
|
|
192
|
+
"type",
|
|
193
|
+
"host",
|
|
194
|
+
"port",
|
|
195
|
+
"database",
|
|
196
|
+
"user",
|
|
197
|
+
"password",
|
|
198
|
+
"ssl_ca",
|
|
199
|
+
"ssl_cert",
|
|
200
|
+
"ssl_key",
|
|
201
|
+
}
|
|
202
|
+
return MySQLConnector(
|
|
203
|
+
host=config.get("host", "localhost"),
|
|
204
|
+
port=config.get("port", 3306),
|
|
205
|
+
database=config.get("database"),
|
|
206
|
+
user=config.get("user"),
|
|
207
|
+
password=config.get("password"),
|
|
208
|
+
ssl_ca=config.get("ssl_ca"),
|
|
209
|
+
ssl_cert=config.get("ssl_cert"),
|
|
210
|
+
ssl_key=config.get("ssl_key"),
|
|
211
|
+
**{k: v for k, v in config.items() if k not in _mysql_keys},
|
|
212
|
+
)
|
|
213
|
+
elif dialect == "sqlserver":
|
|
214
|
+
from altimate_engine.connectors.sqlserver import SQLServerConnector
|
|
215
|
+
|
|
216
|
+
_sqlserver_keys = {
|
|
217
|
+
"type",
|
|
218
|
+
"host",
|
|
219
|
+
"port",
|
|
220
|
+
"database",
|
|
221
|
+
"user",
|
|
222
|
+
"password",
|
|
223
|
+
"driver",
|
|
224
|
+
"azure_auth",
|
|
225
|
+
"trust_server_certificate",
|
|
226
|
+
}
|
|
227
|
+
return SQLServerConnector(
|
|
228
|
+
host=config.get("host", "localhost"),
|
|
229
|
+
port=config.get("port", 1433),
|
|
230
|
+
database=config.get("database"),
|
|
231
|
+
user=config.get("user"),
|
|
232
|
+
password=config.get("password"),
|
|
233
|
+
driver=config.get("driver", "ODBC Driver 18 for SQL Server"),
|
|
234
|
+
azure_auth=config.get("azure_auth", False),
|
|
235
|
+
trust_server_certificate=config.get("trust_server_certificate", False),
|
|
236
|
+
**{k: v for k, v in config.items() if k not in _sqlserver_keys},
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
raise ValueError(f"Unsupported connector type: {dialect}")
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def list(cls) -> list[dict[str, Any]]:
|
|
243
|
+
cls.load()
|
|
244
|
+
return [
|
|
245
|
+
{"name": name, "type": config.get("type", "unknown")}
|
|
246
|
+
for name, config in cls._connections.items()
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def test(cls, name: str) -> dict[str, Any]:
|
|
251
|
+
try:
|
|
252
|
+
connector = cls.get(name)
|
|
253
|
+
connector.connect()
|
|
254
|
+
connector.execute("SELECT 1")
|
|
255
|
+
connector.close()
|
|
256
|
+
return {"connected": True, "error": None}
|
|
257
|
+
except Exception as e:
|
|
258
|
+
return {"connected": False, "error": str(e)}
|
|
259
|
+
finally:
|
|
260
|
+
stop(name)
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def add(cls, name: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
264
|
+
from altimate_engine.credential_store import save_connection
|
|
265
|
+
|
|
266
|
+
result = save_connection(name, config)
|
|
267
|
+
cls._loaded = False
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def remove(cls, name: str) -> bool:
|
|
272
|
+
from altimate_engine.credential_store import remove_connection
|
|
273
|
+
|
|
274
|
+
result = remove_connection(name)
|
|
275
|
+
cls._loaded = False
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def reload(cls) -> None:
|
|
280
|
+
cls._loaded = False
|
|
281
|
+
cls._connections.clear()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from altimate_engine.connectors.base import Connector
|
|
2
|
+
from altimate_engine.connectors.duckdb import DuckDBConnector
|
|
3
|
+
from altimate_engine.connectors.postgres import PostgresConnector
|
|
4
|
+
from altimate_engine.connectors.snowflake import SnowflakeConnector
|
|
5
|
+
from altimate_engine.connectors.bigquery import BigQueryConnector
|
|
6
|
+
from altimate_engine.connectors.databricks import DatabricksConnector
|
|
7
|
+
from altimate_engine.connectors.redshift import RedshiftConnector
|
|
8
|
+
from altimate_engine.connectors.mysql import MySQLConnector
|
|
9
|
+
from altimate_engine.connectors.sqlserver import SQLServerConnector
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Connector",
|
|
13
|
+
"DuckDBConnector",
|
|
14
|
+
"PostgresConnector",
|
|
15
|
+
"SnowflakeConnector",
|
|
16
|
+
"BigQueryConnector",
|
|
17
|
+
"DatabricksConnector",
|
|
18
|
+
"RedshiftConnector",
|
|
19
|
+
"MySQLConnector",
|
|
20
|
+
"SQLServerConnector",
|
|
21
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Connector(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def connect(self) -> Any:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def execute(self, sql: str, params: tuple | list | None = None, limit: int = 1000) -> list[dict[str, Any]]:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def list_schemas(self) -> list[str]:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def list_tables(self, schema: str) -> list[dict[str, Any]]:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def describe_table(self, schema: str, table: str) -> list[dict[str, Any]]:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def close(self) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def set_statement_timeout(self, timeout_ms: int) -> None:
|
|
33
|
+
"""Set a per-session statement timeout. Override in subclasses that support it.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
timeout_ms: Maximum query execution time in milliseconds.
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def __enter__(self):
|
|
41
|
+
self.connect()
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
45
|
+
self.close()
|
|
46
|
+
return False
|