echotools 1.0.0__tar.gz

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 (97) hide show
  1. echotools-1.0.0/PKG-INFO +80 -0
  2. echotools-1.0.0/README.md +34 -0
  3. echotools-1.0.0/pyproject.toml +82 -0
  4. echotools-1.0.0/setup.cfg +4 -0
  5. echotools-1.0.0/src/echotools/__init__.py +149 -0
  6. echotools-1.0.0/src/echotools/cache/__init__.py +8 -0
  7. echotools-1.0.0/src/echotools/cache/list_cache.py +152 -0
  8. echotools-1.0.0/src/echotools/cache/memory_cache.py +72 -0
  9. echotools-1.0.0/src/echotools/config/__init__.py +23 -0
  10. echotools-1.0.0/src/echotools/config/base.py +144 -0
  11. echotools-1.0.0/src/echotools/config/center.py +443 -0
  12. echotools-1.0.0/src/echotools/config/loader.py +138 -0
  13. echotools-1.0.0/src/echotools/config/merge.py +29 -0
  14. echotools-1.0.0/src/echotools/dispatch/__init__.py +20 -0
  15. echotools-1.0.0/src/echotools/dispatch/candidate.py +51 -0
  16. echotools-1.0.0/src/echotools/dispatch/dispatcher.py +268 -0
  17. echotools-1.0.0/src/echotools/dispatch/selector.py +390 -0
  18. echotools-1.0.0/src/echotools/errors/__init__.py +29 -0
  19. echotools-1.0.0/src/echotools/errors/base.py +32 -0
  20. echotools-1.0.0/src/echotools/errors/classify.py +42 -0
  21. echotools-1.0.0/src/echotools/errors/common.py +74 -0
  22. echotools-1.0.0/src/echotools/events/__init__.py +8 -0
  23. echotools-1.0.0/src/echotools/events/bus.py +104 -0
  24. echotools-1.0.0/src/echotools/events/event.py +31 -0
  25. echotools-1.0.0/src/echotools/files/__init__.py +7 -0
  26. echotools-1.0.0/src/echotools/files/file_util.py +155 -0
  27. echotools-1.0.0/src/echotools/fncall/__init__.py +40 -0
  28. echotools-1.0.0/src/echotools/fncall/parsers/__init__.py +6 -0
  29. echotools-1.0.0/src/echotools/fncall/parsers/stream.py +156 -0
  30. echotools-1.0.0/src/echotools/fncall/parsers/xml_parser.py +320 -0
  31. echotools-1.0.0/src/echotools/fncall/prompt/__init__.py +5 -0
  32. echotools-1.0.0/src/echotools/fncall/prompt/history.py +383 -0
  33. echotools-1.0.0/src/echotools/fncall/prompt/inject.py +108 -0
  34. echotools-1.0.0/src/echotools/fncall/prompt/templates.py +159 -0
  35. echotools-1.0.0/src/echotools/fncall/protocols/__init__.py +28 -0
  36. echotools-1.0.0/src/echotools/fncall/protocols/antml.py +251 -0
  37. echotools-1.0.0/src/echotools/fncall/protocols/bracket.py +154 -0
  38. echotools-1.0.0/src/echotools/fncall/protocols/custom.py +50 -0
  39. echotools-1.0.0/src/echotools/fncall/protocols/nous.py +161 -0
  40. echotools-1.0.0/src/echotools/fncall/protocols/original.py +378 -0
  41. echotools-1.0.0/src/echotools/fncall/protocols/xml.py +249 -0
  42. echotools-1.0.0/src/echotools/fncall/registry.py +96 -0
  43. echotools-1.0.0/src/echotools/fncall/shared/__init__.py +25 -0
  44. echotools-1.0.0/src/echotools/fncall/shared/coercion.py +295 -0
  45. echotools-1.0.0/src/echotools/fncall/shared/loop_detect.py +97 -0
  46. echotools-1.0.0/src/echotools/fncall/shared/normalization.py +170 -0
  47. echotools-1.0.0/src/echotools/fncall/shared/xml_helpers.py +56 -0
  48. echotools-1.0.0/src/echotools/ids/__init__.py +7 -0
  49. echotools-1.0.0/src/echotools/ids/generator.py +62 -0
  50. echotools-1.0.0/src/echotools/io/__init__.py +11 -0
  51. echotools-1.0.0/src/echotools/io/io_utils.py +81 -0
  52. echotools-1.0.0/src/echotools/lifecycle/__init__.py +8 -0
  53. echotools-1.0.0/src/echotools/lifecycle/manager.py +72 -0
  54. echotools-1.0.0/src/echotools/lifecycle/updater.py +176 -0
  55. echotools-1.0.0/src/echotools/logger/__init__.py +12 -0
  56. echotools-1.0.0/src/echotools/logger/manager.py +164 -0
  57. echotools-1.0.0/src/echotools/network/__init__.py +11 -0
  58. echotools-1.0.0/src/echotools/network/http_utils.py +68 -0
  59. echotools-1.0.0/src/echotools/plugin/__init__.py +9 -0
  60. echotools-1.0.0/src/echotools/plugin/base.py +41 -0
  61. echotools-1.0.0/src/echotools/plugin/discovery.py +134 -0
  62. echotools-1.0.0/src/echotools/plugin/registry.py +253 -0
  63. echotools-1.0.0/src/echotools/process/__init__.py +10 -0
  64. echotools-1.0.0/src/echotools/process/port.py +183 -0
  65. echotools-1.0.0/src/echotools/protocol/__init__.py +19 -0
  66. echotools-1.0.0/src/echotools/protocol/base.py +123 -0
  67. echotools-1.0.0/src/echotools/proxy/__init__.py +7 -0
  68. echotools-1.0.0/src/echotools/proxy/manager.py +269 -0
  69. echotools-1.0.0/src/echotools/retry/__init__.py +11 -0
  70. echotools-1.0.0/src/echotools/retry/retry.py +127 -0
  71. echotools-1.0.0/src/echotools/runtime/__init__.py +7 -0
  72. echotools-1.0.0/src/echotools/runtime/collector.py +60 -0
  73. echotools-1.0.0/src/echotools/scheduler/__init__.py +7 -0
  74. echotools-1.0.0/src/echotools/scheduler/scheduler.py +69 -0
  75. echotools-1.0.0/src/echotools/sdk/__init__.py +7 -0
  76. echotools-1.0.0/src/echotools/sdk/facade.py +154 -0
  77. echotools-1.0.0/src/echotools/tracing/__init__.py +28 -0
  78. echotools-1.0.0/src/echotools/tracing/context.py +63 -0
  79. echotools-1.0.0/src/echotools/tracing/span.py +135 -0
  80. echotools-1.0.0/src/echotools/tracing/tracer.py +97 -0
  81. echotools-1.0.0/src/echotools/watcher/__init__.py +7 -0
  82. echotools-1.0.0/src/echotools/watcher/file_watcher.py +110 -0
  83. echotools-1.0.0/src/echotools/web/__init__.py +8 -0
  84. echotools-1.0.0/src/echotools/web/application.py +120 -0
  85. echotools-1.0.0/src/echotools/web/utils.py +186 -0
  86. echotools-1.0.0/src/echotools.egg-info/PKG-INFO +80 -0
  87. echotools-1.0.0/src/echotools.egg-info/SOURCES.txt +95 -0
  88. echotools-1.0.0/src/echotools.egg-info/dependency_links.txt +1 -0
  89. echotools-1.0.0/src/echotools.egg-info/requires.txt +33 -0
  90. echotools-1.0.0/src/echotools.egg-info/top_level.txt +1 -0
  91. echotools-1.0.0/tests/test_cache_retry.py +33 -0
  92. echotools-1.0.0/tests/test_config.py +36 -0
  93. echotools-1.0.0/tests/test_dispatch.py +35 -0
  94. echotools-1.0.0/tests/test_events.py +35 -0
  95. echotools-1.0.0/tests/test_plugin.py +31 -0
  96. echotools-1.0.0/tests/test_protocols.py +22 -0
  97. echotools-1.0.0/tests/test_tracing.py +30 -0
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: echotools
3
+ Version: 1.0.0
4
+ Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
+ Author: nichengfuben
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/nichengfuben/echotools
8
+ Project-URL: Repository, https://github.com/nichengfuben/echotools
9
+ Project-URL: Issues, https://github.com/nichengfuben/echotools/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: typing-extensions>=4.7.0
24
+ Provides-Extra: toml
25
+ Requires-Dist: tomlkit>=0.11.0; extra == "toml"
26
+ Requires-Dist: tomli>=2.0.0; python_version < "3.11" and extra == "toml"
27
+ Provides-Extra: watch
28
+ Requires-Dist: watchdog>=3.0.0; extra == "watch"
29
+ Provides-Extra: http
30
+ Requires-Dist: aiohttp>=3.8.0; extra == "http"
31
+ Provides-Extra: socks
32
+ Requires-Dist: aiohttp-socks>=0.8.0; extra == "socks"
33
+ Provides-Extra: all
34
+ Requires-Dist: tomlkit>=0.11.0; extra == "all"
35
+ Requires-Dist: tomli>=2.0.0; python_version < "3.11" and extra == "all"
36
+ Requires-Dist: watchdog>=3.0.0; extra == "all"
37
+ Requires-Dist: aiohttp>=3.8.0; extra == "all"
38
+ Requires-Dist: aiohttp-socks>=0.8.0; extra == "all"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
41
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
42
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
43
+ Requires-Dist: mypy>=1.5.0; extra == "dev"
44
+ Requires-Dist: black>=23.0.0; extra == "dev"
45
+ Requires-Dist: isort>=5.12.0; extra == "dev"
46
+
47
+ # echotools
48
+
49
+ 通用基础设施 SDK:配置中心、日志、事件总线、调用链、任务调度、
50
+ 插件框架、协议系统、自适应选择器,完全项目无关,兼容 Python 3.8-3.14。
51
+
52
+ ## 安装
53
+
54
+ pip install echotools[all]
55
+
56
+ ## 快速开始
57
+
58
+ from echotools import EchoTools
59
+
60
+ et = EchoTools(service_name="myapp")
61
+ et.logger.configure(level="INFO", color=True)
62
+ cfg = et.config
63
+ cfg.load("config.toml")
64
+
65
+ with et.tracer.trace("request") as trace:
66
+ with et.tracer.span(trace, "db") as span:
67
+ span.set_tag("query", "select 1")
68
+
69
+ ## 能力总览
70
+
71
+ - ConfigCenter 点路径配置 + 热重载 + 类型绑定
72
+ - LoggerManager 调用链注入 + 颜色 + 轮转
73
+ - EventBus 同步/异步事件
74
+ - Tracer 轻量调用链
75
+ - TaskDispatcher 单发/竞速 + 自适应选择
76
+ - PluginRegistry 自动发现 + 热重载
77
+ - get_protocol XML/antml/original/bracket/nous/custom 协议
78
+ - ProxyManager HTTP/HTTPS/SOCKS
79
+ - AutoUpdater git 自动更新
80
+ - FileWatcher 轮询文件监视
@@ -0,0 +1,34 @@
1
+ # echotools
2
+
3
+ 通用基础设施 SDK:配置中心、日志、事件总线、调用链、任务调度、
4
+ 插件框架、协议系统、自适应选择器,完全项目无关,兼容 Python 3.8-3.14。
5
+
6
+ ## 安装
7
+
8
+ pip install echotools[all]
9
+
10
+ ## 快速开始
11
+
12
+ from echotools import EchoTools
13
+
14
+ et = EchoTools(service_name="myapp")
15
+ et.logger.configure(level="INFO", color=True)
16
+ cfg = et.config
17
+ cfg.load("config.toml")
18
+
19
+ with et.tracer.trace("request") as trace:
20
+ with et.tracer.span(trace, "db") as span:
21
+ span.set_tag("query", "select 1")
22
+
23
+ ## 能力总览
24
+
25
+ - ConfigCenter 点路径配置 + 热重载 + 类型绑定
26
+ - LoggerManager 调用链注入 + 颜色 + 轮转
27
+ - EventBus 同步/异步事件
28
+ - Tracer 轻量调用链
29
+ - TaskDispatcher 单发/竞速 + 自适应选择
30
+ - PluginRegistry 自动发现 + 热重载
31
+ - get_protocol XML/antml/original/bracket/nous/custom 协议
32
+ - ProxyManager HTTP/HTTPS/SOCKS
33
+ - AutoUpdater git 自动更新
34
+ - FileWatcher 轮询文件监视
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "echotools"
7
+ version = "1.0.0"
8
+ description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "nichengfuben" }]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Libraries",
25
+ ]
26
+ dependencies = [
27
+ "typing-extensions>=4.7.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ toml = ["tomlkit>=0.11.0", "tomli>=2.0.0; python_version < '3.11'"]
32
+ watch = ["watchdog>=3.0.0"]
33
+ http = ["aiohttp>=3.8.0"]
34
+ socks = ["aiohttp-socks>=0.8.0"]
35
+ all = [
36
+ "tomlkit>=0.11.0",
37
+ "tomli>=2.0.0; python_version < '3.11'",
38
+ "watchdog>=3.0.0",
39
+ "aiohttp>=3.8.0",
40
+ "aiohttp-socks>=0.8.0",
41
+ ]
42
+ dev = [
43
+ "pytest>=7.4.0",
44
+ "pytest-asyncio>=0.23.0",
45
+ "pytest-cov>=4.1.0",
46
+ "mypy>=1.5.0",
47
+ "black>=23.0.0",
48
+ "isort>=5.12.0",
49
+ ]
50
+
51
+ [project.urls]
52
+ Homepage = "https://github.com/nichengfuben/echotools"
53
+ Repository = "https://github.com/nichengfuben/echotools"
54
+ Issues = "https://github.com/nichengfuben/echotools/issues"
55
+
56
+ [tool.black]
57
+ line-length = 88
58
+ target-version = ["py38", "py39", "py310", "py311", "py312", "py313"]
59
+
60
+ [tool.isort]
61
+ profile = "black"
62
+ line_length = 88
63
+ known_first_party = ["echotools"]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ asyncio_mode = "auto"
68
+ addopts = ["--strict-markers", "--tb=short"]
69
+
70
+ [tool.coverage.run]
71
+ source = ["src/echotools"]
72
+ branch = true
73
+ omit = ["*/tests/*", "*/__init__.py"]
74
+
75
+ [tool.coverage.report]
76
+ fail_under = 90
77
+ exclude_lines = [
78
+ "pragma: no cover",
79
+ "def __repr__",
80
+ "if TYPE_CHECKING:",
81
+ "raise NotImplementedError",
82
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]