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/utils/batch.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Batch processing utilities for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides concurrent execution helpers for tools that process multiple items.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
from ot.utils import batch_execute, normalize_items
|
|
7
|
+
|
|
8
|
+
# Process URLs concurrently
|
|
9
|
+
def fetch_one(url: str, label: str) -> tuple[str, str]:
|
|
10
|
+
result = fetch(url)
|
|
11
|
+
return label, result
|
|
12
|
+
|
|
13
|
+
urls = ["https://a.com", ("https://b.com", "Custom Label")]
|
|
14
|
+
normalized = normalize_items(urls) # [(url, label), ...]
|
|
15
|
+
results = batch_execute(fetch_one, normalized, max_workers=5)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
21
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
|
|
26
|
+
__all__ = ["batch_execute", "format_batch_results", "normalize_items"]
|
|
27
|
+
|
|
28
|
+
T = TypeVar("T")
|
|
29
|
+
R = TypeVar("R")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize_items(
|
|
33
|
+
items: list[str] | list[tuple[str, str]],
|
|
34
|
+
) -> list[tuple[str, str]]:
|
|
35
|
+
"""Normalize a list of items to (value, label) tuples.
|
|
36
|
+
|
|
37
|
+
Accepts items as either:
|
|
38
|
+
- A string (used as both value and label)
|
|
39
|
+
- A tuple of (value, label)
|
|
40
|
+
|
|
41
|
+
This is the standard pattern for batch operations in OneTool,
|
|
42
|
+
allowing users to provide custom labels for results.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
items: List of items as strings or (value, label) tuples
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of (value, label) tuples
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
# Simple list
|
|
52
|
+
normalize_items(["a", "b", "c"])
|
|
53
|
+
# Returns: [("a", "a"), ("b", "b"), ("c", "c")]
|
|
54
|
+
|
|
55
|
+
# Mixed list
|
|
56
|
+
normalize_items(["a", ("b", "Custom B")])
|
|
57
|
+
# Returns: [("a", "a"), ("b", "Custom B")]
|
|
58
|
+
|
|
59
|
+
# Labeled list
|
|
60
|
+
normalize_items([
|
|
61
|
+
("https://example.com", "Example"),
|
|
62
|
+
("https://docs.python.org", "Python Docs"),
|
|
63
|
+
])
|
|
64
|
+
# Returns: [("https://example.com", "Example"), ...]
|
|
65
|
+
"""
|
|
66
|
+
normalized: list[tuple[str, str]] = []
|
|
67
|
+
for item in items:
|
|
68
|
+
if isinstance(item, str):
|
|
69
|
+
normalized.append((item, item))
|
|
70
|
+
else:
|
|
71
|
+
normalized.append(item)
|
|
72
|
+
return normalized
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def batch_execute(
|
|
76
|
+
func: Callable[[str, str], tuple[str, R]],
|
|
77
|
+
items: list[tuple[str, str]],
|
|
78
|
+
*,
|
|
79
|
+
max_workers: int | None = None,
|
|
80
|
+
preserve_order: bool = True,
|
|
81
|
+
) -> dict[str, R]:
|
|
82
|
+
"""Execute a function concurrently on multiple items.
|
|
83
|
+
|
|
84
|
+
Runs the provided function on each item using a ThreadPoolExecutor.
|
|
85
|
+
The function receives (value, label) and must return (label, result).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
func: Function taking (value: str, label: str) and returning (label, result)
|
|
89
|
+
items: List of (value, label) tuples (use normalize_items to prepare)
|
|
90
|
+
max_workers: Maximum concurrent workers. Defaults to len(items) (up to 10)
|
|
91
|
+
preserve_order: If True (default), results maintain input order
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dict mapping labels to results
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
def search_one(query: str, label: str) -> tuple[str, str]:
|
|
98
|
+
result = some_search(query)
|
|
99
|
+
return label, result
|
|
100
|
+
|
|
101
|
+
items = normalize_items(["query1", ("query2", "Custom")])
|
|
102
|
+
results = batch_execute(search_one, items, max_workers=5)
|
|
103
|
+
# results = {"query1": ..., "Custom": ...}
|
|
104
|
+
"""
|
|
105
|
+
if not items:
|
|
106
|
+
return {}
|
|
107
|
+
|
|
108
|
+
if max_workers is None:
|
|
109
|
+
max_workers = min(len(items), 10)
|
|
110
|
+
|
|
111
|
+
results: dict[str, R] = {}
|
|
112
|
+
|
|
113
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
114
|
+
futures = {
|
|
115
|
+
executor.submit(func, value, label): label for value, label in items
|
|
116
|
+
}
|
|
117
|
+
for future in as_completed(futures):
|
|
118
|
+
label, result = future.result()
|
|
119
|
+
results[label] = result
|
|
120
|
+
|
|
121
|
+
if preserve_order:
|
|
122
|
+
# Rebuild dict in original order
|
|
123
|
+
ordered: dict[str, R] = {}
|
|
124
|
+
for _, label in items:
|
|
125
|
+
if label in results:
|
|
126
|
+
ordered[label] = results[label]
|
|
127
|
+
return ordered
|
|
128
|
+
|
|
129
|
+
return results
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def format_batch_results(
|
|
133
|
+
results: dict[str, Any],
|
|
134
|
+
items: list[tuple[str, str]],
|
|
135
|
+
separator: str = "===",
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Format batch results as labeled sections.
|
|
138
|
+
|
|
139
|
+
Creates a formatted string with section headers for each result,
|
|
140
|
+
preserving the original order from the items list.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
results: Dict mapping labels to result strings
|
|
144
|
+
items: Original list of (value, label) tuples for ordering
|
|
145
|
+
separator: Section separator character(s) (default: "===")
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Formatted string with sections like "=== Label ===\\n{content}"
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
results = {"A": "content a", "B": "content b"}
|
|
152
|
+
items = [("x", "A"), ("y", "B")]
|
|
153
|
+
output = format_batch_results(results, items)
|
|
154
|
+
# "=== A ===\\ncontent a\\n\\n=== B ===\\ncontent b"
|
|
155
|
+
"""
|
|
156
|
+
sections = []
|
|
157
|
+
for _, label in items:
|
|
158
|
+
if label in results:
|
|
159
|
+
content = results[label]
|
|
160
|
+
sections.append(f"{separator} {label} {separator}\n{content}")
|
|
161
|
+
return "\n\n".join(sections)
|
ot/utils/cache.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""In-memory caching with TTL for tools.
|
|
2
|
+
|
|
3
|
+
Provides both a decorator for function memoization and manual cache operations.
|
|
4
|
+
Cache persists for the lifetime of the process.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
|
|
14
|
+
__all__ = ["CacheNamespace", "cache"]
|
|
15
|
+
|
|
16
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CacheEntry:
|
|
20
|
+
"""A cached value with expiration time."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, value: Any, ttl: float) -> None:
|
|
23
|
+
self.value = value
|
|
24
|
+
self.expires_at = time.time() + ttl
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CacheNamespace:
|
|
28
|
+
"""Cache namespace with get/set/clear operations and decorator."""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._store: dict[str, CacheEntry] = {}
|
|
32
|
+
self._memoize_stores: dict[str, dict[str, CacheEntry]] = {}
|
|
33
|
+
|
|
34
|
+
def get(self, key: str) -> Any | None:
|
|
35
|
+
"""Get a cached value by key.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
key: Cache key
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Cached value, or None if not found or expired
|
|
42
|
+
"""
|
|
43
|
+
entry = self._store.get(key)
|
|
44
|
+
if entry is None:
|
|
45
|
+
return None
|
|
46
|
+
if time.time() > entry.expires_at:
|
|
47
|
+
del self._store[key]
|
|
48
|
+
return None
|
|
49
|
+
return entry.value
|
|
50
|
+
|
|
51
|
+
def set(self, key: str, value: Any, ttl: float = 300.0) -> None:
|
|
52
|
+
"""Set a cached value with TTL.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
key: Cache key
|
|
56
|
+
value: Value to cache
|
|
57
|
+
ttl: Time-to-live in seconds (default: 5 minutes)
|
|
58
|
+
"""
|
|
59
|
+
self._store[key] = CacheEntry(value, ttl)
|
|
60
|
+
|
|
61
|
+
def clear(self, key: str | None = None) -> None:
|
|
62
|
+
"""Clear cached values.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
key: Specific key to clear, or None to clear all
|
|
66
|
+
"""
|
|
67
|
+
if key is None:
|
|
68
|
+
self._store.clear()
|
|
69
|
+
self._memoize_stores.clear()
|
|
70
|
+
else:
|
|
71
|
+
self._store.pop(key, None)
|
|
72
|
+
|
|
73
|
+
def __call__(self, ttl: float = 300.0) -> Callable[[F], F]:
|
|
74
|
+
"""Decorator for memoizing function results with TTL.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ttl: Time-to-live in seconds (default: 5 minutes)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Decorator function
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
@cache(ttl=60)
|
|
84
|
+
def expensive_operation(query: str) -> str:
|
|
85
|
+
...
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def decorator(func: F) -> F:
|
|
89
|
+
# Create a separate store for this function
|
|
90
|
+
func_id = f"{func.__module__}.{func.__qualname__}"
|
|
91
|
+
self._memoize_stores[func_id] = {}
|
|
92
|
+
|
|
93
|
+
@functools.wraps(func)
|
|
94
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
95
|
+
# Build cache key from args and kwargs
|
|
96
|
+
key_parts = [repr(arg) for arg in args]
|
|
97
|
+
key_parts.extend(f"{k}={v!r}" for k, v in sorted(kwargs.items()))
|
|
98
|
+
cache_key = ":".join(key_parts)
|
|
99
|
+
|
|
100
|
+
# Get or create store (may have been cleared by cache.clear())
|
|
101
|
+
store = self._memoize_stores.get(func_id)
|
|
102
|
+
if store is None:
|
|
103
|
+
store = {}
|
|
104
|
+
self._memoize_stores[func_id] = store
|
|
105
|
+
entry = store.get(cache_key)
|
|
106
|
+
|
|
107
|
+
if entry is not None and time.time() <= entry.expires_at:
|
|
108
|
+
return entry.value
|
|
109
|
+
|
|
110
|
+
result = func(*args, **kwargs)
|
|
111
|
+
store[cache_key] = CacheEntry(result, ttl)
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
return wrapper # type: ignore[return-value]
|
|
115
|
+
|
|
116
|
+
return decorator
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Singleton instance
|
|
120
|
+
cache = CacheNamespace()
|
ot/utils/deps.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Dependency validation utilities for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides decorators and functions for declaring and checking tool dependencies.
|
|
4
|
+
Supports both CLI tools (external binaries) and Python libraries.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
# Decorator usage
|
|
8
|
+
from ot.utils import requires_cli, requires_lib
|
|
9
|
+
|
|
10
|
+
@requires_cli("rg", install="brew install ripgrep")
|
|
11
|
+
@requires_lib("duckdb", install="pip install duckdb")
|
|
12
|
+
def search(query: str) -> str:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
# Module-level declaration (for AST scanning)
|
|
16
|
+
__ot_requires__ = {
|
|
17
|
+
"cli": [("rg", "brew install ripgrep")],
|
|
18
|
+
"lib": [("duckdb", "pip install duckdb")],
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import importlib.util
|
|
25
|
+
import shutil
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Dependency",
|
|
35
|
+
"DepsCheckResult",
|
|
36
|
+
"check_cli",
|
|
37
|
+
"check_deps",
|
|
38
|
+
"check_lib",
|
|
39
|
+
"check_secret",
|
|
40
|
+
"ensure_cli",
|
|
41
|
+
"ensure_lib",
|
|
42
|
+
"requires_cli",
|
|
43
|
+
"requires_lib",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Dependency:
|
|
51
|
+
"""Represents a single dependency."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
kind: str # "cli" or "lib"
|
|
55
|
+
install: str = ""
|
|
56
|
+
version: str = ""
|
|
57
|
+
available: bool = False
|
|
58
|
+
error: str = ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class DepsCheckResult:
|
|
63
|
+
"""Result of checking dependencies for a tool or pack."""
|
|
64
|
+
|
|
65
|
+
tool: str
|
|
66
|
+
dependencies: list[Dependency] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def ok(self) -> bool:
|
|
70
|
+
"""Check if all dependencies are available."""
|
|
71
|
+
return all(dep.available for dep in self.dependencies)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def missing(self) -> list[Dependency]:
|
|
75
|
+
"""Get list of missing dependencies."""
|
|
76
|
+
return [dep for dep in self.dependencies if not dep.available]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def requires_cli(
|
|
80
|
+
name: str,
|
|
81
|
+
*,
|
|
82
|
+
install: str = "",
|
|
83
|
+
version_flag: str = "--version",
|
|
84
|
+
) -> Callable[[F], F]:
|
|
85
|
+
"""Decorator to declare a CLI tool dependency.
|
|
86
|
+
|
|
87
|
+
Adds dependency metadata to the function for discovery by check_deps().
|
|
88
|
+
Does NOT perform runtime checking - use check_cli() for that.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
name: CLI command name (e.g., "rg", "ffmpeg", "pandoc")
|
|
92
|
+
install: Installation instructions shown when missing
|
|
93
|
+
version_flag: Flag to check version (default: "--version")
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Decorator that adds dependency metadata to the function
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
@requires_cli("rg", install="brew install ripgrep")
|
|
100
|
+
def search_with_ripgrep(pattern: str) -> str:
|
|
101
|
+
...
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def decorator(func: F) -> F:
|
|
105
|
+
# Initialize or extend __ot_requires__ attribute
|
|
106
|
+
if not hasattr(func, "__ot_requires__"):
|
|
107
|
+
func.__ot_requires__ = {"cli": [], "lib": []} # type: ignore[attr-defined]
|
|
108
|
+
|
|
109
|
+
func.__ot_requires__["cli"].append( # type: ignore[attr-defined]
|
|
110
|
+
{"name": name, "install": install, "version_flag": version_flag}
|
|
111
|
+
)
|
|
112
|
+
return func
|
|
113
|
+
|
|
114
|
+
return decorator
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def requires_lib(
|
|
118
|
+
name: str,
|
|
119
|
+
*,
|
|
120
|
+
install: str = "",
|
|
121
|
+
import_name: str = "",
|
|
122
|
+
) -> Callable[[F], F]:
|
|
123
|
+
"""Decorator to declare a Python library dependency.
|
|
124
|
+
|
|
125
|
+
Adds dependency metadata to the function for discovery by check_deps().
|
|
126
|
+
Does NOT perform runtime checking - use check_lib() for that.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name: Package name (e.g., "duckdb", "openai")
|
|
130
|
+
install: Installation instructions (default: "pip install {name}")
|
|
131
|
+
import_name: Import name if different from package name
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Decorator that adds dependency metadata to the function
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
@requires_lib("duckdb", install="pip install duckdb")
|
|
138
|
+
def query_database(sql: str) -> str:
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
@requires_lib("google-genai", import_name="google.genai")
|
|
142
|
+
def search_with_gemini(query: str) -> str:
|
|
143
|
+
...
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def decorator(func: F) -> F:
|
|
147
|
+
if not hasattr(func, "__ot_requires__"):
|
|
148
|
+
func.__ot_requires__ = {"cli": [], "lib": []} # type: ignore[attr-defined]
|
|
149
|
+
|
|
150
|
+
func.__ot_requires__["lib"].append( # type: ignore[attr-defined]
|
|
151
|
+
{
|
|
152
|
+
"name": name,
|
|
153
|
+
"install": install or f"pip install {name}",
|
|
154
|
+
"import_name": import_name or name,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
return func
|
|
158
|
+
|
|
159
|
+
return decorator
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def check_cli(name: str) -> Dependency:
|
|
163
|
+
"""Check if a CLI tool is available in PATH.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: CLI command name to check
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dependency object with availability status
|
|
170
|
+
"""
|
|
171
|
+
dep = Dependency(name=name, kind="cli")
|
|
172
|
+
path = shutil.which(name)
|
|
173
|
+
if path:
|
|
174
|
+
dep.available = True
|
|
175
|
+
else:
|
|
176
|
+
dep.error = f"'{name}' not found in PATH"
|
|
177
|
+
return dep
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check_lib(name: str, import_name: str = "") -> Dependency:
|
|
181
|
+
"""Check if a Python library is importable.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
name: Package name
|
|
185
|
+
import_name: Import name if different from package name
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dependency object with availability status
|
|
189
|
+
"""
|
|
190
|
+
dep = Dependency(name=name, kind="lib")
|
|
191
|
+
module_name = import_name or name
|
|
192
|
+
|
|
193
|
+
spec = importlib.util.find_spec(module_name)
|
|
194
|
+
if spec is not None:
|
|
195
|
+
dep.available = True
|
|
196
|
+
else:
|
|
197
|
+
dep.error = f"Module '{module_name}' not importable"
|
|
198
|
+
|
|
199
|
+
return dep
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def check_secret(name: str) -> Dependency:
|
|
203
|
+
"""Check if a secret is configured.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
name: Secret name (e.g., "BRAVE_API_KEY")
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Dependency object with availability status
|
|
210
|
+
"""
|
|
211
|
+
from ot.config.secrets import get_secret
|
|
212
|
+
|
|
213
|
+
dep = Dependency(name=name, kind="secret")
|
|
214
|
+
value = get_secret(name)
|
|
215
|
+
if value:
|
|
216
|
+
dep.available = True
|
|
217
|
+
else:
|
|
218
|
+
dep.error = f"Secret '{name}' not configured"
|
|
219
|
+
return dep
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def check_deps(
|
|
223
|
+
tool_path: str | Path | None = None,
|
|
224
|
+
) -> list[DepsCheckResult]:
|
|
225
|
+
"""Check dependencies for all tools or a specific tool.
|
|
226
|
+
|
|
227
|
+
Uses the ToolRegistry to scan tool files for __ot_requires__ declarations
|
|
228
|
+
and checks each dependency for availability.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
tool_path: Path to a specific tool file, or None to check all tools
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of DepsCheckResult for each tool with declared dependencies
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
results = check_deps()
|
|
238
|
+
for result in results:
|
|
239
|
+
if not result.ok:
|
|
240
|
+
print(f"{result.tool}: missing {len(result.missing)} deps")
|
|
241
|
+
for dep in result.missing:
|
|
242
|
+
print(f" - {dep.name}: {dep.install}")
|
|
243
|
+
"""
|
|
244
|
+
from pathlib import Path
|
|
245
|
+
|
|
246
|
+
from ot.registry import ToolRegistry
|
|
247
|
+
|
|
248
|
+
results: list[DepsCheckResult] = []
|
|
249
|
+
|
|
250
|
+
# Determine which files to check
|
|
251
|
+
if tool_path:
|
|
252
|
+
files = [Path(tool_path)]
|
|
253
|
+
else:
|
|
254
|
+
files = []
|
|
255
|
+
|
|
256
|
+
# Always include bundled tools from ot_tools package
|
|
257
|
+
try:
|
|
258
|
+
import ot_tools
|
|
259
|
+
|
|
260
|
+
bundled_dir = Path(ot_tools.__file__).parent
|
|
261
|
+
bundled_files = [
|
|
262
|
+
f for f in bundled_dir.glob("*.py") if f.name != "__init__.py"
|
|
263
|
+
]
|
|
264
|
+
files.extend(bundled_files)
|
|
265
|
+
except ImportError:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
# Add config-specified tools
|
|
269
|
+
try:
|
|
270
|
+
from ot.config.loader import get_config
|
|
271
|
+
|
|
272
|
+
config = get_config()
|
|
273
|
+
if config:
|
|
274
|
+
config_files = config.get_tool_files()
|
|
275
|
+
files.extend(config_files)
|
|
276
|
+
except Exception:
|
|
277
|
+
pass # Config may not be available, use bundled tools only
|
|
278
|
+
|
|
279
|
+
if not files:
|
|
280
|
+
return results
|
|
281
|
+
|
|
282
|
+
# Use ToolRegistry to parse files (extracts __ot_requires__ via AST)
|
|
283
|
+
registry = ToolRegistry()
|
|
284
|
+
tools = registry.scan_files(files)
|
|
285
|
+
|
|
286
|
+
# Group tools by pack/module to dedupe (all tools in a file share requires)
|
|
287
|
+
seen_packs: set[str] = set()
|
|
288
|
+
for tool in tools:
|
|
289
|
+
if not tool.requires:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Use pack name or module stem as identifier
|
|
293
|
+
pack_id = tool.pack or tool.module.split(".")[-1]
|
|
294
|
+
if pack_id in seen_packs:
|
|
295
|
+
continue
|
|
296
|
+
seen_packs.add(pack_id)
|
|
297
|
+
|
|
298
|
+
tool_result = DepsCheckResult(tool=pack_id)
|
|
299
|
+
|
|
300
|
+
# Check CLI dependencies
|
|
301
|
+
for cli_dep in tool.requires.get("cli", []):
|
|
302
|
+
# Handle tuple format (name, install), dict format, or string
|
|
303
|
+
if isinstance(cli_dep, tuple):
|
|
304
|
+
name, install = cli_dep[0], cli_dep[1] if len(cli_dep) > 1 else ""
|
|
305
|
+
elif isinstance(cli_dep, dict):
|
|
306
|
+
name = cli_dep.get("name", "")
|
|
307
|
+
install = cli_dep.get("install", "")
|
|
308
|
+
else:
|
|
309
|
+
name, install = str(cli_dep), ""
|
|
310
|
+
dep = check_cli(name)
|
|
311
|
+
dep.install = install
|
|
312
|
+
tool_result.dependencies.append(dep)
|
|
313
|
+
|
|
314
|
+
# Check library dependencies
|
|
315
|
+
for lib_dep in tool.requires.get("lib", []):
|
|
316
|
+
# Handle tuple format (name, install), dict format, or string
|
|
317
|
+
if isinstance(lib_dep, tuple):
|
|
318
|
+
name, install = lib_dep[0], lib_dep[1] if len(lib_dep) > 1 else ""
|
|
319
|
+
import_name = ""
|
|
320
|
+
elif isinstance(lib_dep, dict):
|
|
321
|
+
name = lib_dep.get("name", "")
|
|
322
|
+
install = lib_dep.get("install", f"pip install {name}")
|
|
323
|
+
import_name = lib_dep.get("import_name", "")
|
|
324
|
+
else:
|
|
325
|
+
name, install, import_name = str(lib_dep), "", ""
|
|
326
|
+
dep = check_lib(name, import_name)
|
|
327
|
+
dep.install = install or f"pip install {name}"
|
|
328
|
+
tool_result.dependencies.append(dep)
|
|
329
|
+
|
|
330
|
+
# Check secret dependencies (secrets are always strings)
|
|
331
|
+
for secret_item in tool.requires.get("secrets", []):
|
|
332
|
+
secret_name = str(secret_item) if not isinstance(secret_item, str) else secret_item
|
|
333
|
+
dep = check_secret(secret_name)
|
|
334
|
+
dep.install = "Add to secrets.yaml"
|
|
335
|
+
tool_result.dependencies.append(dep)
|
|
336
|
+
|
|
337
|
+
if tool_result.dependencies:
|
|
338
|
+
results.append(tool_result)
|
|
339
|
+
|
|
340
|
+
return results
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def ensure_cli(
|
|
344
|
+
name: str,
|
|
345
|
+
*,
|
|
346
|
+
install: str = "",
|
|
347
|
+
) -> str | None:
|
|
348
|
+
"""Check CLI availability at runtime, returning error message if missing.
|
|
349
|
+
|
|
350
|
+
Convenience function for early validation in tool functions.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
name: CLI command name
|
|
354
|
+
install: Installation instructions
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
None if available, error message string if missing
|
|
358
|
+
|
|
359
|
+
Example:
|
|
360
|
+
def my_tool() -> str:
|
|
361
|
+
if error := ensure_cli("rg", install="brew install ripgrep"):
|
|
362
|
+
return error
|
|
363
|
+
# Continue with ripgrep operations
|
|
364
|
+
"""
|
|
365
|
+
dep = check_cli(name)
|
|
366
|
+
if dep.available:
|
|
367
|
+
return None
|
|
368
|
+
msg = f"Error: '{name}' CLI tool not found"
|
|
369
|
+
if install:
|
|
370
|
+
msg += f". Install with: {install}"
|
|
371
|
+
return msg
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def ensure_lib(
|
|
375
|
+
name: str,
|
|
376
|
+
*,
|
|
377
|
+
import_name: str = "",
|
|
378
|
+
install: str = "",
|
|
379
|
+
) -> str | None:
|
|
380
|
+
"""Check library availability at runtime, returning error message if missing.
|
|
381
|
+
|
|
382
|
+
Convenience function for early validation in tool functions.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
name: Package name
|
|
386
|
+
import_name: Import name if different from package name
|
|
387
|
+
install: Installation instructions
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
None if available, error message string if missing
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
def my_tool() -> str:
|
|
394
|
+
if error := ensure_lib("duckdb"):
|
|
395
|
+
return error
|
|
396
|
+
import duckdb
|
|
397
|
+
# Continue with duckdb operations
|
|
398
|
+
"""
|
|
399
|
+
dep = check_lib(name, import_name)
|
|
400
|
+
if dep.available:
|
|
401
|
+
return None
|
|
402
|
+
install = install or f"pip install {name}"
|
|
403
|
+
return f"Error: '{name}' library not available. Install with: {install}"
|
ot/utils/exceptions.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Exception handling utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def flatten_exception_group(
|
|
7
|
+
eg: BaseExceptionGroup[BaseException],
|
|
8
|
+
) -> list[BaseException]:
|
|
9
|
+
"""Recursively flatten nested ExceptionGroups to get leaf exceptions.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
eg: The exception group to flatten.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
List of leaf exceptions (non-group exceptions).
|
|
16
|
+
"""
|
|
17
|
+
result: list[BaseException] = []
|
|
18
|
+
for exc in eg.exceptions:
|
|
19
|
+
if isinstance(exc, BaseExceptionGroup):
|
|
20
|
+
result.extend(flatten_exception_group(exc))
|
|
21
|
+
else:
|
|
22
|
+
result.append(exc)
|
|
23
|
+
return result
|