onetool-mcp 1.0.0b1__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.
- bench/__init__.py +5 -0
- bench/cli.py +69 -0
- bench/harness/__init__.py +66 -0
- bench/harness/client.py +692 -0
- bench/harness/config.py +397 -0
- bench/harness/csv_writer.py +109 -0
- bench/harness/evaluate.py +512 -0
- bench/harness/metrics.py +283 -0
- bench/harness/runner.py +899 -0
- bench/py.typed +0 -0
- bench/reporter.py +629 -0
- bench/run.py +487 -0
- bench/secrets.py +101 -0
- bench/utils.py +16 -0
- onetool/__init__.py +4 -0
- onetool/cli.py +391 -0
- onetool/py.typed +0 -0
- onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
- onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
- onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
- onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
- ot/__init__.py +37 -0
- ot/__main__.py +6 -0
- ot/_cli.py +107 -0
- ot/_tui.py +53 -0
- ot/config/__init__.py +46 -0
- ot/config/defaults/bench.yaml +4 -0
- ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
- ot/config/defaults/diagram-templates/c4-context.puml +30 -0
- ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
- ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
- ot/config/defaults/diagram-templates/microservices.d2 +81 -0
- ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
- ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
- ot/config/defaults/onetool.yaml +25 -0
- ot/config/defaults/prompts.yaml +97 -0
- ot/config/defaults/servers.yaml +7 -0
- ot/config/defaults/snippets.yaml +4 -0
- ot/config/defaults/tool_templates/__init__.py +7 -0
- ot/config/defaults/tool_templates/extension.py +52 -0
- ot/config/defaults/tool_templates/isolated.py +61 -0
- ot/config/dynamic.py +121 -0
- ot/config/global_templates/__init__.py +2 -0
- ot/config/global_templates/bench-secrets-template.yaml +6 -0
- ot/config/global_templates/bench.yaml +9 -0
- ot/config/global_templates/onetool.yaml +27 -0
- ot/config/global_templates/secrets-template.yaml +44 -0
- ot/config/global_templates/servers.yaml +18 -0
- ot/config/global_templates/snippets.yaml +235 -0
- ot/config/loader.py +1087 -0
- ot/config/mcp.py +145 -0
- ot/config/secrets.py +190 -0
- ot/config/tool_config.py +125 -0
- ot/decorators.py +116 -0
- ot/executor/__init__.py +35 -0
- ot/executor/base.py +16 -0
- ot/executor/fence_processor.py +83 -0
- ot/executor/linter.py +142 -0
- ot/executor/pack_proxy.py +260 -0
- ot/executor/param_resolver.py +140 -0
- ot/executor/pep723.py +288 -0
- ot/executor/result_store.py +369 -0
- ot/executor/runner.py +496 -0
- ot/executor/simple.py +163 -0
- ot/executor/tool_loader.py +396 -0
- ot/executor/validator.py +398 -0
- ot/executor/worker_pool.py +388 -0
- ot/executor/worker_proxy.py +189 -0
- ot/http_client.py +145 -0
- ot/logging/__init__.py +37 -0
- ot/logging/config.py +315 -0
- ot/logging/entry.py +213 -0
- ot/logging/format.py +188 -0
- ot/logging/span.py +349 -0
- ot/meta.py +1555 -0
- ot/paths.py +453 -0
- ot/prompts.py +218 -0
- ot/proxy/__init__.py +21 -0
- ot/proxy/manager.py +396 -0
- ot/py.typed +0 -0
- ot/registry/__init__.py +189 -0
- ot/registry/models.py +57 -0
- ot/registry/parser.py +269 -0
- ot/registry/registry.py +413 -0
- ot/server.py +315 -0
- ot/shortcuts/__init__.py +15 -0
- ot/shortcuts/aliases.py +87 -0
- ot/shortcuts/snippets.py +258 -0
- ot/stats/__init__.py +35 -0
- ot/stats/html.py +250 -0
- ot/stats/jsonl_writer.py +283 -0
- ot/stats/reader.py +354 -0
- ot/stats/timing.py +57 -0
- ot/support.py +63 -0
- ot/tools.py +114 -0
- ot/utils/__init__.py +81 -0
- ot/utils/batch.py +161 -0
- ot/utils/cache.py +120 -0
- ot/utils/deps.py +403 -0
- ot/utils/exceptions.py +23 -0
- ot/utils/factory.py +179 -0
- ot/utils/format.py +65 -0
- ot/utils/http.py +202 -0
- ot/utils/platform.py +45 -0
- ot/utils/sanitize.py +130 -0
- ot/utils/truncate.py +69 -0
- ot_tools/__init__.py +4 -0
- ot_tools/_convert/__init__.py +12 -0
- ot_tools/_convert/excel.py +279 -0
- ot_tools/_convert/pdf.py +254 -0
- ot_tools/_convert/powerpoint.py +268 -0
- ot_tools/_convert/utils.py +358 -0
- ot_tools/_convert/word.py +283 -0
- ot_tools/brave_search.py +604 -0
- ot_tools/code_search.py +736 -0
- ot_tools/context7.py +495 -0
- ot_tools/convert.py +614 -0
- ot_tools/db.py +415 -0
- ot_tools/diagram.py +1604 -0
- ot_tools/diagram.yaml +167 -0
- ot_tools/excel.py +1372 -0
- ot_tools/file.py +1348 -0
- ot_tools/firecrawl.py +732 -0
- ot_tools/grounding_search.py +646 -0
- ot_tools/package.py +604 -0
- ot_tools/py.typed +0 -0
- ot_tools/ripgrep.py +544 -0
- ot_tools/scaffold.py +471 -0
- ot_tools/transform.py +213 -0
- ot_tools/web_fetch.py +384 -0
ot_tools/db.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
"""Database introspection and query execution tool.
|
|
5
|
+
|
|
6
|
+
Provides SQL database access via SQLAlchemy. Supports any SQLAlchemy-compatible
|
|
7
|
+
database (PostgreSQL, MySQL, SQLite, Oracle, MS SQL Server, etc.).
|
|
8
|
+
|
|
9
|
+
Based on mcp-alchemy by Rui Machado (MPL 2.0).
|
|
10
|
+
https://github.com/runekaagaard/mcp-alchemy
|
|
11
|
+
|
|
12
|
+
Requires explicit db_url parameter for all operations.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
# Get db_url from project config
|
|
16
|
+
db.tables(db_url=proj.attr("myproject", "db_url"))
|
|
17
|
+
|
|
18
|
+
# Or use literal URL
|
|
19
|
+
db.tables(db_url="sqlite:///path/to/database.db")
|
|
20
|
+
db.query("SELECT 1", db_url="postgresql://user:pass@localhost/dbname")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
# Pack for dot notation: db.tables(), db.schema(), db.query()
|
|
26
|
+
pack = "db"
|
|
27
|
+
|
|
28
|
+
__all__ = ["query", "schema", "tables"]
|
|
29
|
+
|
|
30
|
+
import contextlib
|
|
31
|
+
import threading
|
|
32
|
+
from collections import OrderedDict
|
|
33
|
+
from datetime import date, datetime
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from pydantic import BaseModel, Field
|
|
37
|
+
from sqlalchemy import Engine, create_engine, inspect, text
|
|
38
|
+
|
|
39
|
+
from ot.config import get_tool_config
|
|
40
|
+
from ot.logging import LogSpan
|
|
41
|
+
from ot.paths import resolve_cwd_path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Config(BaseModel):
|
|
45
|
+
"""Pack configuration - discovered by registry."""
|
|
46
|
+
|
|
47
|
+
max_chars: int = Field(
|
|
48
|
+
default=4000,
|
|
49
|
+
ge=100,
|
|
50
|
+
le=100000,
|
|
51
|
+
description="Maximum characters in query result output",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_config() -> Config:
|
|
56
|
+
"""Get db pack configuration."""
|
|
57
|
+
return get_tool_config("db", Config)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Connection pool keyed by URL - persists across calls in process
|
|
61
|
+
# Uses OrderedDict for LRU eviction with bounded size
|
|
62
|
+
_ENGINES_MAXSIZE = 8
|
|
63
|
+
_engines_lock = threading.Lock()
|
|
64
|
+
_engines: OrderedDict[str, Engine] = OrderedDict()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _resolve_sqlite_url(db_url: str) -> str:
|
|
68
|
+
"""Resolve relative paths in SQLite URLs.
|
|
69
|
+
|
|
70
|
+
SQLite URLs use the format sqlite:///path/to/db.
|
|
71
|
+
If the path is relative, resolve it against the project working directory.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
db_url: Database URL string
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
URL with resolved path if SQLite with relative path, otherwise unchanged
|
|
78
|
+
"""
|
|
79
|
+
if not db_url.startswith("sqlite:///"):
|
|
80
|
+
return db_url
|
|
81
|
+
|
|
82
|
+
# Extract path from sqlite:///path
|
|
83
|
+
path = db_url[10:] # len("sqlite:///") == 10
|
|
84
|
+
|
|
85
|
+
# Skip in-memory databases and absolute paths
|
|
86
|
+
if not path or path == ":memory:" or path.startswith("/"):
|
|
87
|
+
return db_url
|
|
88
|
+
|
|
89
|
+
# Resolve relative path against project directory
|
|
90
|
+
resolved = resolve_cwd_path(path)
|
|
91
|
+
return f"sqlite:///{resolved}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _create_engine(db_url: str) -> Engine:
|
|
95
|
+
"""Create SQLAlchemy engine with MCP-optimized settings."""
|
|
96
|
+
# MCP-optimized defaults
|
|
97
|
+
options: dict[str, Any] = {
|
|
98
|
+
"isolation_level": "AUTOCOMMIT",
|
|
99
|
+
"pool_pre_ping": True, # Test connections before use
|
|
100
|
+
"pool_size": 1, # Single connection for MCP patterns
|
|
101
|
+
"max_overflow": 2, # Allow temporary burst capacity
|
|
102
|
+
"pool_recycle": 3600, # Refresh connections older than 1hr
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return create_engine(db_url, **options)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_engine(db_url: str) -> Engine:
|
|
109
|
+
"""Get or create engine for given URL with retry logic."""
|
|
110
|
+
# Resolve relative paths in SQLite URLs
|
|
111
|
+
resolved_url = _resolve_sqlite_url(db_url)
|
|
112
|
+
|
|
113
|
+
# Fast path: check cache with lock
|
|
114
|
+
with _engines_lock:
|
|
115
|
+
if resolved_url in _engines:
|
|
116
|
+
# LRU: move to end on access
|
|
117
|
+
_engines.move_to_end(resolved_url)
|
|
118
|
+
return _engines[resolved_url]
|
|
119
|
+
|
|
120
|
+
# Create engine outside lock (slow operation)
|
|
121
|
+
with LogSpan(span="db.connect", db_url=resolved_url) as span:
|
|
122
|
+
try:
|
|
123
|
+
engine = _create_engine(resolved_url)
|
|
124
|
+
except Exception:
|
|
125
|
+
span.add(retry=True)
|
|
126
|
+
# One retry with fresh engine
|
|
127
|
+
engine = _create_engine(resolved_url)
|
|
128
|
+
|
|
129
|
+
# Double-check after acquiring lock
|
|
130
|
+
with _engines_lock:
|
|
131
|
+
if resolved_url in _engines:
|
|
132
|
+
# Another thread created it while we were waiting
|
|
133
|
+
engine.dispose()
|
|
134
|
+
_engines.move_to_end(resolved_url)
|
|
135
|
+
return _engines[resolved_url]
|
|
136
|
+
|
|
137
|
+
_engines[resolved_url] = engine
|
|
138
|
+
|
|
139
|
+
# LRU eviction: dispose oldest engine when over maxsize
|
|
140
|
+
while len(_engines) > _ENGINES_MAXSIZE:
|
|
141
|
+
_, oldest_engine = _engines.popitem(last=False)
|
|
142
|
+
with contextlib.suppress(Exception):
|
|
143
|
+
oldest_engine.dispose()
|
|
144
|
+
|
|
145
|
+
span.add(cached=False)
|
|
146
|
+
return engine
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _format_value(val: Any) -> str:
|
|
150
|
+
"""Format a value for display."""
|
|
151
|
+
if val is None:
|
|
152
|
+
return "NULL"
|
|
153
|
+
if isinstance(val, (datetime, date)):
|
|
154
|
+
return val.isoformat()
|
|
155
|
+
return str(val)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def tables(
|
|
159
|
+
*, db_url: str, filter: str | None = None, ignore_case: bool = False
|
|
160
|
+
) -> str:
|
|
161
|
+
"""List table names in the database.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
db_url: Database URL (required)
|
|
165
|
+
filter: Optional substring to filter table names
|
|
166
|
+
ignore_case: If True, filter matching is case-insensitive
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Comma-separated list of table names
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
# List all tables
|
|
173
|
+
db.tables(db_url=proj.attr("myproject", "db_url"))
|
|
174
|
+
|
|
175
|
+
# Filter tables containing "user"
|
|
176
|
+
db.tables(db_url="sqlite:///data.db", filter="user")
|
|
177
|
+
|
|
178
|
+
# Case-insensitive filter
|
|
179
|
+
db.tables(db_url="sqlite:///data.db", filter="USER", ignore_case=True)
|
|
180
|
+
"""
|
|
181
|
+
with LogSpan(span="db.tables", db_url=db_url, filter=filter) as s:
|
|
182
|
+
if not db_url or not db_url.strip():
|
|
183
|
+
s.add(error="empty_db_url")
|
|
184
|
+
return "Error: db_url parameter is required"
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
engine = _get_engine(db_url)
|
|
188
|
+
with engine.connect() as conn:
|
|
189
|
+
inspector = inspect(conn)
|
|
190
|
+
all_tables = inspector.get_table_names()
|
|
191
|
+
|
|
192
|
+
if filter:
|
|
193
|
+
if ignore_case:
|
|
194
|
+
filter_lower = filter.lower()
|
|
195
|
+
all_tables = [t for t in all_tables if filter_lower in t.lower()]
|
|
196
|
+
else:
|
|
197
|
+
all_tables = [t for t in all_tables if filter in t]
|
|
198
|
+
|
|
199
|
+
s.add(resultCount=len(all_tables))
|
|
200
|
+
return ", ".join(all_tables) if all_tables else "No tables found"
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
s.add(error=str(e))
|
|
204
|
+
return f"Error: {e}"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def schema(*, table_names: list[str], db_url: str) -> str:
|
|
208
|
+
"""Get schema definitions for specified tables.
|
|
209
|
+
|
|
210
|
+
Returns column names, types, primary keys, and foreign key relationships.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
table_names: List of table names to inspect
|
|
214
|
+
db_url: Database URL (required)
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Formatted schema definitions
|
|
218
|
+
|
|
219
|
+
Example:
|
|
220
|
+
# Single table
|
|
221
|
+
db.schema(table_names=["users"], db_url=ot.project("myproject", attr="db_url"))
|
|
222
|
+
|
|
223
|
+
# Multiple tables
|
|
224
|
+
db.schema(table_names=["users", "orders"], db_url="sqlite:///data.db")
|
|
225
|
+
"""
|
|
226
|
+
with LogSpan(span="db.schema", tables=table_names, db_url=db_url) as s:
|
|
227
|
+
if not db_url or not db_url.strip():
|
|
228
|
+
s.add(error="empty_db_url")
|
|
229
|
+
return "Error: db_url parameter is required"
|
|
230
|
+
|
|
231
|
+
if not table_names:
|
|
232
|
+
s.add(error="no_tables")
|
|
233
|
+
return "Error: table_names parameter is required"
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
engine = _get_engine(db_url)
|
|
237
|
+
with engine.connect() as conn:
|
|
238
|
+
inspector = inspect(conn)
|
|
239
|
+
results: list[str] = []
|
|
240
|
+
|
|
241
|
+
for table_name in table_names:
|
|
242
|
+
results.append(_format_table_schema(inspector, table_name))
|
|
243
|
+
|
|
244
|
+
s.add(resultCount=len(table_names))
|
|
245
|
+
return "\n".join(results)
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
s.add(error=str(e))
|
|
249
|
+
return f"Error: {e}"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _format_table_schema(inspector: Any, table_name: str) -> str:
|
|
253
|
+
"""Format schema for a single table."""
|
|
254
|
+
try:
|
|
255
|
+
columns = inspector.get_columns(table_name)
|
|
256
|
+
except Exception:
|
|
257
|
+
return f"{table_name}: [table not found]"
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
foreign_keys = inspector.get_foreign_keys(table_name)
|
|
261
|
+
pk_constraint = inspector.get_pk_constraint(table_name)
|
|
262
|
+
except Exception:
|
|
263
|
+
foreign_keys = []
|
|
264
|
+
pk_constraint = {}
|
|
265
|
+
|
|
266
|
+
primary_keys = set(pk_constraint.get("constrained_columns", []))
|
|
267
|
+
|
|
268
|
+
result = [f"{table_name}:"]
|
|
269
|
+
|
|
270
|
+
# Process columns - use explicit key access to avoid mutating the dict
|
|
271
|
+
show_key_only = {"nullable", "autoincrement"}
|
|
272
|
+
skip_keys = {"name", "type", "comment"}
|
|
273
|
+
for column in columns:
|
|
274
|
+
name = column["name"]
|
|
275
|
+
col_type = str(column["type"])
|
|
276
|
+
|
|
277
|
+
parts = []
|
|
278
|
+
if name in primary_keys:
|
|
279
|
+
parts.append("primary key")
|
|
280
|
+
parts.append(col_type)
|
|
281
|
+
|
|
282
|
+
for k, v in column.items():
|
|
283
|
+
if k in skip_keys:
|
|
284
|
+
continue
|
|
285
|
+
if v:
|
|
286
|
+
if k in show_key_only:
|
|
287
|
+
parts.append(k)
|
|
288
|
+
else:
|
|
289
|
+
parts.append(f"{k}={v}")
|
|
290
|
+
|
|
291
|
+
result.append(f" {name}: " + ", ".join(parts))
|
|
292
|
+
|
|
293
|
+
# Process relationships
|
|
294
|
+
if foreign_keys:
|
|
295
|
+
result.extend(["", " Relationships:"])
|
|
296
|
+
for fk in foreign_keys:
|
|
297
|
+
constrained = ", ".join(fk["constrained_columns"])
|
|
298
|
+
referred_table = fk["referred_table"]
|
|
299
|
+
referred_cols = ", ".join(fk["referred_columns"])
|
|
300
|
+
result.append(f" {constrained} -> {referred_table}.{referred_cols}")
|
|
301
|
+
|
|
302
|
+
return "\n".join(result)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def query(*, sql: str, db_url: str, params: dict[str, Any] | None = None) -> str:
|
|
306
|
+
"""Execute a SQL query and return formatted results.
|
|
307
|
+
|
|
308
|
+
IMPORTANT: Always use the params parameter for variable substitution
|
|
309
|
+
(e.g., 'WHERE id = :id' with params={'id': 123}) to prevent SQL injection.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
sql: SQL query to execute
|
|
313
|
+
db_url: Database URL (required)
|
|
314
|
+
params: Query parameters for safe substitution
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Formatted query results or error message
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
# Basic query
|
|
321
|
+
db.query(sql="SELECT * FROM users LIMIT 5", db_url=ot.project("myproject", attr="db_url"))
|
|
322
|
+
|
|
323
|
+
# Parameterized query (safe from SQL injection)
|
|
324
|
+
db.query(
|
|
325
|
+
sql="SELECT * FROM users WHERE status = :status",
|
|
326
|
+
db_url="sqlite:///data.db",
|
|
327
|
+
params={"status": "active"}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# INSERT/UPDATE/DELETE
|
|
331
|
+
db.query(
|
|
332
|
+
sql="UPDATE users SET status = :status WHERE id = :id",
|
|
333
|
+
db_url="postgresql://user:pass@localhost/db",
|
|
334
|
+
params={"status": "inactive", "id": 123}
|
|
335
|
+
)
|
|
336
|
+
"""
|
|
337
|
+
with LogSpan(span="db.query", sql=sql, db_url=db_url) as s:
|
|
338
|
+
if not db_url or not db_url.strip():
|
|
339
|
+
s.add(error="empty_db_url")
|
|
340
|
+
return "Error: db_url parameter is required"
|
|
341
|
+
|
|
342
|
+
if not sql or not sql.strip():
|
|
343
|
+
s.add(error="empty_query")
|
|
344
|
+
return "Error: sql parameter is required"
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
engine = _get_engine(db_url)
|
|
348
|
+
with engine.connect() as conn:
|
|
349
|
+
cursor_result = conn.execute(text(sql), params or {})
|
|
350
|
+
|
|
351
|
+
if not cursor_result.returns_rows:
|
|
352
|
+
affected = cursor_result.rowcount
|
|
353
|
+
s.add(rowsAffected=affected)
|
|
354
|
+
return f"Success: {affected} rows affected"
|
|
355
|
+
|
|
356
|
+
max_chars = _get_config().max_chars
|
|
357
|
+
output, row_count, truncated = _format_query_results(
|
|
358
|
+
cursor_result, max_chars
|
|
359
|
+
)
|
|
360
|
+
s.add(rows=row_count, truncated=truncated)
|
|
361
|
+
return output
|
|
362
|
+
|
|
363
|
+
except Exception as e:
|
|
364
|
+
s.add(error=str(e))
|
|
365
|
+
return f"Error: {e}"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _format_query_results(cursor_result: Any, max_chars: int) -> tuple[str, int, bool]:
|
|
369
|
+
"""Format query results in vertical format.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
cursor_result: SQLAlchemy cursor result
|
|
373
|
+
max_chars: Maximum characters for output
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Tuple of (formatted_output, row_count, was_truncated)
|
|
377
|
+
"""
|
|
378
|
+
result: list[str] = []
|
|
379
|
+
size = 0
|
|
380
|
+
row_count = 0
|
|
381
|
+
displayed_count = 0
|
|
382
|
+
truncated = False
|
|
383
|
+
keys = list(cursor_result.keys())
|
|
384
|
+
|
|
385
|
+
while row := cursor_result.fetchone():
|
|
386
|
+
row_count += 1
|
|
387
|
+
|
|
388
|
+
if truncated:
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
sub_result = [f"{row_count}. row"]
|
|
392
|
+
for col, val in zip(keys, row, strict=True):
|
|
393
|
+
sub_result.append(f"{col}: {_format_value(val)}")
|
|
394
|
+
sub_result.append("")
|
|
395
|
+
|
|
396
|
+
row_size = sum(len(x) + 1 for x in sub_result)
|
|
397
|
+
size += row_size
|
|
398
|
+
|
|
399
|
+
if size > max_chars:
|
|
400
|
+
truncated = True
|
|
401
|
+
else:
|
|
402
|
+
displayed_count += 1
|
|
403
|
+
result.extend(sub_result)
|
|
404
|
+
|
|
405
|
+
if row_count == 0:
|
|
406
|
+
return "No rows returned", 0, False
|
|
407
|
+
|
|
408
|
+
if truncated:
|
|
409
|
+
result.append(
|
|
410
|
+
f"Result: showing first {displayed_count} of {row_count} rows (output truncated)"
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
result.append(f"Result: {row_count} rows")
|
|
414
|
+
|
|
415
|
+
return "\n".join(result), row_count, truncated
|