onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__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.
- onetool/cli.py +63 -4
- onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
- onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
- ot/__main__.py +6 -6
- ot/config/__init__.py +48 -46
- ot/config/global_templates/__init__.py +2 -2
- ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
- ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
- ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
- ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
- ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
- ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -0
- ot/config/global_templates/onetool.yaml +3 -1
- ot/config/{defaults → global_templates}/prompts.yaml +102 -97
- ot/config/global_templates/security.yaml +31 -0
- ot/config/global_templates/servers.yaml +93 -12
- ot/config/global_templates/snippets.yaml +5 -26
- ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
- ot/config/loader.py +221 -105
- ot/config/mcp.py +5 -1
- ot/config/secrets.py +192 -190
- ot/decorators.py +116 -116
- ot/executor/__init__.py +35 -35
- ot/executor/base.py +16 -16
- ot/executor/fence_processor.py +83 -83
- ot/executor/linter.py +142 -142
- ot/executor/pep723.py +288 -288
- ot/executor/runner.py +20 -6
- ot/executor/simple.py +163 -163
- ot/executor/validator.py +603 -164
- ot/http_client.py +145 -145
- ot/logging/__init__.py +37 -37
- ot/logging/entry.py +213 -213
- ot/logging/format.py +191 -188
- ot/logging/span.py +349 -349
- ot/meta.py +236 -14
- ot/paths.py +32 -49
- ot/prompts.py +218 -218
- ot/proxy/manager.py +14 -2
- ot/registry/__init__.py +189 -189
- ot/registry/parser.py +269 -269
- ot/server.py +330 -315
- ot/shortcuts/__init__.py +15 -15
- ot/shortcuts/aliases.py +87 -87
- ot/shortcuts/snippets.py +258 -258
- ot/stats/__init__.py +35 -35
- ot/stats/html.py +2 -2
- ot/stats/reader.py +354 -354
- ot/stats/timing.py +57 -57
- ot/support.py +63 -63
- ot/tools.py +1 -1
- ot/utils/batch.py +161 -161
- ot/utils/cache.py +120 -120
- ot/utils/exceptions.py +23 -23
- ot/utils/factory.py +178 -179
- ot/utils/format.py +65 -65
- ot/utils/http.py +202 -202
- ot/utils/platform.py +45 -45
- ot/utils/truncate.py +69 -69
- ot_tools/__init__.py +4 -4
- ot_tools/_convert/__init__.py +12 -12
- ot_tools/_convert/pdf.py +254 -254
- ot_tools/diagram.yaml +167 -167
- ot_tools/scaffold.py +2 -2
- ot_tools/transform.py +124 -19
- ot_tools/web_fetch.py +94 -43
- onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
- onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
- ot/config/defaults/bench.yaml +0 -4
- ot/config/defaults/onetool.yaml +0 -25
- ot/config/defaults/servers.yaml +0 -7
- ot/config/defaults/snippets.yaml +0 -4
- ot_tools/firecrawl.py +0 -732
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/utils/cache.py
CHANGED
|
@@ -1,120 +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()
|
|
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/exceptions.py
CHANGED
|
@@ -1,23 +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
|
|
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
|
ot/utils/factory.py
CHANGED
|
@@ -1,179 +1,178 @@
|
|
|
1
|
-
"""Factory utilities for OneTool.
|
|
2
|
-
|
|
3
|
-
Provides thread-safe lazy initialization patterns for API clients.
|
|
4
|
-
|
|
5
|
-
Example:
|
|
6
|
-
from ot.utils import lazy_client
|
|
7
|
-
|
|
8
|
-
# Define a client factory
|
|
9
|
-
def create_my_client():
|
|
10
|
-
from mylib import Client
|
|
11
|
-
api_key = get_secret("MY_API_KEY")
|
|
12
|
-
return Client(api_key=api_key)
|
|
13
|
-
|
|
14
|
-
# Create a lazy-initialized getter
|
|
15
|
-
get_client = lazy_client(create_my_client)
|
|
16
|
-
|
|
17
|
-
# Use it anywhere - initialized once, thread-safe
|
|
18
|
-
client = get_client()
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from __future__ import annotations
|
|
22
|
-
|
|
23
|
-
import threading
|
|
24
|
-
from typing import TYPE_CHECKING, TypeVar
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from collections.abc import Callable
|
|
28
|
-
|
|
29
|
-
__all__ = ["LazyClient", "lazy_client"]
|
|
30
|
-
|
|
31
|
-
T = TypeVar("T")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def lazy_client(
|
|
35
|
-
factory: Callable[[], T | None],
|
|
36
|
-
*,
|
|
37
|
-
allow_none: bool = False,
|
|
38
|
-
) -> Callable[[], T | None]:
|
|
39
|
-
"""Create a thread-safe lazy-initialized client getter.
|
|
40
|
-
|
|
41
|
-
Wraps a factory function with double-checked locking to ensure
|
|
42
|
-
the client is created exactly once, even under concurrent access.
|
|
43
|
-
|
|
44
|
-
The factory function should:
|
|
45
|
-
- Return the client instance on success
|
|
46
|
-
- Return None if required credentials are missing
|
|
47
|
-
- Raise exceptions for other errors
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
factory: Callable that creates and returns the client instance
|
|
51
|
-
allow_none: If True, cache None results. If False (default), retry
|
|
52
|
-
factory on each call when it returns None.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
A callable that returns the lazily-initialized client
|
|
56
|
-
|
|
57
|
-
Example:
|
|
58
|
-
from ot.utils import lazy_client
|
|
59
|
-
from ot.config import get_secret
|
|
60
|
-
|
|
61
|
-
def
|
|
62
|
-
from
|
|
63
|
-
api_key = get_secret("
|
|
64
|
-
if not api_key:
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
client
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
client
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
self.
|
|
141
|
-
self.
|
|
142
|
-
self.
|
|
143
|
-
self.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
self.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
self.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return self._initialized
|
|
1
|
+
"""Factory utilities for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides thread-safe lazy initialization patterns for API clients.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
from ot.utils import lazy_client
|
|
7
|
+
|
|
8
|
+
# Define a client factory
|
|
9
|
+
def create_my_client():
|
|
10
|
+
from mylib import Client
|
|
11
|
+
api_key = get_secret("MY_API_KEY")
|
|
12
|
+
return Client(api_key=api_key)
|
|
13
|
+
|
|
14
|
+
# Create a lazy-initialized getter
|
|
15
|
+
get_client = lazy_client(create_my_client)
|
|
16
|
+
|
|
17
|
+
# Use it anywhere - initialized once, thread-safe
|
|
18
|
+
client = get_client()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import threading
|
|
24
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
|
|
29
|
+
__all__ = ["LazyClient", "lazy_client"]
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def lazy_client(
|
|
35
|
+
factory: Callable[[], T | None],
|
|
36
|
+
*,
|
|
37
|
+
allow_none: bool = False,
|
|
38
|
+
) -> Callable[[], T | None]:
|
|
39
|
+
"""Create a thread-safe lazy-initialized client getter.
|
|
40
|
+
|
|
41
|
+
Wraps a factory function with double-checked locking to ensure
|
|
42
|
+
the client is created exactly once, even under concurrent access.
|
|
43
|
+
|
|
44
|
+
The factory function should:
|
|
45
|
+
- Return the client instance on success
|
|
46
|
+
- Return None if required credentials are missing
|
|
47
|
+
- Raise exceptions for other errors
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
factory: Callable that creates and returns the client instance
|
|
51
|
+
allow_none: If True, cache None results. If False (default), retry
|
|
52
|
+
factory on each call when it returns None.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A callable that returns the lazily-initialized client
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
from ot.utils import lazy_client
|
|
59
|
+
from ot.config import get_secret
|
|
60
|
+
|
|
61
|
+
def create_brave():
|
|
62
|
+
from brave import Brave
|
|
63
|
+
api_key = get_secret("BRAVE_API_KEY")
|
|
64
|
+
if not api_key:
|
|
65
|
+
return None
|
|
66
|
+
return Brave(api_key=api_key)
|
|
67
|
+
|
|
68
|
+
get_brave = lazy_client(create_brave)
|
|
69
|
+
|
|
70
|
+
# Later, in tool functions:
|
|
71
|
+
def search(query: str) -> str:
|
|
72
|
+
client = get_brave()
|
|
73
|
+
if client is None:
|
|
74
|
+
return "Error: BRAVE_API_KEY not configured"
|
|
75
|
+
return client.search(query)
|
|
76
|
+
"""
|
|
77
|
+
client: T | None = None
|
|
78
|
+
initialized = False
|
|
79
|
+
lock = threading.Lock()
|
|
80
|
+
|
|
81
|
+
def get_client() -> T | None:
|
|
82
|
+
nonlocal client, initialized
|
|
83
|
+
|
|
84
|
+
# Fast path: already initialized
|
|
85
|
+
if initialized:
|
|
86
|
+
return client
|
|
87
|
+
|
|
88
|
+
# Slow path: acquire lock and initialize
|
|
89
|
+
with lock:
|
|
90
|
+
# Double-check after acquiring lock
|
|
91
|
+
if initialized:
|
|
92
|
+
return client
|
|
93
|
+
|
|
94
|
+
result = factory()
|
|
95
|
+
|
|
96
|
+
# Cache result if successful or allow_none is True
|
|
97
|
+
if result is not None or allow_none:
|
|
98
|
+
client = result
|
|
99
|
+
initialized = True
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
return get_client
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class LazyClient:
|
|
107
|
+
"""Class-based lazy client for more complex initialization patterns.
|
|
108
|
+
|
|
109
|
+
Useful when you need to pass the factory as a method or need
|
|
110
|
+
additional client management features.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
class MyPack:
|
|
114
|
+
def __init__(self):
|
|
115
|
+
self._client = LazyClient(self._create_client)
|
|
116
|
+
|
|
117
|
+
def _create_client(self):
|
|
118
|
+
return SomeClient(api_key=get_secret("API_KEY"))
|
|
119
|
+
|
|
120
|
+
def search(self, query: str) -> str:
|
|
121
|
+
client = self._client.get()
|
|
122
|
+
if client is None:
|
|
123
|
+
return "Error: API_KEY not configured"
|
|
124
|
+
return client.search(query)
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
factory: Callable[[], T | None],
|
|
130
|
+
*,
|
|
131
|
+
allow_none: bool = False,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Initialize the lazy client wrapper.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
factory: Callable that creates the client
|
|
137
|
+
allow_none: If True, cache None results
|
|
138
|
+
"""
|
|
139
|
+
self._factory = factory
|
|
140
|
+
self._allow_none = allow_none
|
|
141
|
+
self._client: T | None = None
|
|
142
|
+
self._initialized = False
|
|
143
|
+
self._lock = threading.Lock()
|
|
144
|
+
|
|
145
|
+
def get(self) -> T | None:
|
|
146
|
+
"""Get the client, initializing if necessary.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The client instance, or None if not available
|
|
150
|
+
"""
|
|
151
|
+
if self._initialized:
|
|
152
|
+
return self._client # type: ignore[return-value]
|
|
153
|
+
|
|
154
|
+
with self._lock:
|
|
155
|
+
if self._initialized:
|
|
156
|
+
return self._client
|
|
157
|
+
|
|
158
|
+
result = self._factory()
|
|
159
|
+
|
|
160
|
+
if result is not None or self._allow_none:
|
|
161
|
+
self._client = result
|
|
162
|
+
self._initialized = True
|
|
163
|
+
|
|
164
|
+
return result # type: ignore[return-value]
|
|
165
|
+
|
|
166
|
+
def reset(self) -> None:
|
|
167
|
+
"""Reset the client, forcing re-initialization on next access.
|
|
168
|
+
|
|
169
|
+
Useful for testing or when configuration changes.
|
|
170
|
+
"""
|
|
171
|
+
with self._lock:
|
|
172
|
+
self._client = None
|
|
173
|
+
self._initialized = False
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def is_initialized(self) -> bool:
|
|
177
|
+
"""Check if the client has been initialized."""
|
|
178
|
+
return self._initialized
|