querypilot 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.
Files changed (54) hide show
  1. querypilot/__init__.py +36 -0
  2. querypilot/access/__init__.py +3 -0
  3. querypilot/access/policy.py +32 -0
  4. querypilot/adapters/__init__.py +1 -0
  5. querypilot/adapters/anthropic.py +49 -0
  6. querypilot/adapters/openai.py +53 -0
  7. querypilot/audit/__init__.py +10 -0
  8. querypilot/audit/sinks.py +46 -0
  9. querypilot/audit/types.py +53 -0
  10. querypilot/cli.py +401 -0
  11. querypilot/connectors/__init__.py +5 -0
  12. querypilot/connectors/base.py +24 -0
  13. querypilot/connectors/postgres.py +48 -0
  14. querypilot/connectors/sqlite.py +49 -0
  15. querypilot/core/__init__.py +1 -0
  16. querypilot/core/client.py +286 -0
  17. querypilot/core/config.py +29 -0
  18. querypilot/core/safety_defaults.py +12 -0
  19. querypilot/core/types.py +89 -0
  20. querypilot/evals/__init__.py +81 -0
  21. querypilot/evals/check.py +222 -0
  22. querypilot/evals/compare.py +286 -0
  23. querypilot/evals/cost.py +202 -0
  24. querypilot/evals/factory.py +83 -0
  25. querypilot/evals/init.py +104 -0
  26. querypilot/evals/loader.py +179 -0
  27. querypilot/evals/pipeline.py +484 -0
  28. querypilot/evals/replay.py +217 -0
  29. querypilot/evals/report.py +246 -0
  30. querypilot/evals/suite.py +91 -0
  31. querypilot/evals/suite_runner.py +361 -0
  32. querypilot/execution/__init__.py +1 -0
  33. querypilot/execution/formatter.py +11 -0
  34. querypilot/execution/runner.py +13 -0
  35. querypilot/generation/__init__.py +9 -0
  36. querypilot/generation/llm.py +180 -0
  37. querypilot/generation/prompt_builder.py +49 -0
  38. querypilot/generation/sql_generator.py +82 -0
  39. querypilot/mcp/__init__.py +3 -0
  40. querypilot/mcp/server.py +51 -0
  41. querypilot/py.typed +0 -0
  42. querypilot/schema/__init__.py +3 -0
  43. querypilot/schema/introspector.py +8 -0
  44. querypilot/schema/search.py +33 -0
  45. querypilot/server/__init__.py +3 -0
  46. querypilot/server/app.py +157 -0
  47. querypilot/validation/__init__.py +3 -0
  48. querypilot/validation/policies.py +3 -0
  49. querypilot/validation/validator.py +592 -0
  50. querypilot-0.1.0.dist-info/METADATA +601 -0
  51. querypilot-0.1.0.dist-info/RECORD +54 -0
  52. querypilot-0.1.0.dist-info/WHEEL +4 -0
  53. querypilot-0.1.0.dist-info/entry_points.txt +2 -0
  54. querypilot-0.1.0.dist-info/licenses/LICENSE +21 -0
