echotools 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. echotools/__init__.py +149 -0
  2. echotools/cache/__init__.py +8 -0
  3. echotools/cache/list_cache.py +152 -0
  4. echotools/cache/memory_cache.py +72 -0
  5. echotools/config/__init__.py +23 -0
  6. echotools/config/base.py +144 -0
  7. echotools/config/center.py +443 -0
  8. echotools/config/loader.py +138 -0
  9. echotools/config/merge.py +29 -0
  10. echotools/dispatch/__init__.py +20 -0
  11. echotools/dispatch/candidate.py +51 -0
  12. echotools/dispatch/dispatcher.py +268 -0
  13. echotools/dispatch/selector.py +390 -0
  14. echotools/errors/__init__.py +29 -0
  15. echotools/errors/base.py +32 -0
  16. echotools/errors/classify.py +42 -0
  17. echotools/errors/common.py +74 -0
  18. echotools/events/__init__.py +8 -0
  19. echotools/events/bus.py +104 -0
  20. echotools/events/event.py +31 -0
  21. echotools/files/__init__.py +7 -0
  22. echotools/files/file_util.py +155 -0
  23. echotools/fncall/__init__.py +40 -0
  24. echotools/fncall/parsers/__init__.py +6 -0
  25. echotools/fncall/parsers/stream.py +156 -0
  26. echotools/fncall/parsers/xml_parser.py +320 -0
  27. echotools/fncall/prompt/__init__.py +5 -0
  28. echotools/fncall/prompt/history.py +383 -0
  29. echotools/fncall/prompt/inject.py +108 -0
  30. echotools/fncall/prompt/templates.py +159 -0
  31. echotools/fncall/protocols/__init__.py +28 -0
  32. echotools/fncall/protocols/antml.py +251 -0
  33. echotools/fncall/protocols/bracket.py +154 -0
  34. echotools/fncall/protocols/custom.py +50 -0
  35. echotools/fncall/protocols/nous.py +161 -0
  36. echotools/fncall/protocols/original.py +378 -0
  37. echotools/fncall/protocols/xml.py +249 -0
  38. echotools/fncall/registry.py +96 -0
  39. echotools/fncall/shared/__init__.py +25 -0
  40. echotools/fncall/shared/coercion.py +295 -0
  41. echotools/fncall/shared/loop_detect.py +97 -0
  42. echotools/fncall/shared/normalization.py +170 -0
  43. echotools/fncall/shared/xml_helpers.py +56 -0
  44. echotools/ids/__init__.py +7 -0
  45. echotools/ids/generator.py +62 -0
  46. echotools/io/__init__.py +11 -0
  47. echotools/io/io_utils.py +81 -0
  48. echotools/lifecycle/__init__.py +8 -0
  49. echotools/lifecycle/manager.py +72 -0
  50. echotools/lifecycle/updater.py +176 -0
  51. echotools/logger/__init__.py +12 -0
  52. echotools/logger/manager.py +164 -0
  53. echotools/network/__init__.py +11 -0
  54. echotools/network/http_utils.py +68 -0
  55. echotools/plugin/__init__.py +9 -0
  56. echotools/plugin/base.py +41 -0
  57. echotools/plugin/discovery.py +134 -0
  58. echotools/plugin/registry.py +253 -0
  59. echotools/process/__init__.py +10 -0
  60. echotools/process/port.py +183 -0
  61. echotools/protocol/__init__.py +19 -0
  62. echotools/protocol/base.py +123 -0
  63. echotools/proxy/__init__.py +7 -0
  64. echotools/proxy/manager.py +269 -0
  65. echotools/retry/__init__.py +11 -0
  66. echotools/retry/retry.py +127 -0
  67. echotools/runtime/__init__.py +7 -0
  68. echotools/runtime/collector.py +60 -0
  69. echotools/scheduler/__init__.py +7 -0
  70. echotools/scheduler/scheduler.py +69 -0
  71. echotools/sdk/__init__.py +7 -0
  72. echotools/sdk/facade.py +154 -0
  73. echotools/tracing/__init__.py +28 -0
  74. echotools/tracing/context.py +63 -0
  75. echotools/tracing/span.py +135 -0
  76. echotools/tracing/tracer.py +97 -0
  77. echotools/watcher/__init__.py +7 -0
  78. echotools/watcher/file_watcher.py +110 -0
  79. echotools/web/__init__.py +8 -0
  80. echotools/web/application.py +120 -0
  81. echotools/web/utils.py +186 -0
  82. echotools-1.0.0.dist-info/METADATA +80 -0
  83. echotools-1.0.0.dist-info/RECORD +85 -0
  84. echotools-1.0.0.dist-info/WHEEL +5 -0
  85. echotools-1.0.0.dist-info/top_level.txt +1 -0
