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.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. 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