querypilot/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ from querypilot.access import AccessPolicy, MaskingRule
2
+ from querypilot.core.client import QueryPilot
3
+ from querypilot.core.types import (
4
+ ColumnSchema,
5
+ DatabaseSchema,
6
+ GeneratedSQL,
7
+ PolicyCheck,
8
+ QueryPilotAnswer,
9
+ QueryResult,
10
+ SchemaMatch,
11
+ TableSchema,
12
+ ValidationResult,
13
+ )
14
+ from querypilot.generation import AnthropicSQLGenerator, OpenAISQLGenerator
15
+ from querypilot.audit import AuditMetadata, InMemoryAuditSink, JSONLAuditSink, QueryAuditRecord
16
+
17
+ __all__ = [
18
+ "ColumnSchema",
19
+ "DatabaseSchema",
20
+ "GeneratedSQL",
21
+ "PolicyCheck",
22
+ "QueryPilot",
23
+ "QueryPilotAnswer",
24
+ "QueryResult",
25
+ "AnthropicSQLGenerator",
26
+ "AccessPolicy",
27
+ "AuditMetadata",
28
+ "InMemoryAuditSink",
29
+ "JSONLAuditSink",
30
+ "MaskingRule",
31
+ "OpenAISQLGenerator",
32
+ "QueryAuditRecord",
33
+ "SchemaMatch",
34
+ "TableSchema",
35
+ "ValidationResult",
36
+ ]
@@ -0,0 +1,3 @@
1
+ from querypilot.access.policy import AccessPolicy, MaskingRule
2
+
3
+ __all__ = ["AccessPolicy", "MaskingRule"]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class MaskingRule(BaseModel):
9
+ mode: Literal["redact", "null", "hash"] = "redact"
10
+
11
+
12
+ class AccessPolicy(BaseModel):
13
+ allowed_columns: dict[str, list[str]] = Field(default_factory=dict)
14
+ blocked_columns: dict[str, list[str]] = Field(default_factory=dict)
15
+ row_filters: dict[str, str] = Field(default_factory=dict)
16
+ masking_rules: dict[str, dict[str, MaskingRule]] = Field(default_factory=dict)
17
+
18
+ def summary(self) -> dict:
19
+ return {
20
+ "allowed_columns": {
21
+ table: sorted(columns) for table, columns in self.allowed_columns.items()
22
+ },
23
+ "blocked_columns": {
24
+ table: sorted(columns) for table, columns in self.blocked_columns.items()
25
+ },
26
+ "row_filters": dict(self.row_filters),
27
+ "masked_columns": {
28
+ f"{table}.{column}": rule.mode
29
+ for table, rules in self.masking_rules.items()
30
+ for column, rule in rules.items()
31
+ },
32
+ }
@@ -0,0 +1 @@
1
+ """Agent tool adapters."""
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def anthropic_tools() -> list[dict[str, Any]]:
7
+ return [
8
+ {
9
+ "name": "ask_database",
10
+ "description": "Ask a natural-language question of the database using QueryPilot safety checks.",
11
+ "input_schema": _object_schema(
12
+ {"question": {"type": "string", "description": "Question to answer."}},
13
+ ["question"],
14
+ ),
15
+ },
16
+ {
17
+ "name": "search_schema",
18
+ "description": "Search tables and columns relevant to a natural-language query.",
19
+ "input_schema": _object_schema(
20
+ {"query": {"type": "string", "description": "Schema search query."}},
21
+ ["query"],
22
+ ),
23
+ },
24
+ {
25
+ "name": "validate_sql",
26
+ "description": "Validate SQL and return the safe rewritten form when allowed.",
27
+ "input_schema": _object_schema(
28
+ {"sql": {"type": "string", "description": "SQL to validate."}},
29
+ ["sql"],
30
+ ),
31
+ },
32
+ {
33
+ "name": "execute_sql",
34
+ "description": "Validate and execute read-only SQL safely.",
35
+ "input_schema": _object_schema(
36
+ {"sql": {"type": "string", "description": "SQL to execute."}},
37
+ ["sql"],
38
+ ),
39
+ },
40
+ ]
41
+
42
+
43
+ def _object_schema(properties: dict[str, Any], required: list[str]) -> dict[str, Any]:
44
+ return {
45
+ "type": "object",
46
+ "properties": properties,
47
+ "required": required,
48
+ "additionalProperties": False,
49
+ }
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def openai_tools() -> list[dict[str, Any]]:
7
+ return [
8
+ _function_tool(
9
+ "ask_database",
10
+ "Ask a natural-language question of the database using QueryPilot safety checks.",
11
+ {"question": {"type": "string", "description": "Question to answer."}},
12
+ ["question"],
13
+ ),
14
+ _function_tool(
15
+ "search_schema",
16
+ "Search tables and columns relevant to a natural-language query.",
17
+ {"query": {"type": "string", "description": "Schema search query."}},
18
+ ["query"],
19
+ ),
20
+ _function_tool(
21
+ "validate_sql",
22
+ "Validate SQL and return the safe rewritten form when allowed.",
23
+ {"sql": {"type": "string", "description": "SQL to validate."}},
24
+ ["sql"],
25
+ ),
26
+ _function_tool(
27
+ "execute_sql",
28
+ "Validate and execute read-only SQL safely.",
29
+ {"sql": {"type": "string", "description": "SQL to execute."}},
30
+ ["sql"],
31
+ ),
32
+ ]
33
+
34
+
35
+ def _function_tool(
36
+ name: str,
37
+ description: str,
38
+ properties: dict[str, Any],
39
+ required: list[str],
40
+ ) -> dict[str, Any]:
41
+ return {
42
+ "type": "function",
43
+ "function": {
44
+ "name": name,
45
+ "description": description,
46
+ "parameters": {
47
+ "type": "object",
48
+ "properties": properties,
49
+ "required": required,
50
+ "additionalProperties": False,
51
+ },
52
+ },
53
+ }
@@ -0,0 +1,10 @@
1
+ from querypilot.audit.sinks import AuditSink, InMemoryAuditSink, JSONLAuditSink
2
+ from querypilot.audit.types import AuditMetadata, QueryAuditRecord
3
+
4
+ __all__ = [
5
+ "AuditMetadata",
6
+ "AuditSink",
7
+ "InMemoryAuditSink",
8
+ "JSONLAuditSink",
9
+ "QueryAuditRecord",
10
+ ]
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol
5
+
6
+ from querypilot.audit.types import QueryAuditRecord
7
+
8
+
9
+ class AuditSink(Protocol):
10
+ def write(self, record: QueryAuditRecord) -> None:
11
+ ...
12
+
13
+ def recent(self, limit: int = 100) -> list[QueryAuditRecord]:
14
+ ...
15
+
16
+
17
+ class InMemoryAuditSink:
18
+ def __init__(self) -> None:
19
+ self.records: list[QueryAuditRecord] = []
20
+
21
+ def write(self, record: QueryAuditRecord) -> None:
22
+ self.records.append(record)
23
+
24
+ def recent(self, limit: int = 100) -> list[QueryAuditRecord]:
25
+ return list(reversed(self.records[-limit:]))
26
+
27
+
28
+ class JSONLAuditSink:
29
+ def __init__(self, path: str | Path) -> None:
30
+ self.path = Path(path)
31
+ self._records: list[QueryAuditRecord] = []
32
+
33
+ def write(self, record: QueryAuditRecord) -> None:
34
+ self.path.parent.mkdir(parents=True, exist_ok=True)
35
+ with self.path.open("a", encoding="utf-8") as handle:
36
+ handle.write(record.model_dump_json() + "\n")
37
+ self._records.append(record)
38
+
39
+ def recent(self, limit: int = 100) -> list[QueryAuditRecord]:
40
+ if self._records:
41
+ return list(reversed(self._records[-limit:]))
42
+ if not self.path.exists():
43
+ return []
44
+ lines = self.path.read_text(encoding="utf-8").splitlines()[-limit:]
45
+ records = [QueryAuditRecord.model_validate_json(line) for line in lines]
46
+ return list(reversed(records))
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from uuid import uuid4
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from querypilot.core.types import ValidationResult
9
+
10
+
11
+ class AuditMetadata(BaseModel):
12
+ actor: str | None = None
13
+ session_id: str | None = None
14
+ app_name: str | None = None
15
+ trace_id: str | None = None
16
+
17
+
18
+ class QueryAuditRecord(BaseModel):
19
+ audit_id: str = Field(default_factory=lambda: str(uuid4()))
20
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
21
+ operation: str
22
+ question: str | None = None
23
+ sql: str | None = None
24
+ rewritten_sql: str | None = None
25
+ validation: ValidationResult | None = None
26
+ valid: bool | None = None
27
+ executed: bool = False
28
+ row_count: int | None = None
29
+ execution_time_ms: int | None = None
30
+ error: str | None = None
31
+ access_policy: dict = Field(default_factory=dict)
32
+ actor: str | None = None
33
+ session_id: str | None = None
34
+ app_name: str | None = None
35
+ trace_id: str | None = None
36
+
37
+ @classmethod
38
+ def create(
39
+ cls,
40
+ *,
41
+ operation: str,
42
+ metadata: AuditMetadata | None = None,
43
+ **kwargs,
44
+ ) -> "QueryAuditRecord":
45
+ metadata = metadata or AuditMetadata()
46
+ return cls(
47
+ operation=operation,
48
+ actor=metadata.actor,
49
+ session_id=metadata.session_id,
50
+ app_name=metadata.app_name,
51
+ trace_id=metadata.trace_id,
52
+ **kwargs,
53
+ )
querypilot/cli.py ADDED
@@ -0,0 +1,401 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from querypilot.access import AccessPolicy
10
+ from querypilot import QueryPilot
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ parser = argparse.ArgumentParser(prog="querypilot")
15
+ subparsers = parser.add_subparsers(dest="command", required=True)
16
+
17
+ serve_parser = subparsers.add_parser("serve", help="Run the FastAPI QueryPilot server.")
18
+ _add_runtime_args(serve_parser)
19
+ serve_parser.add_argument("--host", default=os.getenv("QUERYPILOT_HOST", "127.0.0.1"))
20
+ serve_parser.add_argument("--port", type=int, default=int(os.getenv("QUERYPILOT_PORT", "8000")))
21
+
22
+ mcp_parser = subparsers.add_parser("mcp", help="Run the QueryPilot MCP server.")
23
+ _add_runtime_args(mcp_parser)
24
+ mcp_parser.add_argument(
25
+ "--transport",
26
+ default=os.getenv("QUERYPILOT_MCP_TRANSPORT", "stdio"),
27
+ choices=["stdio", "streamable-http", "sse"],
28
+ )
29
+
30
+ eval_parser = subparsers.add_parser("eval", help="Eval-driven SQL reliability harness.")
31
+ eval_subparsers = eval_parser.add_subparsers(dest="eval_command", required=True)
32
+
33
+ eval_run_parser = eval_subparsers.add_parser(
34
+ "run",
35
+ help="Run a benchmark suite end-to-end and emit a SuiteReport.",
36
+ )
37
+ _add_eval_run_args(eval_run_parser)
38
+
39
+ eval_replay_parser = eval_subparsers.add_parser(
40
+ "replay",
41
+ help="Materialize a regression suite from an audit log.",
42
+ )
43
+ _add_eval_replay_args(eval_replay_parser)
44
+
45
+ eval_check_parser = eval_subparsers.add_parser(
46
+ "check",
47
+ help="Compare a SuiteReport JSON against thresholds and a baseline.",
48
+ )
49
+ _add_eval_check_args(eval_check_parser)
50
+
51
+ eval_init_parser = eval_subparsers.add_parser(
52
+ "init",
53
+ help="Scaffold suites/ and .eval/ in the current directory.",
54
+ )
55
+ eval_init_parser.add_argument(
56
+ "--target",
57
+ default=".",
58
+ help="Directory to scaffold into (default: current working directory).",
59
+ )
60
+ eval_init_parser.add_argument(
61
+ "--force",
62
+ action="store_true",
63
+ help="Overwrite existing files.",
64
+ )
65
+
66
+ args = parser.parse_args(argv)
67
+
68
+ if args.command == "serve":
69
+ _serve(args)
70
+ return 0
71
+ if args.command == "mcp":
72
+ _mcp(args)
73
+ return 0
74
+ if args.command == "eval":
75
+ if args.eval_command == "run":
76
+ return _eval_run(args)
77
+ if args.eval_command == "replay":
78
+ return _eval_replay(args)
79
+ if args.eval_command == "check":
80
+ return _eval_check(args)
81
+ if args.eval_command == "init":
82
+ return _eval_init(args)
83
+ raise SystemExit(f"Unknown eval subcommand: {args.eval_command}")
84
+ return 0
85
+
86
+
87
+ def _add_runtime_args(parser: argparse.ArgumentParser) -> None:
88
+ parser.add_argument("--database-url", default=os.getenv("QUERYPILOT_DATABASE_URL"))
89
+ parser.add_argument("--dialect", default=os.getenv("QUERYPILOT_DIALECT", "sqlite"))
90
+ parser.add_argument("--max-rows", type=int, default=int(os.getenv("QUERYPILOT_MAX_ROWS", "100")))
91
+ parser.add_argument(
92
+ "--timeout-seconds",
93
+ type=int,
94
+ default=int(os.getenv("QUERYPILOT_TIMEOUT_SECONDS", "10")),
95
+ )
96
+ parser.add_argument(
97
+ "--access-policy-json",
98
+ default=os.getenv("QUERYPILOT_ACCESS_POLICY_JSON"),
99
+ help="JSON object for AccessPolicy configuration.",
100
+ )
101
+
102
+
103
+ def _add_eval_replay_args(parser: argparse.ArgumentParser) -> None:
104
+ parser.add_argument(
105
+ "--audit-jsonl",
106
+ required=True,
107
+ help="Path to a JSONL audit log written by JSONLAuditSink.",
108
+ )
109
+ parser.add_argument(
110
+ "--fixture-db",
111
+ required=True,
112
+ help="Database URL to attach to every replayed case (e.g. sqlite:///fixtures/demo.db).",
113
+ )
114
+ parser.add_argument(
115
+ "--fixture-dialect",
116
+ default=None,
117
+ help="Override the dialect inferred from --fixture-db (sqlite/postgres/mysql/snowflake/bigquery/redshift).",
118
+ )
119
+ parser.add_argument(
120
+ "--output",
121
+ required=True,
122
+ help="Where to write the replayed suite (.yaml/.yml/.json).",
123
+ )
124
+ parser.add_argument(
125
+ "--name",
126
+ default="audit_replay",
127
+ help="Suite name to embed in the output file.",
128
+ )
129
+ parser.add_argument(
130
+ "--limit",
131
+ type=int,
132
+ default=1000,
133
+ help="Maximum number of cases to materialize.",
134
+ )
135
+ parser.add_argument(
136
+ "--include-masked",
137
+ action="store_true",
138
+ help="Include cases whose audit record applied a non-empty access policy.",
139
+ )
140
+ parser.add_argument(
141
+ "--include-failures",
142
+ action="store_true",
143
+ help="Include cases that failed validation, execution, or had errors.",
144
+ )
145
+ parser.add_argument(
146
+ "--include-empty",
147
+ action="store_true",
148
+ help="Include cases that returned zero rows.",
149
+ )
150
+ parser.add_argument(
151
+ "--tag",
152
+ action="append",
153
+ default=[],
154
+ help="Extra tag to attach to every replayed case (repeatable).",
155
+ )
156
+
157
+
158
+ def _add_eval_check_args(parser: argparse.ArgumentParser) -> None:
159
+ parser.add_argument(
160
+ "--report",
161
+ required=True,
162
+ help="Path to a SuiteReport JSON written by `querypilot eval run --report ...`.",
163
+ )
164
+ parser.add_argument(
165
+ "--baseline",
166
+ default=None,
167
+ help="Optional baseline SuiteReport JSON for regression comparison.",
168
+ )
169
+ parser.add_argument(
170
+ "--threshold",
171
+ type=float,
172
+ default=None,
173
+ help="Minimum overall pass_rate (0..1).",
174
+ )
175
+ parser.add_argument(
176
+ "--max-p95-ms",
177
+ type=int,
178
+ default=None,
179
+ help="Maximum p95 case latency in milliseconds.",
180
+ )
181
+ parser.add_argument(
182
+ "--require-safety",
183
+ type=float,
184
+ default=None,
185
+ help="Minimum safety_pass_rate (0..1).",
186
+ )
187
+ parser.add_argument(
188
+ "--require-correctness",
189
+ type=float,
190
+ default=None,
191
+ help="Minimum correctness_rate (0..1).",
192
+ )
193
+ parser.add_argument(
194
+ "--outcome-json",
195
+ default=None,
196
+ help="Optional path to write the structured CheckOutcome JSON.",
197
+ )
198
+
199
+
200
+ def _add_eval_run_args(parser: argparse.ArgumentParser) -> None:
201
+ parser.add_argument(
202
+ "--suite",
203
+ required=True,
204
+ help="Path to a suite YAML/JSON file or a directory of suite files.",
205
+ )
206
+ parser.add_argument(
207
+ "--database-url",
208
+ default=os.getenv("QUERYPILOT_DATABASE_URL"),
209
+ help="Default database URL when a case does not set fixture_db.",
210
+ )
211
+ parser.add_argument("--dialect", default=os.getenv("QUERYPILOT_DIALECT", "sqlite"))
212
+ parser.add_argument(
213
+ "--generator",
214
+ default="demo",
215
+ choices=("demo", "openai", "anthropic"),
216
+ help="SQL generator to evaluate. Use 'demo' for the offline deterministic generator.",
217
+ )
218
+ parser.add_argument(
219
+ "--model",
220
+ default=None,
221
+ help="Override the default model for openai/anthropic generators.",
222
+ )
223
+ parser.add_argument(
224
+ "--report",
225
+ default=None,
226
+ help="Path to write the JSON SuiteReport. Terminal output is always printed.",
227
+ )
228
+ parser.add_argument(
229
+ "--workers",
230
+ type=int,
231
+ default=1,
232
+ help="Parallel workers (>1 uses ThreadPoolExecutor; LLM rate limits apply).",
233
+ )
234
+ parser.add_argument(
235
+ "--max-rows",
236
+ type=int,
237
+ default=int(os.getenv("QUERYPILOT_MAX_ROWS", "100")),
238
+ )
239
+ parser.add_argument(
240
+ "--timeout-seconds",
241
+ type=int,
242
+ default=int(os.getenv("QUERYPILOT_TIMEOUT_SECONDS", "10")),
243
+ )
244
+ parser.add_argument("--no-color", action="store_true", help="Disable ANSI colors in terminal output.")
245
+
246
+
247
+ def _serve(args: argparse.Namespace) -> None:
248
+ if not args.database_url:
249
+ raise SystemExit("--database-url or QUERYPILOT_DATABASE_URL is required.")
250
+ try:
251
+ import uvicorn
252
+ except ImportError as exc:
253
+ raise SystemExit("Install querypilot[server] to use `querypilot serve`.") from exc
254
+
255
+ from querypilot.server.app import create_app
256
+
257
+ app = create_app(
258
+ database_url=args.database_url,
259
+ dialect=args.dialect,
260
+ max_rows=args.max_rows,
261
+ timeout_seconds=args.timeout_seconds,
262
+ access_policy=_access_policy_from_args(args),
263
+ )
264
+ uvicorn.run(app, host=args.host, port=args.port)
265
+
266
+
267
+ def _mcp(args: argparse.Namespace) -> None:
268
+ if not args.database_url:
269
+ raise SystemExit("--database-url or QUERYPILOT_DATABASE_URL is required.")
270
+
271
+ from querypilot.mcp.server import create_mcp_server
272
+
273
+ qp = QueryPilot.connect(
274
+ database_url=args.database_url,
275
+ dialect=args.dialect,
276
+ max_rows=args.max_rows,
277
+ timeout_seconds=args.timeout_seconds,
278
+ access_policy=_access_policy_from_args(args),
279
+ )
280
+ server = create_mcp_server(qp)
281
+ server.run(transport=args.transport)
282
+
283
+
284
+ def _eval_run(args: argparse.Namespace) -> int:
285
+ from querypilot.evals.factory import (
286
+ build_cost_tracker_factory,
287
+ build_generator,
288
+ build_qp_factory,
289
+ load_suite_or_dir,
290
+ )
291
+ from querypilot.evals.report import render_terminal, write_json
292
+ from querypilot.evals.suite_runner import run_suite
293
+
294
+ try:
295
+ suite = load_suite_or_dir(args.suite)
296
+ except Exception as exc:
297
+ raise SystemExit(f"Failed to load suite at {args.suite}: {exc}") from exc
298
+
299
+ if not args.database_url and suite.fixture_db is None:
300
+ raise SystemExit(
301
+ "--database-url is required when the suite does not declare fixture_db."
302
+ )
303
+
304
+ generator = build_generator(args.generator, model=args.model)
305
+ tracker_factory = build_cost_tracker_factory(args.generator)
306
+ qp_factory = build_qp_factory(
307
+ database_url=args.database_url or suite.fixture_db or "",
308
+ dialect=args.dialect,
309
+ generator=generator,
310
+ max_rows=args.max_rows,
311
+ timeout_seconds=args.timeout_seconds,
312
+ )
313
+
314
+ report = run_suite(
315
+ suite,
316
+ qp_factory=qp_factory,
317
+ cost_tracker_factory=tracker_factory,
318
+ max_workers=args.workers,
319
+ generator_name=args.generator,
320
+ model_name=args.model,
321
+ database_url=args.database_url,
322
+ )
323
+
324
+ if args.report:
325
+ write_json(report, args.report)
326
+
327
+ print(render_terminal(report, color=not args.no_color))
328
+ return 0
329
+
330
+
331
+ def _eval_check(args: argparse.Namespace) -> int:
332
+ from querypilot.evals.check import (
333
+ check_report,
334
+ format_outcome,
335
+ load_report,
336
+ write_outcome,
337
+ )
338
+
339
+ report = load_report(args.report)
340
+ baseline = load_report(args.baseline) if args.baseline else None
341
+
342
+ outcome = check_report(
343
+ report,
344
+ baseline=baseline,
345
+ threshold=args.threshold,
346
+ max_p95_ms=args.max_p95_ms,
347
+ require_safety_pass_rate=args.require_safety,
348
+ require_correctness_rate=args.require_correctness,
349
+ )
350
+
351
+ if args.outcome_json:
352
+ write_outcome(outcome, args.outcome_json)
353
+
354
+ print(format_outcome(outcome))
355
+ return 0 if outcome.ok else 1
356
+
357
+
358
+ def _eval_init(args: argparse.Namespace) -> int:
359
+ from querypilot.evals.init import scaffold
360
+
361
+ target = Path(args.target).resolve()
362
+ written = scaffold(target, force=args.force)
363
+ print(f"Scaffolded {len(written)} files in {target}")
364
+ for path in written:
365
+ print(f" {path.relative_to(target)}")
366
+ return 0
367
+
368
+
369
+ def _eval_replay(args: argparse.Namespace) -> int:
370
+ from querypilot.evals.loader import write_suite
371
+ from querypilot.evals.replay import replay_from_jsonl
372
+
373
+ suite = replay_from_jsonl(
374
+ args.audit_jsonl,
375
+ fixture_db=args.fixture_db,
376
+ fixture_dialect=args.fixture_dialect,
377
+ suite_name=args.name,
378
+ only_successful=not args.include_failures,
379
+ skip_masked=not args.include_masked,
380
+ skip_empty_results=not args.include_empty,
381
+ limit=args.limit,
382
+ extra_tags=args.tag,
383
+ )
384
+
385
+ target = write_suite(suite, args.output)
386
+ print(f"Wrote {len(suite.cases)} cases to {target}")
387
+ return 0
388
+
389
+
390
+ def _access_policy_from_args(args: argparse.Namespace) -> AccessPolicy | None:
391
+ if not args.access_policy_json:
392
+ return None
393
+ try:
394
+ payload = json.loads(args.access_policy_json)
395
+ except json.JSONDecodeError as exc:
396
+ raise SystemExit(f"Invalid --access-policy-json: {exc}") from exc
397
+ return AccessPolicy.model_validate(payload)
398
+
399
+
400
+ if __name__ == "__main__":
401
+ sys.exit(main())
@@ -0,0 +1,5 @@
1
+ from querypilot.connectors.base import BaseConnector
2
+ from querypilot.connectors.postgres import PostgresConnector
3
+ from querypilot.connectors.sqlite import SQLiteConnector
4
+
5
+ __all__ = ["BaseConnector", "PostgresConnector", "SQLiteConnector"]