echotools/__init__.py ADDED
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ """echotools:通用基础设施 SDK。
4
+
5
+ 完全项目无关,支持调用链,覆盖 Python 3.8-3.14。
6
+ """
7
+
8
+ from echotools.cache import ListCache, MemoryCache
9
+ from echotools.config import ConfigBase, ConfigCenter
10
+ from echotools.dispatch import (
11
+ AdaptiveSelector,
12
+ TaskCandidate,
13
+ TaskDispatcher,
14
+ make_id,
15
+ )
16
+ from echotools.errors import (
17
+ ConfigError,
18
+ EchoError,
19
+ NetworkError,
20
+ NoCandidateError,
21
+ NotSupportedError,
22
+ PluginError,
23
+ ProtocolError,
24
+ TimeoutError,
25
+ ValidationError,
26
+ classify_http_error,
27
+ )
28
+ from echotools.events import Event, EventBus
29
+ from echotools.files import FileUtil
30
+ from echotools.ids import short_id, span_id, trace_id, uuid7
31
+ from echotools.io import (
32
+ atomic_write_text,
33
+ ensure_directory,
34
+ read_text_if_exists,
35
+ )
36
+ from echotools.lifecycle import AutoUpdater, LifecycleManager
37
+ from echotools.logger import LoggerManager, configure, get_logger, set_color
38
+ from echotools.plugin import Plugin, PluginRegistry, discover_plugins
39
+ from echotools.process import PortReleaseResult, ensure_port_available
40
+ from echotools.protocol.base import (
41
+ ToolProtocol,
42
+ get_protocol_by_id,
43
+ list_protocols,
44
+ register_protocol,
45
+ )
46
+ from echotools.proxy import ProxyManager
47
+ from echotools.retry import (
48
+ retry_on_empty,
49
+ retry_on_exception,
50
+ retry_with_backoff,
51
+ )
52
+ from echotools.runtime import RuntimeCollector
53
+ from echotools.scheduler import TaskScheduler
54
+ from echotools.sdk import EchoTools
55
+ from echotools.tracing import (
56
+ Span,
57
+ Trace,
58
+ Tracer,
59
+ get_current_span_id,
60
+ get_current_trace_id,
61
+ get_request_id,
62
+ set_current_span_id,
63
+ set_current_trace_id,
64
+ set_request_id,
65
+ )
66
+ from echotools.web import WebApplication, json_body, safe_flush
67
+
68
+ __version__ = "1.0.0"
69
+
70
+ __all__ = [
71
+ "__version__",
72
+ # SDK 门面
73
+ "EchoTools",
74
+ # 配置
75
+ "ConfigBase",
76
+ "ConfigCenter",
77
+ # 日志
78
+ "LoggerManager",
79
+ "get_logger",
80
+ "set_color",
81
+ "configure",
82
+ # 事件
83
+ "Event",
84
+ "EventBus",
85
+ # 调用链
86
+ "Tracer",
87
+ "Trace",
88
+ "Span",
89
+ "get_current_trace_id",
90
+ "set_current_trace_id",
91
+ "get_current_span_id",
92
+ "set_current_span_id",
93
+ "get_request_id",
94
+ "set_request_id",
95
+ # 缓存
96
+ "ListCache",
97
+ "MemoryCache",
98
+ # 调度
99
+ "TaskCandidate",
100
+ "make_id",
101
+ "AdaptiveSelector",
102
+ "TaskDispatcher",
103
+ "TaskScheduler",
104
+ # 插件
105
+ "Plugin",
106
+ "PluginRegistry",
107
+ "discover_plugins",
108
+ # 协议
109
+ "ToolProtocol",
110
+ "register_protocol",
111
+ "get_protocol_by_id",
112
+ "list_protocols",
113
+ # 生命周期
114
+ "LifecycleManager",
115
+ "AutoUpdater",
116
+ # 网络/代理/进程
117
+ "ProxyManager",
118
+ "PortReleaseResult",
119
+ "ensure_port_available",
120
+ # 运行时
121
+ "RuntimeCollector",
122
+ # Web
123
+ "WebApplication",
124
+ "json_body",
125
+ "safe_flush",
126
+ # 工具
127
+ "FileUtil",
128
+ "uuid7",
129
+ "short_id",
130
+ "trace_id",
131
+ "span_id",
132
+ "atomic_write_text",
133
+ "ensure_directory",
134
+ "read_text_if_exists",
135
+ "retry_with_backoff",
136
+ "retry_on_empty",
137
+ "retry_on_exception",
138
+ # 错误
139
+ "EchoError",
140
+ "ConfigError",
141
+ "ValidationError",
142
+ "NetworkError",
143
+ "TimeoutError",
144
+ "NotSupportedError",
145
+ "NoCandidateError",
146
+ "PluginError",
147
+ "ProtocolError",
148
+ "classify_http_error",
149
+ ]
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ """cache 模块导出。"""
4
+
5
+ from echotools.cache.list_cache import ListCache
6
+ from echotools.cache.memory_cache import MemoryCache
7
+
8
+ __all__ = ["ListCache", "MemoryCache"]
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ """通用列表缓存:持久化 + 定时刷新 + 合并策略。
4
+
5
+ 从 ModelsCache 抽象为完全通用的版本,不预设"模型"语义。
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Awaitable, Callable, List, Optional
13
+
14
+ from echotools.logger.manager import get_logger
15
+
16
+ __all__ = ["ListCache"]
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class ListCache:
22
+ """通用字符串列表缓存管理器。
23
+
24
+ 职责:
25
+ 1. 从持久化文件读取缓存
26
+ 2. 定时调用 fetch_fn 刷新远程列表
27
+ 3. 根据 overwrite 决定覆盖或追加
28
+ 4. 更新后触发 on_update 回调
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ name: str,
34
+ fallback: List[str],
35
+ cache_path: str,
36
+ overwrite: bool = True,
37
+ ) -> None:
38
+ """初始化列表缓存。
39
+
40
+ Args:
41
+ name: 缓存标识名(仅用于日志)。
42
+ fallback: 兜底列表。
43
+ cache_path: 持久化文件路径。
44
+ overwrite: True=覆盖,False=只增不减。
45
+ """
46
+ self._name = name
47
+ self._fallback = list(fallback)
48
+ self._overwrite = overwrite
49
+ self._items: List[str] = list(fallback)
50
+ self._cache_path = Path(cache_path)
51
+ self._refreshing = False
52
+
53
+ async def load(self) -> List[str]:
54
+ """从缓存文件加载列表。
55
+
56
+ Returns:
57
+ 缓存列表,无缓存则返回兜底列表。
58
+ """
59
+ try:
60
+ if self._cache_path.is_file():
61
+ text = self._cache_path.read_text(encoding="utf-8")
62
+ data = json.loads(text)
63
+ items = data.get("items", [])
64
+ if items:
65
+ self._items = list(items)
66
+ logger.info(
67
+ "[%s] 从缓存加载 %d 项",
68
+ self._name,
69
+ len(self._items),
70
+ )
71
+ except Exception as e:
72
+ logger.warning("[%s] 缓存加载失败: %s", self._name, e)
73
+ return list(self._items)
74
+
75
+ async def save(self, items: List[str]) -> None:
76
+ """保存列表到缓存文件。
77
+
78
+ Args:
79
+ items: 要保存的列表。
80
+ """
81
+ try:
82
+ self._cache_path.parent.mkdir(parents=True, exist_ok=True)
83
+ data = {"items": items, "updated_at": int(time.time())}
84
+ self._cache_path.write_text(
85
+ json.dumps(data, ensure_ascii=False, indent=2),
86
+ encoding="utf-8",
87
+ )
88
+ except Exception as e:
89
+ logger.warning("[%s] 缓存保存失败: %s", self._name, e)
90
+
91
+ def _merge(self, remote: List[str]) -> List[str]:
92
+ """根据策略合并列表。"""
93
+ if self._overwrite:
94
+ return list(remote) if remote else list(self._items)
95
+ existing = set(self._items)
96
+ merged = list(self._items)
97
+ for m in remote:
98
+ if m not in existing:
99
+ merged.append(m)
100
+ existing.add(m)
101
+ return merged
102
+
103
+ async def start_refresh_loop(
104
+ self,
105
+ fetch_fn: Callable[[], Awaitable[List[str]]],
106
+ interval: int = 86400,
107
+ on_update: Optional[
108
+ Callable[[List[str]], Awaitable[None]]
109
+ ] = None,
110
+ ) -> None:
111
+ """启动定时刷新循环(永久运行)。
112
+
113
+ Args:
114
+ fetch_fn: 返回远程列表的异步函数。
115
+ interval: 刷新间隔(秒)。
116
+ on_update: 更新回调。
117
+ """
118
+ while True:
119
+ await self._do_refresh(fetch_fn, on_update)
120
+ await asyncio.sleep(interval)
121
+
122
+ async def _do_refresh(
123
+ self,
124
+ fetch_fn: Callable[[], Awaitable[List[str]]],
125
+ on_update: Optional[
126
+ Callable[[List[str]], Awaitable[None]]
127
+ ] = None,
128
+ ) -> None:
129
+ """执行一次刷新。"""
130
+ if self._refreshing:
131
+ return
132
+ self._refreshing = True
133
+ try:
134
+ remote = await fetch_fn()
135
+ if remote:
136
+ merged = self._merge(remote)
137
+ self._items = merged
138
+ await self.save(merged)
139
+ if on_update is not None:
140
+ await on_update(merged)
141
+ logger.info(
142
+ "[%s] 列表已刷新: %d 项", self._name, len(merged)
143
+ )
144
+ except Exception as e:
145
+ logger.warning("[%s] 列表刷新失败: %s", self._name, e)
146
+ finally:
147
+ self._refreshing = False
148
+
149
+ @property
150
+ def items(self) -> List[str]:
151
+ """当前列表副本。"""
152
+ return list(self._items)
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ """带 TTL 的内存缓存。"""
4
+
5
+ import time
6
+ from typing import Any, Dict, Optional, Tuple
7
+
8
+ __all__ = ["MemoryCache"]
9
+
10
+
11
+ class MemoryCache:
12
+ """简单的 TTL 内存缓存。"""
13
+
14
+ def __init__(self, default_ttl: float = 0.0) -> None:
15
+ """初始化缓存。
16
+
17
+ Args:
18
+ default_ttl: 默认存活秒数,0 表示永不过期。
19
+ """
20
+ self._store: Dict[str, Tuple[Any, float]] = {}
21
+ self._default_ttl = default_ttl
22
+
23
+ def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
24
+ """写入缓存。
25
+
26
+ Args:
27
+ key: 键。
28
+ value: 值。
29
+ ttl: 存活秒数,None 用默认。
30
+ """
31
+ effective = self._default_ttl if ttl is None else ttl
32
+ expire = 0.0 if effective <= 0 else time.time() + effective
33
+ self._store[key] = (value, expire)
34
+
35
+ def get(self, key: str, default: Any = None) -> Any:
36
+ """读取缓存。
37
+
38
+ Args:
39
+ key: 键。
40
+ default: 缺省值。
41
+
42
+ Returns:
43
+ 值或默认值(过期自动剔除)。
44
+ """
45
+ item = self._store.get(key)
46
+ if item is None:
47
+ return default
48
+ value, expire = item
49
+ if expire and time.time() > expire:
50
+ self._store.pop(key, None)
51
+ return default
52
+ return value
53
+
54
+ def delete(self, key: str) -> None:
55
+ """删除键。"""
56
+ self._store.pop(key, None)
57
+
58
+ def clear(self) -> None:
59
+ """清空缓存。"""
60
+ self._store.clear()
61
+
62
+ def cleanup(self) -> int:
63
+ """清理过期项,返回清理数量。"""
64
+ now = time.time()
65
+ expired = [
66
+ k
67
+ for k, (_, exp) in self._store.items()
68
+ if exp and now > exp
69
+ ]
70
+ for k in expired:
71
+ self._store.pop(k, None)
72
+ return len(expired)
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ """config 模块导出。"""
4
+
5
+ from echotools.config.base import ConfigBase
6
+ from echotools.config.center import ConfigCenter
7
+ from echotools.config.loader import (
8
+ find_config,
9
+ find_template,
10
+ load_file,
11
+ write_toml,
12
+ )
13
+ from echotools.config.merge import merge_dicts
14
+
15
+ __all__ = [
16
+ "ConfigBase",
17
+ "ConfigCenter",
18
+ "load_file",
19
+ "find_config",
20
+ "find_template",
21
+ "write_toml",
22
+ "merge_dicts",
23
+ ]
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ """配置数据类基类,提供自动 from_dict 反序列化。"""
4
+
5
+ from dataclasses import MISSING, fields, is_dataclass
6
+ from typing import (
7
+ Any,
8
+ Literal,
9
+ Type,
10
+ TypeVar,
11
+ Union,
12
+ get_args,
13
+ get_origin,
14
+ get_type_hints,
15
+ )
16
+
17
+ __all__ = ["ConfigBase"]
18
+
19
+ T = TypeVar("T", bound="ConfigBase")
20
+
21
+ _type_hints_cache: "dict[type, dict[str, Any]]" = {}
22
+
23
+
24
+ class ConfigBase:
25
+ """配置类基类,提供 from_dict 自动反序列化。"""
26
+
27
+ @classmethod
28
+ def from_dict(cls: Type[T], data: "dict[str, Any]") -> T:
29
+ """从字典构造实例。"""
30
+ if not isinstance(data, dict):
31
+ raise TypeError(
32
+ "Expected a dict, got {}".format(type(data).__name__)
33
+ )
34
+ init_args: "dict[str, Any]" = {}
35
+ type_hints = _get_type_hints(cls)
36
+ for f in fields(cls): # type: ignore[arg-type]
37
+ field_name = f.name
38
+ field_type = type_hints.get(field_name, f.type)
39
+ if field_name.startswith("_"):
40
+ continue
41
+ if not f.init:
42
+ continue
43
+ if field_name not in data:
44
+ if f.default is not MISSING:
45
+ init_args[field_name] = f.default
46
+ continue
47
+ if f.default_factory is not MISSING: # type: ignore[misc]
48
+ init_args[field_name] = f.default_factory() # type: ignore[misc]
49
+ continue
50
+ raise ValueError(
51
+ "Missing required field: '{}'".format(field_name)
52
+ )
53
+ value = data[field_name]
54
+ init_args[field_name] = cls._convert_field(value, field_type)
55
+ return cls(**init_args)
56
+
57
+ @classmethod
58
+ def _convert_field(cls, value: Any, field_type: Any) -> Any:
59
+ """转换单个字段。"""
60
+ if isinstance(field_type, type) and is_dataclass(field_type):
61
+ return field_type.from_dict(value) # type: ignore[attr-defined]
62
+ origin = get_origin(field_type)
63
+ args = get_args(field_type)
64
+ if origin in {list, set, tuple}:
65
+ if not isinstance(value, list):
66
+ raise TypeError(
67
+ "Expected list for {}, got {}".format(
68
+ field_type, type(value).__name__
69
+ )
70
+ )
71
+ if origin is list:
72
+ item_type = args[0] if args else Any
73
+ return [cls._convert_field(i, item_type) for i in value]
74
+ if origin is set:
75
+ item_type = args[0] if args else Any
76
+ return {cls._convert_field(i, item_type) for i in value}
77
+ if origin is tuple:
78
+ return tuple(
79
+ cls._convert_field(i, a) for i, a in zip(value, args)
80
+ )
81
+ if origin is dict:
82
+ if not isinstance(value, dict):
83
+ raise TypeError(
84
+ "Expected dict for {}, got {}".format(
85
+ field_type, type(value).__name__
86
+ )
87
+ )
88
+ key_type, val_type = args if len(args) == 2 else (Any, Any)
89
+ result = {}
90
+ for k, v in value.items():
91
+ ck = cls._convert_field(k, key_type)
92
+ try:
93
+ cv = cls._convert_field(v, val_type)
94
+ except (TypeError, ValueError):
95
+ cv = v
96
+ result[ck] = cv
97
+ return result
98
+ if origin is Union:
99
+ if value is None:
100
+ return None
101
+ real_type = next(
102
+ (a for a in args if a is not type(None)), Any
103
+ )
104
+ return cls._convert_field(value, real_type)
105
+ if origin is Literal:
106
+ allowed = get_args(field_type)
107
+ if value not in allowed:
108
+ raise TypeError(
109
+ "Value '{}' not in allowed values {}".format(
110
+ value, allowed
111
+ )
112
+ )
113
+ return value
114
+ if isinstance(field_type, type):
115
+ if isinstance(value, field_type):
116
+ return value
117
+ try:
118
+ return field_type(value)
119
+ except (ValueError, TypeError) as e:
120
+ raise TypeError(
121
+ "Cannot convert {} to {}".format(
122
+ type(value).__name__, field_type.__name__
123
+ )
124
+ ) from e
125
+ return value
126
+
127
+ def __str__(self) -> str:
128
+ field_strs = [
129
+ "{}={!r}".format(f.name, getattr(self, f.name))
130
+ for f in fields(self) # type: ignore[arg-type]
131
+ ]
132
+ return "{}({})".format(
133
+ self.__class__.__name__, ", ".join(field_strs)
134
+ )
135
+
136
+
137
+ def _get_type_hints(cls: type) -> "dict[str, Any]":
138
+ """带缓存的 get_type_hints。"""
139
+ if cls not in _type_hints_cache:
140
+ try:
141
+ _type_hints_cache[cls] = get_type_hints(cls)
142
+ except Exception:
143
+ _type_hints_cache[cls] = {}
144
+ return _type_hints_cache[cls]